Siel_Acumulus - Version 4.3.0

Version Notes

See changelog.txt

Download this release

Release Info

Developer SIEL Acumulus
Extension Siel_Acumulus
Version 4.3.0
Comparing to
See all releases


Version 4.3.0

Files changed (137) hide show
  1. app/code/community/Siel/Acumulus/Block/Adminhtml/Form.php +59 -0
  2. app/code/community/Siel/Acumulus/Block/Adminhtml/Form/Form.php +43 -0
  3. app/code/community/Siel/Acumulus/Helper/CompiledMagentoAutoLoader.php +16 -0
  4. app/code/community/Siel/Acumulus/Helper/Data.php +60 -0
  5. app/code/community/Siel/Acumulus/Model/Entry.php +46 -0
  6. app/code/community/Siel/Acumulus/Model/Order/Observer.php +67 -0
  7. app/code/community/Siel/Acumulus/Model/Resource/Entry.php +15 -0
  8. app/code/community/Siel/Acumulus/Model/Resource/Entry/Collection.php +17 -0
  9. app/code/community/Siel/Acumulus/Model/Resource/Setup.php +47 -0
  10. app/code/community/Siel/Acumulus/changelog.txt +275 -0
  11. app/code/community/Siel/Acumulus/controllers/Adminhtml/AcumulusController.php +75 -0
  12. app/code/community/Siel/Acumulus/etc/adminhtml.xml +41 -0
  13. app/code/community/Siel/Acumulus/etc/config.xml +102 -0
  14. app/code/community/Siel/Acumulus/events.txt +230 -0
  15. app/code/community/Siel/Acumulus/leesmij.txt +37 -0
  16. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Countries.php +365 -0
  17. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Form.php +590 -0
  18. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/FormRenderer.php +813 -0
  19. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Log.php +210 -0
  20. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/MailTranslations.php +153 -0
  21. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Mailer.php +176 -0
  22. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Number.php +81 -0
  23. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Requirements.php +36 -0
  24. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/TranslationCollection.php +34 -0
  25. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Translator.php +51 -0
  26. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/TranslatorInterface.php +40 -0
  27. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Completor.php +786 -0
  28. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorInvoiceLines.php +299 -0
  29. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/ApplySameVatRate.php +61 -0
  30. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/Fail.php +21 -0
  31. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitKnownDiscountLine.php +128 -0
  32. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitLine.php +233 -0
  33. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitNonMatchingLine.php +119 -0
  34. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/TryAllVatRatePermutations.php +112 -0
  35. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategyBase.php +344 -0
  36. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategyLines.php +177 -0
  37. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/ConfigInterface.php +102 -0
  38. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Creator.php +908 -0
  39. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Source.php +137 -0
  40. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Translations.php +69 -0
  41. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/FormRenderer.php +45 -0
  42. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/Log.php +56 -0
  43. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/Mailer.php +29 -0
  44. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Invoice/Creator.php +321 -0
  45. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Invoice/Source.php +51 -0
  46. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Shop/ConfigForm.php +40 -0
  47. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Shop/InvoiceManager.php +79 -0
  48. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/AcumulusEntryModel.php +107 -0
  49. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/BatchForm.php +39 -0
  50. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/ConfigForm.php +23 -0
  51. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/ConfigStore.php +94 -0
  52. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/InvoiceManager.php +120 -0
  53. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Invoice/Creator.php +448 -0
  54. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Invoice/Source.php +52 -0
  55. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Shop/ConfigForm.php +41 -0
  56. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Shop/InvoiceManager.php +84 -0
  57. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/FormMapper.php +222 -0
  58. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/Log.php +46 -0
  59. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/Mailer.php +27 -0
  60. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Completor.php +55 -0
  61. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Creator.php +527 -0
  62. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Source.php +95 -0
  63. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/AcumulusEntryModel.php +104 -0
  64. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/BatchForm.php +49 -0
  65. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/ConfigForm.php +45 -0
  66. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/ConfigStore.php +74 -0
  67. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/InvoiceManager.php +111 -0
  68. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Log.php +22 -0
  69. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Mailer.php +43 -0
  70. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/OcHelper.php +319 -0
  71. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Registry.php +96 -0
  72. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Invoice/Creator.php +352 -0
  73. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Invoice/Source.php +41 -0
  74. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Helpers/FormRenderer.php +27 -0
  75. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Helpers/OcHelper.php +12 -0
  76. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Shop/InvoiceManager.php +39 -0
  77. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Helpers/FormRenderer.php +51 -0
  78. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Helpers/OcHelper.php +96 -0
  79. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Shop/InvoiceManager.php +50 -0
  80. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/AcumulusEntryModel.php +163 -0
  81. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/BatchForm.php +27 -0
  82. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/ConfigForm.php +59 -0
  83. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/ConfigStore.php +71 -0
  84. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/InvoiceManager.php +86 -0
  85. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/FormMapper.php +199 -0
  86. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/Log.php +45 -0
  87. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/Mailer.php +73 -0
  88. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/.htaccess +14 -0
  89. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/en/message.html +0 -0
  90. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/en/message.txt +0 -0
  91. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/nl/message.html +0 -0
  92. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/nl/message.txt +0 -0
  93. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Invoice/Creator.php +499 -0
  94. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Invoice/Source.php +95 -0
  95. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/AcumulusEntryModel.php +107 -0
  96. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/BatchForm.php +45 -0
  97. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/ConfigForm.php +115 -0
  98. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/ConfigStore.php +73 -0
  99. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/InvoiceManager.php +115 -0
  100. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/AcumulusEntryModel.php +148 -0
  101. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchForm.php +254 -0
  102. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchFormTranslations.php +125 -0
  103. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchTranslations.php +32 -0
  104. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/Config.php +808 -0
  105. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigForm.php +647 -0
  106. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigFormTranslations.php +286 -0
  107. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigInterface.php +26 -0
  108. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigStore.php +46 -0
  109. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigStoreInterface.php +45 -0
  110. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfirmUninstallForm.php +70 -0
  111. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfirmUninstallFormTranslations.php +20 -0
  112. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/InjectorInterface.php +84 -0
  113. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/InvoiceManager.php +429 -0
  114. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ModuleTranslations.php +60 -0
  115. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Communicator.php +487 -0
  116. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/CommunicatorLocal.php +48 -0
  117. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/ConfigInterface.php +117 -0
  118. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Service.php +412 -0
  119. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Translations.php +36 -0
  120. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/FormMapper.php +81 -0
  121. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/FormRenderer.php +118 -0
  122. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/Log.php +23 -0
  123. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/Mailer.php +32 -0
  124. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Invoice/Creator.php +486 -0
  125. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Invoice/Source.php +57 -0
  126. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/AcumulusEntryModel.php +150 -0
  127. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/ConfigForm.php +67 -0
  128. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/ConfigStore.php +61 -0
  129. app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/InvoiceManager.php +181 -0
  130. app/code/community/Siel/Acumulus/libraries/Siel/leesmij.txt +54 -0
  131. app/code/community/Siel/Acumulus/libraries/Siel/psr4.php +28 -0
  132. app/code/community/Siel/Acumulus/license.txt +674 -0
  133. app/code/community/Siel/Acumulus/licentie-nl.pdf +0 -0
  134. app/code/community/Siel/Acumulus/sql/acumulus_setup/install-4.0.0.php +8 -0
  135. app/code/community/Siel/Acumulus/sql/acumulus_setup/upgrade-3.4.4-4.0.0.php +45 -0
  136. app/etc/modules/Siel_Acumulus.xml +9 -0
  137. package.xml +32 -0
app/code/community/Siel/Acumulus/Block/Adminhtml/Form.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class Siel_Acumulus_Block_Adminhtml_Form extends Mage_Adminhtml_Block_Widget_Form_Container {
4
+
5
+ /** @var Siel_Acumulus_Helper_Data */
6
+ protected $helper;
7
+
8
+ /** @var string */
9
+ protected $formType;
10
+
11
+ /**
12
+ * Constructor.
13
+ *
14
+ * @param array $args
15
+ */
16
+ public function __construct(array $args = array()) {
17
+ $this->helper = Mage::helper('acumulus');
18
+ $this->formType = $args['formType'];
19
+
20
+ parent::__construct();
21
+
22
+ $this->_blockGroup = 'acumulus';
23
+ $this->_controller = 'adminhtml_form';
24
+ $this->_removeButton('delete');
25
+ $this->_removeButton('back');
26
+ if ($this->formType === 'batch') {
27
+ $this->_updateButton('save', 'label', $this->t('button_send'));
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Helper method to translate strings.
33
+ *
34
+ * @param string $key
35
+ * The key to get a translation for.
36
+ *
37
+ * @return string
38
+ * The translation for the given key or the key itself if no translation
39
+ * could be found.
40
+ */
41
+ protected function t($key) {
42
+ return $this->helper->t($key);
43
+ }
44
+
45
+ protected function _prepareLayout()
46
+ {
47
+ // Don't let the parent create the block.
48
+ $old = $this->_mode;
49
+ $this->_mode = '';
50
+ $this->setChild('form', $this->getLayout()->createBlock('acumulus/adminhtml_form_form', '', array('formType' => $this->formType)));
51
+ $result = parent::_prepareLayout();
52
+ $this->_mode = $old;
53
+ return $result;
54
+ }
55
+
56
+ public function getHeaderText() {
57
+ return $this->t("{$this->formType}_form_header");
58
+ }
59
+ }
app/code/community/Siel/Acumulus/Block/Adminhtml/Form/Form.php ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use Siel\Acumulus\Magento\Helpers\FormMapper;
4
+
5
+ class Siel_Acumulus_Block_Adminhtml_Form_Form extends Mage_Adminhtml_Block_Widget_Form {
6
+
7
+ /** @var Siel_Acumulus_Helper_Data */
8
+ protected $helper = NULL;
9
+
10
+ /** @var string */
11
+ protected $formType;
12
+
13
+ /**
14
+ * Siel_Acumulus_Block_Adminhtml_Form constructor.
15
+ *
16
+ * @param array $attributes
17
+ */
18
+ public function __construct(array $attributes = array()) {
19
+ $this->helper = Mage::helper('acumulus');
20
+ $this->formType = $attributes['formType'];
21
+ parent::__construct($attributes);
22
+ }
23
+
24
+ protected function _prepareForm() {
25
+ $acumulusForm = $this->helper-> getAcumulusConfig()->getForm($this->formType);
26
+ $form = new Varien_Data_Form();
27
+ $mapper = new FormMapper();
28
+ $mapper->map($form, $acumulusForm->getFields());
29
+
30
+ $form->setValues($acumulusForm->getFormValues());
31
+ /** @noinspection PhpUndefinedMethodInspection */
32
+ $form->setAction($this->getUrl("*/*/{$this->formType}"));
33
+ /** @noinspection PhpUndefinedMethodInspection */
34
+ $form->setMethod('post');
35
+ /** @noinspection PhpUndefinedMethodInspection */
36
+ $form->setUseContainer(true);
37
+ $form->setId('edit_form');
38
+ $this->setForm($form);
39
+
40
+ return parent::_prepareForm();
41
+ }
42
+
43
+ }
app/code/community/Siel/Acumulus/Helper/CompiledMagentoAutoLoader.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @file This file registers an autoloader for the Siel namespace library when
4
+ * Magento has been compiled.
5
+ */
6
+ namespace Siel;
7
+
8
+ // Prepend this autoloader: it will not throw, nor warn, while the shop specific
9
+ // autoloader will do so.
10
+ spl_autoload_register(function($class) {
11
+ if (strpos($class, __NAMESPACE__ . '\\') === 0) {
12
+ $fileName = __DIR__ . DIRECTORY_SEPARATOR . 'Siel_Acumulus_libraries_' . str_replace('\\', '_', $class) . '.php';
13
+ /** @noinspection PhpIncludeInspection */
14
+ include($fileName);
15
+ }
16
+ }, FALSE, TRUE);
app/code/community/Siel/Acumulus/Helper/Data.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ use Siel\Acumulus\Shop\Config;
3
+
4
+ class Siel_Acumulus_Helper_Data extends Mage_Core_Helper_Abstract {
5
+
6
+ /** @var \Siel\Acumulus\Shop\Config */
7
+ protected static $acumulusConfig = NULL;
8
+
9
+ /**
10
+ * Siel_Acumulus_Helper_Data constructor.
11
+ */
12
+ public function __construct() {
13
+ $this->init();
14
+ }
15
+
16
+ /**
17
+ * Helper method that initializes our environment:
18
+ * - autoloader for the library part.
19
+ * - translator
20
+ * - acumulusConfig
21
+ */
22
+ protected function init() {
23
+ if (static::$acumulusConfig === NULL) {
24
+ // Our library structure is incompatible with autoload in Magento: we
25
+ // register our own auto loader.
26
+ $acumulusDir = dirname(dirname(__FILE__));
27
+ if (!@include_once($acumulusDir . '/libraries/Siel/psr4.php')) {
28
+ // Magento has been "compiled", use a more specific autoloader.
29
+ require_once(dirname(__FILE__) . '/CompiledMagentoAutoLoader.php');
30
+ }
31
+ static::$acumulusConfig = new Config('Magento', Mage::app()->getLocale()->getLocaleCode());
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Helper method to translate strings.
37
+ *
38
+ * @param string $key
39
+ * The key to get a translation for.
40
+ *
41
+ * @return string
42
+ * The translation for the given key or the key itself if no translation
43
+ * could be found.
44
+ */
45
+ public function t($key) {
46
+ return static::$acumulusConfig->getTranslator()->get($key);
47
+ }
48
+
49
+ /**
50
+ * Returns the configuration settings object central to this extension.
51
+ *
52
+ * @return \Siel\Acumulus\Shop\Config
53
+ * The Acumulus config.
54
+ */
55
+ public function getAcumulusConfig() {
56
+ $this->init();
57
+ return static::$acumulusConfig;
58
+ }
59
+
60
+ }
app/code/community/Siel/Acumulus/Model/Entry.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php /** @noinspection PhpLanguageLevelInspection */
2
+
3
+ /**
4
+ * Class Siel_Acumulus_Model_Entry Acumulus is used by
5
+ * Siel\Acumulus\Shop\Magento\AcumulusEntryModel to access the entry table from
6
+ * the database.
7
+ *
8
+ * @method $this setEntryId(int $value)
9
+ * @method int getEntryId()
10
+ * @method $this setToken(string $value)
11
+ * @method string getToken()
12
+ * @method $this setSourceType(string $value)
13
+ * @method string getSourceType()
14
+ * @method $this setSourceId(int $value)
15
+ * @method int getSourceId()
16
+ * @method $this setCreated(int $value)
17
+ * @method int getCreated()
18
+ * @method $this setUpdated(int $value)
19
+ * @method int getUpdated()
20
+ */
21
+ class Siel_Acumulus_Model_Entry extends Mage_Core_Model_Abstract {
22
+
23
+ /**
24
+ * Magento "internal constructor" not receiving any parameters.
25
+ */
26
+ protected function _construct() {
27
+ $this->_init('acumulus/entry');
28
+ }
29
+
30
+ /**
31
+ * Overrides the save() method to clear the created column and set the updated
32
+ * column before being written to the database. The created timestamp is set
33
+ * by the database and should not be set by the application. As MySQl < 5.6.5
34
+ * only allows one timestamp with a default value, we do set the updated
35
+ * timestamp in code (http://stackoverflow.com/a/17498167/1475662).
36
+ *
37
+ * @return $this
38
+ */
39
+ public function save() {
40
+ $this
41
+ ->setUpdated(Mage::app()->getLocale()->storeTimeStamp())
42
+ ->unsetData('created');
43
+ return parent::save();
44
+ }
45
+
46
+ }
app/code/community/Siel/Acumulus/Model/Order/Observer.php ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use Siel\Acumulus\Invoice\Source;
4
+ use Siel\Acumulus\Web\ConfigInterface as WebConfigInterface;
5
+
6
+ class Siel_Acumulus_Model_Order_Observer extends Mage_Core_Model_Abstract {
7
+
8
+ /** @var Siel_Acumulus_Helper_Data */
9
+ protected $helper;
10
+
11
+ public function __construct() {
12
+ $this->helper = Mage::helper('acumulus');
13
+ parent::__construct();
14
+ }
15
+
16
+ /**
17
+ * Event handler for the sales_order_save_after event.
18
+ *
19
+ * @param Varien_Event_Observer $observer
20
+ *
21
+ * @return bool
22
+ */
23
+ public function orderSaveAfter(Varien_Event_Observer $observer) {
24
+ /** @var Varien_Event $event */
25
+ $event = $observer->getEvent();
26
+ /** @var Mage_Sales_Model_Order $order */
27
+ /** @noinspection PhpUndefinedMethodInspection */
28
+ $order = $event->getOrder();
29
+ $source = $this->helper->getAcumulusConfig()->getSource(Source::Order, $order);
30
+ return $this->helper->getAcumulusConfig()->getManager()->sourceStatusChange($source) !== WebConfigInterface::Status_Exception;
31
+ }
32
+
33
+ /**
34
+ * Event handler for the sales_order_creditmemo_save_after event.
35
+ *
36
+ * @param Varien_Event_Observer $observer
37
+ *
38
+ * @return bool
39
+ */
40
+ public function creditMemoSaveAfter(Varien_Event_Observer $observer) {
41
+ /** @var Varien_Event $event */
42
+ $event = $observer->getEvent();
43
+ /** @var Mage_Sales_Model_Order_Creditmemo $creditMemo */
44
+ /** @noinspection PhpUndefinedMethodInspection */
45
+ $creditMemo = $event->getCreditmemo();
46
+ $source = $this->helper->getAcumulusConfig()->getSource(Source::CreditNote, $creditMemo);
47
+ return $this->helper->getAcumulusConfig()->getManager()->sourceStatusChange($source) !== WebConfigInterface::Status_Exception;
48
+ }
49
+
50
+ /**
51
+ * Event handler for the sales_order_invoice_save_after event.
52
+ *
53
+ * @param Varien_Event_Observer $observer
54
+ *
55
+ * @return bool
56
+ */
57
+ public function invoiceSaveAfter(Varien_Event_Observer $observer) {
58
+ /** @var Varien_Event $event */
59
+ $event = $observer->getEvent();
60
+ /** @var Mage_Sales_Model_Order_Invoice $invoice */
61
+ /** @noinspection PhpUndefinedMethodInspection */
62
+ $invoice = $event->getInvoice();
63
+ $source = $this->helper->getAcumulusConfig()->getSource(Source::Order, $invoice->getOrderId());
64
+ return $this->helper->getAcumulusConfig()->getManager()->invoiceCreate($source) !== WebConfigInterface::Status_Exception;
65
+ }
66
+
67
+ }
app/code/community/Siel/Acumulus/Model/Resource/Entry.php ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Acumulus entry resource
5
+ */
6
+ class Siel_Acumulus_Model_Resource_Entry extends Mage_Core_Model_Resource_Db_Abstract {
7
+
8
+ /**
9
+ * Magento "internal constructor" not receiving any parameters.
10
+ */
11
+ protected function _construct() {
12
+ $this->_init('acumulus/entry', 'entity_id');
13
+ }
14
+
15
+ }
app/code/community/Siel/Acumulus/Model/Resource/Entry/Collection.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Acumulus entry collection class is used by
5
+ * Siel\Acumulus\Shop\Magento\AcumulusEntryModel to retrieve record sets from
6
+ * the database.
7
+ */
8
+ class Siel_Acumulus_Model_Resource_Entry_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract {
9
+
10
+ /**
11
+ * Magento "internal constructor" not receiving any parameters.
12
+ */
13
+ protected function _construct() {
14
+ $this->_init('acumulus/entry');
15
+ }
16
+
17
+ }
app/code/community/Siel/Acumulus/Model/Resource/Setup.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Acumulus resource setup
5
+ */
6
+ class Siel_Acumulus_Model_Resource_Setup extends Mage_Core_Model_Resource_Setup {
7
+
8
+ /**
9
+ * Defines the table definition.
10
+ *
11
+ * Called by the install script and the update script.
12
+ *
13
+ * @return Varien_Db_Ddl_Table
14
+ */
15
+ public function getTableDefinition() {
16
+ $table = $this->getConnection()->newTable($this->getTable('acumulus/entry'))
17
+ ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
18
+ 'unsigned' => true,
19
+ 'nullable' => false,
20
+ 'primary' => true,
21
+ 'identity' => true,
22
+ ), 'Technical key')
23
+ ->addColumn('entry_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
24
+ 'unsigned' => true,
25
+ 'nullable' => false,
26
+ ), 'Acumulus entry id')
27
+ ->addColumn('token', Varien_Db_Ddl_Table::TYPE_CHAR, 32, array(
28
+ 'nullable' => false,
29
+ ), 'Acumulus invoice token')
30
+ ->addColumn('source_type', Varien_Db_Ddl_Table::TYPE_VARCHAR, 20, array(
31
+ 'nullable' => false,
32
+ ), 'Invoice source type')
33
+ ->addColumn('source_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
34
+ 'unsigned' => true,
35
+ 'nullable' => true,
36
+ ), 'Magento invoice source id')
37
+ ->addColumn('created', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
38
+ 'default' => Varien_Db_Ddl_Table::TIMESTAMP_INIT,
39
+ ), 'Timestamp created')
40
+ ->addColumn('updated', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(), 'Timestamp updated')
41
+ ->addIndex('siel_acumulus_entry_id', 'entry_id', array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
42
+ ->addIndex('siel_acumulus_source', array('source_type', 'source_id'), array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
43
+ ->setComment('Acumulus entry table');
44
+ return $table;
45
+ }
46
+
47
+ }
app/code/community/Siel/Acumulus/changelog.txt ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Changelog Acumulus webkoppelingen
2
+ =================================
3
+ ALL = Alle webshops.
4
+ HS = HikaShop (Joomla)
5
+ MA = Magento
6
+ OC = OpenCart 1
7
+ OC2 = OpenCart 2
8
+ PS = PrestaShop
9
+ VM3 = VirtueMart3 (Joomla)
10
+ WC = WooCommerce (WordPress)
11
+
12
+ 4.3.0 (2016-03-08)
13
+ ------------------
14
+ - ALL: Op PHP5.3: Fatal error: Uncaught exception 'ReflectionException' with
15
+ message 'Class Siel\Acumulus\WooCommerce\Shop\AcumulusEntryModel does not
16
+ have a constructor, so you cannot pass any constructor arguments' in
17
+ /wp-content/plugins/acumulus/libraries/Siel/Acumulus/Shop/Config.php:211.
18
+ - OC1: Eerste versie op deze library: Europese BTW tarieven; Batch verzending.
19
+ Nog geen refunds.
20
+ - OC2: Typo die voorkomt dat coupon regels gesplitst kunnen worden over
21
+ verschillende BTW tarieven.
22
+ - OC2: Typo die voorkomt dat het acumulus.invoice.sent event uitgevoerd word.
23
+ - ALL: Waarden voor de instellingen "moment van versturen" en "Bestelstatus"
24
+ worden niet getoond op formulier (wel opgeslagen en gebruikt).
25
+ - WC: Class batchform verwijderd, base batchform is niet meer abstract.
26
+ - PS: PS specifieke code volgens standards van https://addons.prestashop.com.
27
+ - ALL: Code nu volgens PSR-2 coding standard opgemaakt.
28
+
29
+ 4.2.1 (2016-02-24)
30
+ ------------------
31
+ - PS: Form submit van instellingenformulier wordt niet herkend en verwerkt.
32
+ - WC: Variabele producten waarbij de varities niet als aparte SKUs aangemaakt
33
+ zijn kwamen zonder variantomschrijving terecht op de factuurregels.
34
+ - ALL: Nieuwe optie toegevoegd om facturen met verlegde BTW te kunnen
35
+ onderscheiden van facturen met alleen BTW vrije producten. Dit voorkomt een
36
+ hoop waarschuwingen bij de meeste winkels die toch geen BTW vrije producten
37
+ of diensten leveren.
38
+
39
+ 4.2.0 (2016-02-16)
40
+ ------------------
41
+ - ALL: De completor kijkt nu naar de bestellingstotalen en de totalen van de
42
+ afzonderlijke bestellingsregels en voegt een correctieregel toe als deze niet
43
+ gelijk zijn. Dit leverde fouten in verschillende plugins op, met name bij
44
+ kortingen in refunds:
45
+ * MA: BTW op korting op verzendkosten in een credit note worden niet
46
+ opgeslagen, bereken aan de hand van ontbrekende bedragen.
47
+ * PS: Kortingsbedrag (ex BTW) op een order dat teruggevorderd wordt op een
48
+ credit slip is niet bekend: bereken aan de hand van het matchen van
49
+ ontbrekende bedragen met kortingen op de oorspronkelijke order.
50
+ * VM3: Verbeterde behandeling van kortingen zowel via calc rules als coupons.
51
+ - ALL: Sla geen leeg wachtwoord op (staat toe dat, eenmaal ingevuld, het niet
52
+ meer naar en van de browser verstuurd hoeft te worden).
53
+ - ALL: type fout waardoor removeEmptyShipping() niet correct werkte.
54
+ - PS: Mogelijke PHP warning als een order via batchformulier verzonden wordt.
55
+ - MA: Foute verzendkosten werden verstuurd als een korting ook van toepassing is
56
+ op die verzendkosten.
57
+ - MA: Payment fee kan vooralsnog niet worden herkend: getPaymentchargeAmount()
58
+ is een niet bestaande method.
59
+ - ALL: Source:;getStatus() toegevoegd. allows further refactorings.
60
+ - ALL: refactorings, waaronder
61
+ * hernoemen van een aantal 'meta-...' tags.
62
+ * introductie en gebruik van ShopConfig::getForm().
63
+ * introductie en gebruik van Source:;getStatus().
64
+ - OC2: bug fixing.
65
+
66
+ 4.2.0-beta9 (2016-01-25)
67
+ ------------------------
68
+ - OC2: Eerste versie op deze library: Europese BTW tarieven; Batch verzending.
69
+ Nog geen refunds.
70
+ - ALL: Verbeterde verwerking van facturen die (mogelijk of zeker) gesplitst
71
+ moeten worden. Als er meerdere factuurtypes mogelijk zijn, wordt er 1
72
+ conceptfactuur verstuurd die in Acumulus gecorrigeerd en evt gesplitst moet
73
+ worden.
74
+ - HS: zelf aangemaakte statussen werden niet herkend bij een orderstatuswijziging.
75
+
76
+ 4.2.0-beta8 (2016-01-18)
77
+ ------------------------
78
+ - ALL: Optie toegevoegd om aan te geven of u in uw winkel digitale diensten
79
+ aanbiedt. Dit wordt gebruikt voor een betere selectie van en controle op de
80
+ mogelijke BTW types. GEEF DEZE OPTIE EEN WAARDE IN HET INSTELLINGENSCHERM!
81
+ - WC: Klikbare link naar de changelog in de WP readme.txt.
82
+ - WC: filters.txt aangepast zodat huidige parameters en hun type beschreven
83
+ worden.
84
+ - PS: do no longer update not changed fields id_shop and id_shop_group.
85
+
86
+
87
+ 4.2.0-beta7 (2016-01-13)
88
+ ------------------------
89
+ - WC: Gebruik WC logging ipv de wel erg basic (= afwezige) WP logging. Acumulus
90
+ heeft nu zijn eigen log onder WooCommerce - Systeem status - Logs.
91
+ - ALL: Verwijder dubbele email adresseringen en display names.
92
+ - ALL: Verbeterde verwerking van gelijke BTW percentages (b.v. BE en NL 21%).
93
+ Als er meerdere factuurtypes mogelijk zijn wordt er 1 conceptfactuur verstuurd
94
+ die in Acumulus gecorrigeerd en evt gesplitst kan worden.
95
+ - ALL: Meer logging.
96
+ - HS/VM3: log message (gebruikt voor debugging) verwijderd.
97
+
98
+ 4.2.0-beta6 (2016-01-11)
99
+ ------------------------
100
+ - HS/VM3: Logging werkte alleen als Joomla in debug modus is en ook dan nog niet
101
+ voor debug messages. Acumulus heeft nu zijn eigen log file.
102
+ - ALL: empty <line> in XML als item with key = 0 is verwijderd, bv. door de
103
+ "Verzend geen gratis verzending regels" optie.
104
+
105
+ 4.2.0-beta5 (2016-01-07)
106
+ ------------------------
107
+ - HS/VM3: Corrected bad fix introduced in 4.2.0-beta3 (PHP5.5 constructs).
108
+
109
+ 4.2.0-beta4 (2016-01-06)
110
+ ------------------------
111
+ - WC, PS, MA: Uncaught exception 'ReflectionException' with message 'Class
112
+ Siel\Acumulus\WooCommerce\Shop\ConfigStore does not have a constructor, so you
113
+ cannot pass any constructor arguments' (error introduced in 4.2.0-beta2).
114
+ - WC: Hoodlettergevoeligheid bij doorgeven van namespace. Geeft: Fatal error:
115
+ Cannot instantiate abstract class Siel\Acumulus\Shop\InvoiceManager in
116
+ /Siel/Acumulus/Shop/Config.php on line 203 (error introduced in 4.2.0-alpha1).
117
+
118
+ 4.2.0-beta3 (2016-01-05)
119
+ ------------------------
120
+ - VM3,HS: PHP5.5 constructs in Joomla\Shop\InvoiceManager.php.
121
+ - WC: Added to Wordpress.org
122
+
123
+ 4.2.0-beta2 (2016-01-05)
124
+ ------------------------
125
+ - VM3,HS: Joomla componenten mogen dezelfde naam hebben.
126
+
127
+ 4.2.0-beta1 (2016-01-04)
128
+ ------------------------
129
+ - HS: Orders verzenden werkt, refunds nog niet
130
+
131
+ 4.2.0-alpha5 (2015-12-31)
132
+ -------------------------
133
+ - VM3,HS: Meer logging tijdens installeren.
134
+ - PS: Ondersteun versies voor 1.6.0.x door te controleren of getOrderDetailTaxes
135
+ wel aanwezig is.
136
+ - HS: customer gedeelte van facturen zo nu correct moeten werken.
137
+
138
+ 4.2.0-alpha3 (2015-12-28)
139
+ -------------------------
140
+ - VM3: Define and trigger Joomla events onAcumulusInvoice[Created|Completed|Sent].
141
+ - WC: Added readme.txt conform the wordpress.org instructions.
142
+ - WC: Add our own manual line to the manual lines of the system instead of
143
+ replacing them.
144
+ - VM3: return type van AcumulusEntryModel::getByEntryId en getByInvoiceSourceId
145
+ waren fout.
146
+ - VM3: Sta selecteren statussen om te verzenden toe op ConfigForm.
147
+
148
+ 4.2.0-alpha1
149
+ ------------
150
+ - Groot aantal refactorings om ondersteuning voor HikaShop toe te voegen. Code
151
+ die geen gebruik maakt van VM3 functies maar alleen maar van Joomla functies
152
+ is apart gezet.
153
+ - VM3: Ondersteuning voor automatische versturing.
154
+ - VM3: return type van AcumulusEntryModel::getByEntryId en getByInvoiceSourceId
155
+ waren fout, maar worden (nog) niet gebruikt.
156
+ - PS,WC: handmatig ingevoerde regels bij refunds konden genegeerd worden.
157
+ - VM3: Error in install script: readme-nl.txt is hernoemd naar leesmij.txt.
158
+ - PS: De koppeling werkt ook met PS1.5. Minimale versie requirement aangepast.
159
+ - ALL: Prevent autoloader warnings when file does not exist.
160
+ - VM3: Parse error: syntax error, unexpected T_OBJECT_OPERATOR in
161
+ com_acumulusInstallerScript.php on line 68: PHP5.3 incompatibiliteit.
162
+
163
+ 4.1.1 (2015-11-07)
164
+ ------------------
165
+ - ALL: Batchverzending: onterechte/verkeerde melding "niet verzonden omdat
166
+ factuur al verzonden is" als factuurverzending wordt tegengehouden door een
167
+ event.
168
+ - WC: Versienummer van de plugin zelf was niet bijgewerkt, alleen die van de
169
+ library.
170
+ - WC: Updated the documentation of filters and actions in filters.txt
171
+ - ALL: Undefined index: countrycode in Siel/Acumulus/Invoice/Creator.php line 294
172
+ - ALL: API hernoemd naar Library (in versieinfo).
173
+ - ALL [#39961]: Er bestaan producten die niet onderhevig aan BTW zijn.
174
+
175
+ 4.1.0 (2015-10-31)
176
+ ------------------
177
+ - MA,PS,WC [#33383]: Het is nu mogelijk om meerdere order statussen te kiezen
178
+ waarbij de order naar Acumulus verstuurd moet worden.
179
+ - WC [#33222]: Om de betaalstatus van een order te bepalen, wordt niet meer
180
+ gekeken naar paid_date, alleen nog maar naar needs_payments().
181
+ - WC [https://forum.acumulus.nl/index.php?topic=4984.0]: In WC is het mogelijk
182
+ om producten te verwijderen.
183
+
184
+
185
+ 4.0.3 (2015-10-11)
186
+ ------------------
187
+ - WC [https://forum.acumulus.nl/index.php?topic=4963 ]: Regression: WC denkt
188
+ altijd dat factuur al verzonden is.
189
+
190
+
191
+ 4.0.2 (2015-10-07)
192
+ ------------------
193
+ - PS,WC: Support upgrading from 3.x.
194
+ - WC: Lazy load config (LogLevel, debug).
195
+
196
+
197
+ 4.0.1 (2015-10-04)
198
+ ------------------
199
+ - WC [https://forum.acumulus.nl/index.php?topic=4942.0]: PHP Fatal error: Class
200
+ 'Requirements' not found in AcumulusSetup.php line 81.
201
+
202
+
203
+ 4.0.0 (2015-10-02)
204
+ ------------------
205
+ De library 4.x is volledig vernieuwd. Er is meer gemeenschappelijke code en er
206
+ is meer documentatie. Hierdoor kan de library ook losstaand gebruikt worden.
207
+
208
+ Alle kennis en ervaring die met de library 3.x is opgedaan is verwerkt in deze
209
+ versie. Uiteraard zijn alle problemen die in 3.x zijn gevonden en ook van
210
+ toepassing waren op 4.x ook hier opgelost. Vanwege de grote verschillen is de
211
+ 3.x changelog verwijderd en beginnen we hier met een schone lijst.
212
+
213
+ - ALL: Europese BTW tarieven.
214
+ - MA,PS,WC: Ondersteuning voor credit memo's.
215
+ - ALL: Batch verzendformulier.
216
+ - VM: Ondersteuning voor VirtueMart 3. Doch nog geen ondersteuning voor credit
217
+ memos, automatische verzending en de betaalde EU VAT checker extensie.
218
+ - WC: Ondersteuning voor de EU VAT extensie.
219
+ - WC: Ondersteuning voor de plugins woocommerce-sequential-order-numbers(-pro)
220
+ en wc-sequential-order-numbers.
221
+
222
+
223
+
224
+
225
+ Ondersteunde versies van de verschillende pakketten
226
+ ===================================================
227
+
228
+ HikaShop
229
+ --------
230
+ * 2.6.1 (Joomla 3.4.8)
231
+ * 2.6.0 (Joomla 3.4.8)
232
+
233
+
234
+ Magento
235
+ -------
236
+ Geteste Magento versie(s):
237
+ * 1.9.2.3
238
+ * 1.9.2.1
239
+
240
+
241
+ OpenCart 2.x
242
+ ------------
243
+ * 2.1.0.1
244
+
245
+
246
+ OpenCart 1.x
247
+ ------------
248
+ Nog niet beschikbaar.
249
+
250
+
251
+ PrestaShop
252
+ ----------
253
+ Geteste PrestaShop versie(s):
254
+ * 1.6.1.3
255
+ * 1.6.1.2 bevat een fout die deze koppeling raakt
256
+ * 1.6.1.1
257
+ * 1.6.1.0 bevat fouten die deze koppeling raken.
258
+ * 1.5.x (werkt volgens gebruikers vanaf 4.2.0-alpha5)
259
+
260
+
261
+ VirtueMart
262
+ ----------
263
+ Geteste VirtueMart versie(s):
264
+ * 3.0.10 (Joomla 3.4.4)
265
+ * 3.0.8 (Joomla 3.4.1)
266
+
267
+
268
+ WooCommerce
269
+ -----------
270
+ Geteste WooCommerce versie(s):
271
+ * 2.5.2 (WordPress: 4.4.2)
272
+ * 2.4.12 (WordPress: 4.4.2)
273
+ * 2.4.7 (WordPress: 4.3.1)
274
+ * 2.3.13 (WordPress: 4.2.3)
275
+ * < 2.3 gaat niet correct werken met "apply after tax" coupons.
app/code/community/Siel/Acumulus/controllers/Adminhtml/AcumulusController.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class Siel_Acumulus_Adminhtml_AcumulusController extends Mage_Adminhtml_Controller_Action {
4
+
5
+ /** @var Siel_Acumulus_Helper_Data */
6
+ protected $helper;
7
+
8
+ /**
9
+ * Magento "internal constructor" not receiving any parameters.
10
+ */
11
+ protected function _construct() {
12
+ parent::_construct();
13
+ $this->helper = Mage::helper('acumulus');
14
+ }
15
+
16
+ /**
17
+ * Helper method to translate strings.
18
+ *
19
+ * @param string $key
20
+ * The key to get a translation for.
21
+ *
22
+ * @return string
23
+ * The translation for the given key or the key itself if no translation
24
+ * could be found.
25
+ */
26
+ protected function t($key) {
27
+ return $this->helper->t($key);
28
+ }
29
+
30
+ protected function _isAllowed() {
31
+ /** @var Mage_Admin_Model_Session $session */
32
+ $session = Mage::getSingleton('admin/session');
33
+ return $session->isAllowed('acumulus/acumulus_'. $this->getRequest()->getRequestedActionName() . '_form');
34
+ }
35
+
36
+ protected function formAction($formType) {
37
+ $activeMenu = "acumulus/acumulus_{$formType}_form";
38
+ $block = "siel_acumulus_block_adminhtml_form";
39
+ $titleKey = "{$formType}_form_title";
40
+
41
+ /** @var Mage_Adminhtml_Model_Session $session */
42
+ $session = Mage::getSingleton('adminhtml/session');
43
+ try {
44
+ // Create the form first: this will load the translations.
45
+ $form = $this->helper->getAcumulusConfig()->getForm($formType);
46
+
47
+ $this->_title($this->__('System'))->_title($this->t($titleKey));
48
+
49
+ $form->process();
50
+ foreach($form->getSuccessMessages() as $message) {
51
+ $session->addSuccess($message);
52
+ }
53
+ foreach($form->getErrorMessages() as $message) {
54
+ $session->addError($message);
55
+ }
56
+ } catch (Exception $e) {
57
+ $session->addException($e, $e->getMessage());
58
+ }
59
+
60
+ $this->loadLayout();
61
+ $this->_setActiveMenu($activeMenu);
62
+ $this->_addContent($this->getLayout()->createBlock($block, '', array('formType' => $formType)));
63
+ $this->renderLayout();
64
+ }
65
+
66
+ public function configAction() {
67
+ $this->formAction('config');
68
+ }
69
+
70
+ public function batchAction() {
71
+ $this->formAction('batch');
72
+ }
73
+
74
+ }
75
+
app/code/community/Siel/Acumulus/etc/adminhtml.xml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <menu>
4
+ <acumulus translate="title" module="acumulus">
5
+ <title>Acumulus</title>
6
+ <sort_order>85</sort_order>
7
+ <children>
8
+ <acumulus_config_form translate="title" module="acumulus">
9
+ <title>Acumulus instellingen</title>
10
+ <action>adminhtml/acumulus/config</action>
11
+ <sort_order>5</sort_order>
12
+ </acumulus_config_form>
13
+ <acumulus_batch_form translate="title" module="acumulus">
14
+ <title>Verzend facturen naar Acumulus</title>
15
+ <action>adminhtml/acumulus/batch</action>
16
+ <sort_order>10</sort_order>
17
+ </acumulus_batch_form>
18
+ </children>
19
+ </acumulus>
20
+ </menu>
21
+
22
+ <acl>
23
+ <resources>
24
+ <admin>
25
+ <children>
26
+ <acumulus>
27
+ <children>
28
+ <acumulus_config_form>
29
+ <title>Acumulus instellingen</title>
30
+ </acumulus_config_form>
31
+ <acumulus_batch_form>
32
+ <title>Verzend facturen naar Acumulus</title>
33
+ </acumulus_batch_form>
34
+ </children>
35
+ </acumulus>
36
+ </children>
37
+ </admin>
38
+ </resources>
39
+ </acl>
40
+
41
+ </config>
app/code/community/Siel/Acumulus/etc/config.xml ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Siel_Acumulus>
5
+ <version>4.3.0</version>
6
+ </Siel_Acumulus>
7
+ </modules>
8
+
9
+ <stores>
10
+ <admin>
11
+ <dev>
12
+ <debug>
13
+ <template_hints>0</template_hints>
14
+ <template_hints_blocks>0</template_hints_blocks>
15
+ </debug>
16
+ </dev>
17
+ </admin>
18
+ </stores>
19
+
20
+ <global>
21
+ <models>
22
+ <acumulus>
23
+ <class>Siel_Acumulus_Model</class>
24
+ <resourceModel>acumulus_resource</resourceModel>
25
+ </acumulus>
26
+ <acumulus_resource>
27
+ <class>Siel_Acumulus_Model_Resource</class>
28
+ <entities>
29
+ <entry>
30
+ <table>acumulus_entries</table>
31
+ </entry>
32
+ </entities>
33
+ </acumulus_resource>
34
+ </models>
35
+
36
+ <resources>
37
+ <acumulus_setup>
38
+ <setup>
39
+ <module>Siel_Acumulus</module>
40
+ <class>Siel_Acumulus_Model_Resource_Setup</class>
41
+ </setup>
42
+ </acumulus_setup>
43
+ </resources>
44
+
45
+ <blocks>
46
+ <acumulus>
47
+ <class>Siel_Acumulus_Block</class>
48
+ </acumulus>
49
+ </blocks>
50
+
51
+ <helpers>
52
+ <acumulus>
53
+ <class>Siel_Acumulus_Helper</class>
54
+ </acumulus>
55
+ </helpers>
56
+
57
+ <events>
58
+ <sales_order_save_after>
59
+ <observers>
60
+ <Siel_Acumulus_order_observer>
61
+ <type>singleton</type>
62
+ <class>acumulus/order_observer</class>
63
+ <method>orderSaveAfter</method>
64
+ </Siel_Acumulus_order_observer>
65
+ </observers>
66
+ </sales_order_save_after>
67
+
68
+ <sales_order_invoice_save_after>
69
+ <observers>
70
+ <Siel_Acumulus_order_observer>
71
+ <type>singleton</type>
72
+ <class>acumulus/order_observer</class>
73
+ <method>invoiceSaveAfter</method>
74
+ </Siel_Acumulus_order_observer>
75
+ </observers>
76
+ </sales_order_invoice_save_after>
77
+
78
+ <sales_order_creditmemo_save_after>
79
+ <observers>
80
+ <Siel_Acumulus_order_observer>
81
+ <type>singleton</type>
82
+ <class>acumulus/order_observer</class>
83
+ <method>creditMemoSaveAfter</method>
84
+ </Siel_Acumulus_order_observer>
85
+ </observers>
86
+ </sales_order_creditmemo_save_after>
87
+ </events>
88
+ </global>
89
+
90
+ <admin>
91
+ <routers>
92
+ <adminhtml>
93
+ <args>
94
+ <modules>
95
+ <acumulus before="Mage_Adminhtml">Siel_Acumulus_Adminhtml</acumulus>
96
+ </modules>
97
+ </args>
98
+ </adminhtml>
99
+ </routers>
100
+ </admin>
101
+
102
+ </config>
app/code/community/Siel/Acumulus/events.txt ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Acumulus module events
2
+ =======================
3
+
4
+ Warning
5
+ -------
6
+ The events are experimental. Depending on feedback from actual usage, details
7
+ may change in future versions!
8
+
9
+
10
+ Introduction
11
+ ------------
12
+ This file documents the events that the Acumulus module defines. Currently, 3
13
+ events are defined:
14
+ - acumulus_invoice_created
15
+ - acumulus_invoice_completed
16
+ - acumulus_invoice_sent
17
+
18
+ The event acumulus_invoice_created is triggered when the invoice has been
19
+ created with raw data from the webshop order or credit memo, but has yet to be
20
+ completed and refined. Use this event when you want to do some completion
21
+ yourself. This should be rare and most of the times the
22
+ acumulus_invoice_completed event should be preferred.
23
+
24
+ The event acumulus_invoice_completed is triggered when the invoice has been
25
+ completed. Some defaults have been added; VAT rates have been corrected for
26
+ rounding errors; and the 'vat type' has been determined.
27
+
28
+ Typical usages for the latter event allow to implement business logic specific
29
+ to your business, think of:
30
+ - Template, account number, or cost center selection based on order specifics.
31
+ - Adding descriptive info to the invoice or invoice lines based on custom order
32
+ meta data.
33
+ - Correcting payment info based on payment modules not supported by this module.
34
+
35
+ The event acumulus_invoice_sent is triggered when the invoice has been sent to
36
+ Acumulus. This allows to perform some actions depending on the result. Note that
37
+ this event is called independent of the result, so on success as well as
38
+ failure.
39
+
40
+ Typical usages for this event allow to use the invoice number assigned by
41
+ Acumulus in your webshop or to link your order to Acumulus info, although
42
+ this is also already done by this module itself.
43
+
44
+
45
+ acumulus_invoice_created
46
+ ------------------------
47
+
48
+ Usage
49
+ -----
50
+ When the "acumulus_invoice_created" event is called, it is passed 2 arguments:
51
+ - invoice:
52
+ [Note: To allow an observer to change the invoice it is packed in a
53
+ transportObject, see the example below to see how to get and change the
54
+ invoice.]
55
+ A keyed array containing the invoice that will be sent to Acumulus. The array
56
+ structure is similar to the XML structure as defined on
57
+ https://apidoc.sielsystems.nl/content/invoice-add, though at this stage, only
58
+ the customer key will be present at the highest level. The line tag is
59
+ represented as a numeric array of lines, even if if there is only 1 line.
60
+
61
+ Some keys will be missing, if they are still missing after this event has
62
+ been applied, they will be filled in (not overwritten) by the Completor before
63
+ the actual sending takes place:
64
+ - ['customer']['locationcode']
65
+ - ['customer']['overwriteifexists']
66
+ - ['customer']['type']
67
+ - ['customer']['invoice']['concept']
68
+ - ['customer']['invoice']['accountnumber']
69
+ - ['customer']['invoice']['costcenter']
70
+ - ['customer']['invoice']['template]
71
+ - ['customer']['invoice']['vattype]
72
+
73
+ The following keys may be filled with a null value, if the 'unitprice' of that
74
+ 'line' = 0. These null values will be replaced by the module by the most
75
+ occurring vat rate before the actual sending takes place:
76
+ - ['customer']['invoice']['line']['taxrate']
77
+ - source: The Acumulus \Siel\Acumulus\Invoice\Source object that is the base of
78
+ the invoice to be sent.
79
+
80
+
81
+ acumulus_invoice_completed
82
+ --------------------------
83
+
84
+ Usage
85
+ -----
86
+ When the "acumulus_invoice_completed" event is called, it is passed 2 arguments:
87
+ - invoice:
88
+ [Note: To allow an observer to change the invoice it is packed in a
89
+ transportObject, see the example below to see how to get and change the
90
+ invoice.]
91
+ A keyed array containing the invoice that will be sent to Acumulus. The array
92
+ structure is similar to the XML structure as defined on
93
+ https://apidoc.sielsystems.nl/content/invoice-add, though at this stage, only
94
+ the customer key will be present at the highest level. The line tag is
95
+ represented as a numeric array of lines, even if if there is only 1 line.
96
+ - source: The Acumulus \Siel\Acumulus\Invoice\Source object that is the base of
97
+ the invoice to be sent.
98
+
99
+
100
+ Example
101
+ -------
102
+ config.xml:
103
+ <?xml version="1.0"?>
104
+ <config>
105
+ <modules>
106
+ <My_Module>
107
+ <version>0.1-beta1</version>
108
+ </My_Module>
109
+ </modules>
110
+ ...
111
+ <global>
112
+ ...
113
+ <events>
114
+ <acumulus_invoice_completed>
115
+ <observers>
116
+ <My_Module_acumulus_observer>
117
+ <type>singleton</type>
118
+ <class>module/acumulus_observer</class>
119
+ <method>acumulusInvoiceCompleted</method>
120
+ </My_Module_acumulus_observer>
121
+ </observers>
122
+ </acumulus_invoice_completed>
123
+ </events>
124
+ ...
125
+ </global>
126
+ ...
127
+ </config>
128
+
129
+ app\code\local\My\Module\Model\Acumulus\Observer.php:
130
+ use Siel\Acumulus\Invoice\Source;
131
+
132
+ class My_Module_Model_Acumulus_Observer extends Mage_Core_Model_Abstract {
133
+
134
+ public function acumulusInvoiceCompleted(Varien_Event_Observer $observer) {
135
+ $invoice = $observer->getTransportObject()->getInvoice();
136
+ /** @var \Siel\Acumulus\Invoice\Source $source */
137
+ $source = $observer->getSource();
138
+ if ($source->getType() === Source::Order) {
139
+ // Process order.
140
+ $order = $source->getSource();
141
+ $invoice['test'] = 'order test';
142
+ }
143
+ else { // $source->getType() === Source::CreditNote
144
+ // Process credit memo.
145
+ $creditmemo = $source->getSource();
146
+ $invoice['test'] = 'creditmemo test';
147
+ }
148
+
149
+ // Make sure that the changes on the invoice are returned.
150
+ $observer->getTransportObject()->setInvoice($invoice);
151
+ }
152
+ }
153
+
154
+
155
+
156
+ acumulus_invoice_sent
157
+ ---------------------
158
+
159
+ Usage
160
+ -----
161
+ When the "acumulus_invoice_sent" event is called, it is passed 2 arguments:
162
+ - invoice:
163
+ A keyed array containing the invoice that will be sent to Acumulus. The array
164
+ structure is similar to the XML structure as defined on
165
+ https://apidoc.sielsystems.nl/content/invoice-add, though at this stage, only
166
+ the customer key will be present at the highest level. The line tag is
167
+ represented as a numeric array of lines, even if if there is only 1 line.
168
+ - source: The Acumulus \Siel\Acumulus\Invoice\Source object that is the base of
169
+ the invoice to be sent.
170
+ - result: An array that contains the information as sent back by Acumulus, see
171
+ https://apidoc.sielsystems.nl/content/invoice-add.
172
+
173
+
174
+ Example
175
+ -------
176
+ config.xml:
177
+ <?xml version="1.0"?>
178
+ <config>
179
+ <modules>
180
+ <My_Module>
181
+ <version>0.1-beta1</version>
182
+ </My_Module>
183
+ </modules>
184
+ ...
185
+ <global>
186
+ ...
187
+ <events>
188
+ <acumulus_invoice_sent>
189
+ <observers>
190
+ <My_Module_acumulus_observer>
191
+ <type>singleton</type>
192
+ <class>module/acumulus_observer</class>
193
+ <method>acumulusInvoiceSent</method>
194
+ </My_Module_acumulus_observer>
195
+ </observers>
196
+ </acumulus_invoice_sent>
197
+ </events>
198
+ ...
199
+ </global>
200
+ ...
201
+ </config>
202
+
203
+ app\code\local\My\Module\Model\Acumulus\Observer.php:
204
+ use Siel\Acumulus\Invoice\Source;
205
+
206
+ class My_Module_Model_Acumulus_Observer extends Mage_Core_Model_Abstract {
207
+
208
+ public function acumulusInvoiceSent(Varien_Event_Observer $observer) {
209
+ /** @var array $invoice */
210
+ $invoice = $observer->getInvoice();
211
+ /** @var \Siel\Acumulus\Invoice\Source $source */
212
+ $source = $observer->getSource();
213
+ /** @var array $result */
214
+ $result = $observer->getResult();
215
+ if (!empty($result['invoice']['invoicenumber']) {
216
+ // Success.
217
+ $acumulusInvoiceNumber = $result['invoice']['invoicenumber'];
218
+ $acumulusEntryId = $result['invoice']['entryid'];
219
+ $acumulusToken = $result['invoice']['token'];
220
+ }
221
+ else {
222
+ // Failure.
223
+ }
224
+ }
225
+ }
226
+
227
+ External Resources
228
+ ------------------
229
+ - https://apidoc.sielsystems.nl/content/invoice-add.
230
+ - https://apidoc.sielsystems.nl/content/warning-error-and-status-response-section-most-api-calls
app/code/community/Siel/Acumulus/leesmij.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Deze Acumulus webshopkoppeling is ontwikkeld door Buro Rader
2
+ (http://www.burorader.com/)
3
+
4
+ De koppeling is uitvoerig getest tegen een standaard installatie
5
+ van een recente, stabiele versie van de webshop software.
6
+
7
+ Indien u over een installatie beschikt waarin aangepaste of
8
+ conflicterende plugins opgenomen zijn dan zou de koppeling
9
+ niet naar verwachting kunnen werken.
10
+
11
+ Mocht u in deze situatie, of in een standaardomgeving problemen
12
+ met de installatie of werking ondervinden dan kunt u een
13
+ hulpvraag stellen op het Acumulus forum of voor verdere support
14
+ contact opnemen met Buro Rader (support@burorader.com).
15
+
16
+ Indien u van de ondersteuning van Buro Rader gebruik wilt maken
17
+ dan verdient het de aanbeveling om onderstaande gegevens paraat
18
+ te hebben:
19
+ - De huidige versie van uw webwinkel.
20
+ - Eventuele gemaakte aanpassingen.
21
+ - De versie van de webshopkoppeling die u probeert te installeren.
22
+ - Contactgegevens van u en/of uw webbouwer.
23
+
24
+ En pas als daar uitdrukkelijk om gevraagd wordt:
25
+ - Uw FTP-gegevens voor beheer van de webshop.
26
+ - Uw controlpanel gegevens voor beheer van de webshop.
27
+ - Een test-account waarmee testorders gemaakt kunnen worden.
28
+
29
+ Deze software wordt u ter beschikking gesteld onder de GNU GENERAL
30
+ PUBLIC LICENSE versie 3. Voor meer info zie de licentiebestanden
31
+ die meegeleverd worden in de "hoofdfolder" van de koppeling. Dat
32
+ is voor:
33
+ - Magento: app/code/community/Siel/Acumulus
34
+ - OpenCart: system/library/Siel/Acumulus/OpenCart
35
+ - PrestaShop: modules/acumulus
36
+ - Joomla: administrator/components/com_acumulus
37
+ - WooCommerce: wp-content/plugins/acumulus
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Countries.php ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Class Countries defines country related features.
6
+ */
7
+ class Countries
8
+ {
9
+ /**
10
+ * Returns whether the country is the Netherlands.
11
+ *
12
+ * For now only the alpha-2 codes are allowed. Other notations might be added
13
+ * as soon we support a web shop with a different way of storing countries.
14
+ *
15
+ * @param string $countryCode
16
+ * Case insensitive ISO 3166-1 alpha-2 country code.
17
+ *
18
+ * @return bool
19
+ */
20
+ public function isNl($countryCode)
21
+ {
22
+ return strtoupper($countryCode) === 'NL';
23
+ }
24
+
25
+ /**
26
+ * Returns whether the country is a EU country outside the Netherlands.
27
+ *
28
+ * For now only the alpha-2 codes are allowed. Other notations might be added
29
+ * as soon we support a web shop with a different way of storing countries.
30
+ *
31
+ * @param string $countryCode
32
+ * Case insensitive ISO 3166-1 alpha-2 country code.
33
+ *
34
+ * @return bool
35
+ */
36
+ public function isEu($countryCode)
37
+ {
38
+ // Sources:
39
+ // - http://publications.europa.eu/code/pdf/370000en.htm
40
+ // - http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
41
+ // EFTA countries are not part of this list because regarding invoicing they
42
+ // are considered to be outside of the EU.
43
+ $euCountryCodes = array(
44
+ 'BE',
45
+ 'BG',
46
+ 'CZ',
47
+ 'DK',
48
+ 'DE',
49
+ 'EE',
50
+ 'IE',
51
+ 'ES',
52
+ 'FR',
53
+ 'GB', // Great Britain/United Kingdom according to ISO.
54
+ 'GR', // Greece according to the ISO
55
+ 'HR',
56
+ 'IT',
57
+ 'CY',
58
+ 'LV',
59
+ 'LT',
60
+ 'LU',
61
+ 'HU',
62
+ 'MT',
63
+ //'NL', // In EU, but here we want "in EU but outside the Netherlands".
64
+ 'AT',
65
+ 'PL',
66
+ 'PT',
67
+ 'RO',
68
+ 'SI',
69
+ 'SK',
70
+ 'FI',
71
+ 'SE',
72
+ );
73
+ return in_array(strtoupper($countryCode), $euCountryCodes);
74
+ }
75
+
76
+ /**
77
+ * Converts EU country codes to their ISO equivalent:
78
+ *
79
+ * The EU has 2 country codes that differ form ISO:
80
+ * - UK instead of GB
81
+ * - EL instead of GR.
82
+ *
83
+ * @param string $countryCode
84
+ * An EU orISO country code.
85
+ *
86
+ * @return string
87
+ * The ISO country code.
88
+ */
89
+ public function convertEuCountryCode($countryCode)
90
+ {
91
+ if ($countryCode === 'EL') {
92
+ $countryCode = 'GR';
93
+ }
94
+ if ($countryCode === 'UK') {
95
+ $countryCode = 'GB';
96
+ }
97
+ return $countryCode;
98
+ }
99
+
100
+ /**
101
+ * Returns the dutch name for the given country code.
102
+ *
103
+ * @param $countryCode
104
+ * ISO country code (2 characters).
105
+ *
106
+ * @return string
107
+ * The (dutch) name of the country or the empty string if the code could not
108
+ * be looked up.
109
+ */
110
+ public function getCountryName($countryCode)
111
+ {
112
+ $countryNames = array(
113
+ 'AF' => 'Afghanistan',
114
+ 'AX' => 'Åland',
115
+ 'AL' => 'Albanië',
116
+ 'DZ' => 'Algerije',
117
+ 'VI' => 'Amerikaanse Maagdeneilanden',
118
+ 'AS' => 'Amerikaans-Samoa',
119
+ 'AD' => 'Andorra',
120
+ 'AO' => 'Angola',
121
+ 'AI' => 'Anguilla',
122
+ 'AQ' => 'Antarctica',
123
+ 'AG' => 'Antigua en Barbuda',
124
+ 'AR' => 'Argentinië',
125
+ 'AM' => 'Armenië',
126
+ 'AW' => 'Aruba',
127
+ 'AU' => 'Australië',
128
+ 'AZ' => 'Azerbeidzjan',
129
+ 'BS' => 'Bahama\'s',
130
+ 'BH' => 'Bahrein',
131
+ 'BD' => 'Bangladesh',
132
+ 'BB' => 'Barbados',
133
+ 'BE' => 'België',
134
+ 'BZ' => 'Belize',
135
+ 'BJ' => 'Benin',
136
+ 'BM' => 'Bermuda',
137
+ 'BT' => 'Bhutan',
138
+ 'BO' => 'Bolivia',
139
+ 'BQ' => 'Bonaire, Sint Eustatius en Saba',
140
+ 'BA' => 'Bosnië en Herzegovina',
141
+ 'BW' => 'Botswana',
142
+ 'BV' => 'Bouveteiland',
143
+ 'BR' => 'Brazilië',
144
+ 'VG' => 'Britse Maagdeneilanden',
145
+ 'IO' => 'Brits Indische Oceaanterritorium',
146
+ 'BN' => 'Brunei',
147
+ 'BG' => 'Bulgarije',
148
+ 'BF' => 'Burkina Faso',
149
+ 'BI' => 'Burundi',
150
+ 'KH' => 'Cambodja',
151
+ 'CA' => 'Canada',
152
+ 'CF' => 'Centraal-Afrikaanse Republiek',
153
+ 'CL' => 'Chili',
154
+ 'CN' => 'China',
155
+ 'CX' => 'Christmaseiland',
156
+ 'CC' => 'Cocoseilanden',
157
+ 'CO' => 'Colombia',
158
+ 'KM' => 'Comoren',
159
+ 'CG' => 'Congo-Brazzaville',
160
+ 'CD' => 'Congo-Kinshasa',
161
+ 'CK' => 'Cookeilanden',
162
+ 'CR' => 'Costa Rica',
163
+ 'CU' => 'Cuba',
164
+ 'CW' => 'Curaçao',
165
+ 'CY' => 'Cyprus',
166
+ 'DK' => 'Denemarken',
167
+ 'DJ' => 'Djibouti',
168
+ 'DM' => 'Dominica',
169
+ 'DO' => 'Dominicaanse Republiek',
170
+ 'DE' => 'Duitsland',
171
+ 'EC' => 'Ecuador',
172
+ 'EG' => 'Egypte',
173
+ 'SV' => 'El Salvador',
174
+ 'GQ' => 'Equatoriaal-Guinea',
175
+ 'ER' => 'Eritrea',
176
+ 'EE' => 'Estland',
177
+ 'ET' => 'Ethiopië',
178
+ 'FO' => 'Faeröer',
179
+ 'FK' => 'Falklandeilanden',
180
+ 'FJ' => 'Fiji',
181
+ 'PH' => 'Filipijnen',
182
+ 'FI' => 'Finland',
183
+ 'FR' => 'Frankrijk',
184
+ 'TF' => 'Franse Zuidelijke en Antarctische Gebieden',
185
+ 'GF' => 'Frans-Guyana',
186
+ 'PF' => 'Frans-Polynesië',
187
+ 'GA' => 'Gabon',
188
+ 'GM' => 'Gambia',
189
+ 'GE' => 'Georgië',
190
+ 'GH' => 'Ghana',
191
+ 'GI' => 'Gibraltar',
192
+ 'GD' => 'Grenada',
193
+ 'GR' => 'Griekenland',
194
+ 'GL' => 'Groenland',
195
+ 'GP' => 'Guadeloupe',
196
+ 'GU' => 'Guam',
197
+ 'GT' => 'Guatemala',
198
+ 'GG' => 'Guernsey',
199
+ 'GN' => 'Guinee',
200
+ 'GW' => 'Guinee-Bissau',
201
+ 'GY' => 'Guyana',
202
+ 'HT' => 'Haïti',
203
+ 'HM' => 'Heard en McDonaldeilanden',
204
+ 'HN' => 'Honduras',
205
+ 'HU' => 'Hongarije',
206
+ 'HK' => 'Hongkong',
207
+ 'IE' => 'Ierland',
208
+ 'IS' => 'IJsland',
209
+ 'IN' => 'India',
210
+ 'ID' => 'Indonesië',
211
+ 'IQ' => 'Irak',
212
+ 'IR' => 'Iran',
213
+ 'IL' => 'Israël',
214
+ 'IT' => 'Italië',
215
+ 'CI' => 'Ivoorkust',
216
+ 'JM' => 'Jamaica',
217
+ 'JP' => 'Japan',
218
+ 'YE' => 'Jemen',
219
+ 'JE' => 'Jersey',
220
+ 'JO' => 'Jordanië',
221
+ 'KY' => 'Kaaimaneilanden',
222
+ 'CV' => 'Kaapverdië',
223
+ 'CM' => 'Kameroen',
224
+ 'KZ' => 'Kazachstan',
225
+ 'KE' => 'Kenia',
226
+ 'KG' => 'Kirgizië',
227
+ 'KI' => 'Kiribati',
228
+ 'UM' => 'Kleine Pacifische eilanden van de Verenigde Staten',
229
+ 'KW' => 'Koeweit',
230
+ 'HR' => 'Kroatië',
231
+ 'LA' => 'Laos',
232
+ 'LS' => 'Lesotho',
233
+ 'LV' => 'Letland',
234
+ 'LB' => 'Libanon',
235
+ 'LR' => 'Liberia',
236
+ 'LY' => 'Libië',
237
+ 'LI' => 'Liechtenstein',
238
+ 'LT' => 'Litouwen',
239
+ 'LU' => 'Luxemburg',
240
+ 'MO' => 'Macau',
241
+ 'MK' => 'Macedonië',
242
+ 'MG' => 'Madagaskar',
243
+ 'MW' => 'Malawi',
244
+ 'MV' => 'Maldiven',
245
+ 'MY' => 'Maleisië',
246
+ 'ML' => 'Mali',
247
+ 'MT' => 'Malta',
248
+ 'IM' => 'Man',
249
+ 'MA' => 'Marokko',
250
+ 'MH' => 'Marshalleilanden',
251
+ 'MQ' => 'Martinique',
252
+ 'MR' => 'Mauritanië',
253
+ 'MU' => 'Mauritius',
254
+ 'YT' => 'Mayotte',
255
+ 'MX' => 'Mexico',
256
+ 'FM' => 'Micronesia',
257
+ 'MD' => 'Moldavië',
258
+ 'MC' => 'Monaco',
259
+ 'MN' => 'Mongolië',
260
+ 'ME' => 'Montenegro',
261
+ 'MS' => 'Montserrat',
262
+ 'MZ' => 'Mozambique',
263
+ 'MM' => 'Myanmar',
264
+ 'NA' => 'Namibië',
265
+ 'NR' => 'Nauru',
266
+ 'NL' => 'Nederland',
267
+ 'NP' => 'Nepal',
268
+ 'NI' => 'Nicaragua',
269
+ 'NC' => 'Nieuw-Caledonië',
270
+ 'NZ' => 'Nieuw-Zeeland',
271
+ 'NE' => 'Niger',
272
+ 'NG' => 'Nigeria',
273
+ 'NU' => 'Niue',
274
+ 'MP' => 'Noordelijke Marianen',
275
+ 'KP' => 'Noord-Korea',
276
+ "NO" => 'Noorwegen',
277
+ 'NF' => 'Norfolk',
278
+ 'UG' => 'Oeganda',
279
+ 'UA' => 'Oekraïne',
280
+ 'UZ' => 'Oezbekistan',
281
+ 'OM' => 'Oman',
282
+ 'AT' => 'Oostenrijk',
283
+ 'TL' => 'Oost-Timor',
284
+ 'PK' => 'Pakistan',
285
+ 'PW' => 'Palau',
286
+ 'PS' => 'Palestina',
287
+ 'PA' => 'Panama',
288
+ 'PG' => 'Papoea-Nieuw-Guinea',
289
+ 'PY' => 'Paraguay',
290
+ 'PE' => 'Peru',
291
+ 'PN' => 'Pitcairneilanden',
292
+ 'PL' => 'Polen',
293
+ 'PT' => 'Portugal',
294
+ 'PR' => 'Puerto Rico',
295
+ 'QA' => 'Qatar',
296
+ 'RE' => 'Réunion',
297
+ 'RO' => 'Roemenië',
298
+ 'RU' => 'Rusland',
299
+ 'RW' => 'Rwanda',
300
+ 'BL' => 'Saint-Barthélemy',
301
+ 'KN' => 'Saint Kitts en Nevis',
302
+ 'LC' => 'Saint Lucia',
303
+ 'PM' => 'Saint-Pierre en Miquelon',
304
+ 'VC' => 'Saint Vincent en de Grenadines',
305
+ 'SB' => 'Salomonseilanden',
306
+ 'WS' => 'Samoa',
307
+ 'SM' => 'San Marino',
308
+ 'SA' => 'Saoedi-Arabië',
309
+ 'ST' => 'Sao Tomé en Principe',
310
+ 'SN' => 'Senegal',
311
+ 'RS' => 'Servië',
312
+ 'SC' => 'Seychellen',
313
+ 'SL' => 'Sierra Leone',
314
+ 'SG' => 'Singapore',
315
+ 'SH' => 'Sint-Helena, Ascension en Tristan da Cunha',
316
+ 'MF' => 'Sint-Maarten',
317
+ 'SX' => 'Sint Maarten',
318
+ 'SI' => 'Slovenië',
319
+ 'SK' => 'Slowakije',
320
+ 'SD' => 'Soedan',
321
+ 'SO' => 'Somalië',
322
+ 'ES' => 'Spanje',
323
+ 'SJ' => 'Spitsbergen en Jan Mayen',
324
+ 'LK' => 'Sri Lanka',
325
+ 'SR' => 'Suriname',
326
+ 'SZ' => 'Swaziland',
327
+ 'SY' => 'Syrië',
328
+ 'TJ' => 'Tadzjikistan',
329
+ 'TW' => 'Taiwan',
330
+ 'TZ' => 'Tanzania',
331
+ 'TH' => 'Thailand',
332
+ 'TG' => 'Togo',
333
+ 'TK' => 'Tokelau',
334
+ 'TO' => 'Tonga',
335
+ 'TT' => 'Trinidad en Tobago',
336
+ 'TD' => 'Tsjaad',
337
+ 'CZ' => 'Tsjechië',
338
+ 'TN' => 'Tunesië',
339
+ 'TR' => 'Turkije',
340
+ 'TM' => 'Turkmenistan',
341
+ 'TC' => 'Turks- en Caicoseilanden',
342
+ 'TV' => 'Tuvalu',
343
+ 'UY' => 'Uruguay',
344
+ 'VU' => 'Vanuatu',
345
+ 'VA' => 'Vaticaanstad',
346
+ 'VE' => 'Venezuela',
347
+ 'AE' => 'Verenigde Arabische Emiraten',
348
+ 'US' => 'Verenigde Staten',
349
+ 'GB' => 'Verenigd Koninkrijk',
350
+ 'VN' => 'Vietnam',
351
+ 'WF' => 'Wallis en Futuna',
352
+ 'EH' => 'Westelijke Sahara',
353
+ 'BY' => 'Wit-Rusland',
354
+ 'ZM' => 'Zambia',
355
+ 'ZW' => 'Zimbabwe',
356
+ 'ZA' => 'Zuid-Afrika',
357
+ 'GS' => 'Zuid-Georgia en de Zuidelijke Sandwicheilanden',
358
+ 'KR' => 'Zuid-Korea',
359
+ 'SS' => 'Zuid-Soedan',
360
+ 'SE' => 'Zweden',
361
+ 'CH' => 'Zwitserland',
362
+ );
363
+ return isset($countryNames[$countryCode]) ? $countryNames[$countryCode] : '';
364
+ }
365
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Form.php ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Provides basic form handling.
6
+ *
7
+ * Most webshop and CMS software provide their own sort of form API. To be able
8
+ * to generalize or abstract our form handling, this class defines our own
9
+ * minimal form API. This allows us to define our form handing in a cross
10
+ * webshop compatible way. The webshop/CMs specific part should then define a
11
+ * wrapper/mapper to the webshop specific way of form handling.
12
+ *
13
+ * This base Form class defines a way to:
14
+ * - Define the form elements.
15
+ * - Render the form, including their values.
16
+ * - Process a form submission:
17
+ * - Recognise form submission.
18
+ * - Perform form (submission) validation.
19
+ * - Execute a task on valid form submission.
20
+ * - Show success and/or error messages.
21
+ *
22
+ * Usage:
23
+ * This code is typically performed by a controller that processes the request:
24
+ * <code>
25
+ * $form = new ChildForm();
26
+ * $form->process();
27
+ * // process any messages ... may also be in the view
28
+ * </code>
29
+ *
30
+ * This code is typically performed by a view and should be used when the CMS or
31
+ * webshop does not really provide a form object:
32
+ * <code>
33
+ * // Displays the form.
34
+ * // Create the form and add the values to the elements.
35
+ * $form = new ChildForm();
36
+ * $formFields = $form->addValues();
37
+ * // Render the html for the form.
38
+ * $formRenderer = new FormRenderer();
39
+ * $formRenderer->render($form)
40
+ * </code>
41
+ *
42
+ * This code is to be used when the CMS or webshop does provide its own form
43
+ * handling and processing.
44
+ * <code>
45
+ * // Create shop specific Form object
46
+ * $shopForm = new ShopForm()
47
+ * // Map the elements and settings of the Acumulus Form to the shop form.
48
+ * $formMapper = new FormMapper();
49
+ * $formMapper->map($shopForm, $form);
50
+ * // Continue with the shop form by setting the form values ...
51
+ * $shopForm->setValues($form->GetValues)
52
+ * // and rendering it.
53
+ * $shopForm->render()
54
+ *
55
+ * </code>
56
+ */
57
+ abstract class Form
58
+ {
59
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
60
+ protected $translator;
61
+
62
+ /** @var array[] */
63
+ protected $fields;
64
+
65
+ /** @var bool */
66
+ protected $formValuesSet;
67
+
68
+ /** @var array The values to be placed on the configuration form. */
69
+ protected $formValues;
70
+
71
+ /** @var string[] The values as filled in on form submission. */
72
+ protected $submittedValues;
73
+
74
+ /** @var string[] Any success messages. */
75
+ protected $successMessages;
76
+
77
+ /** @var string[] Any error messages. */
78
+ protected $errorMessages;
79
+
80
+ /**
81
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
82
+ */
83
+ public function __construct(TranslatorInterface $translator)
84
+ {
85
+ $this->successMessages = array();
86
+ $this->errorMessages = array();
87
+ $this->formValuesSet = false;
88
+ $this->submittedValues = array();
89
+
90
+ $this->translator = $translator;
91
+ $this->fields = array();
92
+ }
93
+
94
+ /**
95
+ * Helper method to translate strings.
96
+ *
97
+ * @param string $key
98
+ * The key to get a translation for.
99
+ *
100
+ * @return string
101
+ * The translation for the given key or the key itself if no translation
102
+ * could be found.
103
+ */
104
+ protected function t($key)
105
+ {
106
+ return $this->translator->get($key);
107
+ }
108
+
109
+ /**
110
+ * To be used by web shop specific form handling to display a success message.
111
+ *
112
+ * @return string[]
113
+ */
114
+ public function getSuccessMessages()
115
+ {
116
+ return $this->successMessages;
117
+ }
118
+
119
+ /**
120
+ * To be used by web shop specific form handling to display validation and
121
+ * connection error messages.
122
+ *
123
+ * An empty result indicates successful validation.
124
+ *
125
+ * @return string[]
126
+ * An array of translated messages. In case of validation errors they are
127
+ * keyed by the name of the invalid form field.
128
+ */
129
+ public function getErrorMessages()
130
+ {
131
+ return $this->errorMessages;
132
+ }
133
+
134
+ /**
135
+ * Indicates whether the current form handling is a form submission.
136
+ *
137
+ * @return bool
138
+ */
139
+ public function isSubmitted()
140
+ {
141
+ return $_SERVER['REQUEST_METHOD'] === 'POST';
142
+ }
143
+
144
+ /**
145
+ * Returns whether the submitted form values are valid.
146
+ *
147
+ * @return bool
148
+ */
149
+ public function isValid()
150
+ {
151
+ return empty($this->errorMessages);
152
+ }
153
+
154
+ /**
155
+ * Sets the form values to use.
156
+ *
157
+ * This is typically the union of the default values, any submitted values,
158
+ * and explicitly set field values.
159
+ */
160
+ protected function setFormValues()
161
+ {
162
+ if (!$this->formValuesSet) {
163
+ $this->formValues = array_fill_keys($this->getKeys(), '');
164
+ $this->formValues = array_merge($this->formValues, $this->getDefaultFormValues());
165
+ if (!empty($this->submittedValues)) {
166
+ $this->formValues = array_merge($this->formValues, $this->submittedValues);
167
+ }
168
+ $this->formValues = array_merge($this->formValues, $this->getFieldValues($this->getFields()));
169
+
170
+ $this->formValuesSet = true;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Returns the values for all the fields on the form definition.
176
+ *
177
+ * This method will not have a use on every web shop, but, e.g. Magento and
178
+ * PrestaShop have a separate "bind" method to bind a set of values to a form
179
+ * at once.
180
+ *
181
+ * @return array
182
+ * An array of values keyed by the form field names.
183
+ */
184
+ public function getFormValues()
185
+ {
186
+ $this->setFormValues();
187
+ return $this->formValues;
188
+ }
189
+
190
+ /**
191
+ * Returns the value for a specific form field.
192
+ *
193
+ * @param string $name
194
+ * The name of the form field.
195
+ *
196
+ * @return string
197
+ * The value for this form field or the empty string if not set.
198
+ */
199
+ protected function getFormValue($name)
200
+ {
201
+ $this->setFormValues();
202
+ return isset($this->formValues[$name]) ? $this->formValues[$name] : '';
203
+ }
204
+
205
+ /**
206
+ * Sets the value for a specific form field.
207
+ *
208
+ * @param string $name
209
+ * The name of the form field.
210
+ * @param mixed $value
211
+ * The value for the form field.
212
+ */
213
+ protected function setFormValue($name, $value)
214
+ {
215
+ $this->formValues[$name] = $value;
216
+ }
217
+
218
+ /**
219
+ * Adds the form values to the field definitions.
220
+ *
221
+ * This method will not have a use on every web shop, but, e.g. VirtueMart and
222
+ * OpenCart have a form helper/renderer to render individual fields including
223
+ * their value attribute instead of binding values to a form and rendering the
224
+ * form.
225
+ */
226
+ public function addValues()
227
+ {
228
+ $this->fields = $this->addValuesToFields($this->getFields());
229
+ }
230
+
231
+ /**
232
+ * Adds the form values to the field definitions.
233
+ *
234
+ * This internal version of addValues() passes the fields as a parameter to
235
+ * allow to recursively process field sets.
236
+ *
237
+ * @param array[] $fields
238
+ *
239
+ * @return array[]
240
+ */
241
+ protected function addValuesToFields(array $fields)
242
+ {
243
+ foreach ($fields as $name => &$field) {
244
+ if ($field['type'] === 'fieldset') {
245
+ $field['fields'] = $this->addValuesToFields($field['fields']);
246
+ } else if ($field['type'] === 'checkbox') {
247
+ // Value is a list of checked options.
248
+ $value = array();
249
+ foreach ($field['options'] as $optionName => $optionLabel) {
250
+ if ($this->getFormValue($optionName)) {
251
+ $value[] = $optionName;
252
+ }
253
+ }
254
+ $field['value'] = $value;
255
+ } else {
256
+ // Explicitly set values (in the 'value' key) take precedence over
257
+ // submitted values, which in turn take precedence over default values
258
+ // (gathered via getDefaultFormValues()).
259
+ if (!isset($field['value'])) {
260
+ $field['value'] = $this->getFormValue($name);
261
+ }
262
+ }
263
+ }
264
+ return $fields;
265
+ }
266
+
267
+ /**
268
+ * Returns a set of default values for the form fields.
269
+ *
270
+ * This default implementation returns an empty array, i.e. all form fields
271
+ * are empty, not selected, or unchecked.
272
+ *
273
+ * @return array
274
+ * An array of default values keyed by the form field names.
275
+ */
276
+ protected function getDefaultFormValues()
277
+ {
278
+ return array();
279
+ }
280
+
281
+ /**
282
+ * Returns the set of values directly assigned to the field definitions.
283
+ *
284
+ * These take precedence over default values
285
+ *
286
+ * @param array[] $fields
287
+ *
288
+ * @return array An array of values keyed by the form field names.
289
+ * An array of values keyed by the form field names.
290
+ */
291
+ protected function getFieldValues($fields)
292
+ {
293
+ $result = array();
294
+ foreach ($fields as $id => $field) {
295
+ if (isset($field['value'])) {
296
+ $result[$id] = $field['value'];
297
+ }
298
+ if ($field['type'] === 'fieldset') {
299
+ $result = array_merge($result, $this->getFieldValues($field['fields']));
300
+ }
301
+ }
302
+ return $result;
303
+ }
304
+
305
+ /**
306
+ * Extracts the submitted values.
307
+ *
308
+ * Override to restrict the $_POST values to expected values and to do any
309
+ * sanitation.
310
+ */
311
+ protected function setSubmittedValues()
312
+ {
313
+ $this->submittedValues = $this->getPostedValues();
314
+ }
315
+
316
+ /**
317
+ * Processes the form.
318
+ *
319
+ * @param bool $executeIfValid
320
+ * Whether this method should execute the intended action after successful
321
+ * validation. Some web shops (WooCommerce) sometimes do their own form
322
+ * handling (setting pages) and we should only do the validation and setting
323
+ * admin notices as necessary.
324
+ *
325
+ * @return bool
326
+ * True if there was no form submission or a successful submission.
327
+ */
328
+ public function process($executeIfValid = true)
329
+ {
330
+ $this->formValues = array();
331
+ $this->submittedValues = array();
332
+
333
+ // Process the form if it was submitted.
334
+ if ($this->isSubmitted() && $this->systemValidate()) {
335
+ $this->setSubmittedValues();
336
+ $this->validate();
337
+ if ($executeIfValid && $this->isValid()) {
338
+ if ($this->execute()) {
339
+ $message = $this->t('message_form_success');
340
+ if ($message !== 'message_form_success') {
341
+ $this->successMessages[] = $message;
342
+ }
343
+ } else {
344
+ $message = $this->t('message_form_error');
345
+ if ($message !== 'message_form_error') {
346
+ $this->errorMessages[] = $message;
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ return $this->isValid();
353
+ }
354
+
355
+ /**
356
+ * Checks system specific form properties.
357
+ *
358
+ * This typically includes protection against CSRF attacks and such.
359
+ * Override this per form with the proper checks from the system.
360
+ */
361
+ protected function systemValidate()
362
+ {
363
+ return true;
364
+ }
365
+
366
+ /**
367
+ * Performs config form validation.
368
+ *
369
+ * Any errors are stored as a user readable message in the $errorMessages
370
+ * property and will be keyed by the field name.
371
+ *
372
+ * This default implementation does no validation at all. Override to add form
373
+ * specific validation.
374
+ */
375
+ protected function validate()
376
+ {
377
+ }
378
+
379
+ /**
380
+ * Executes the form action on valid form submission.
381
+ *
382
+ * Override to implement the actual form handling, like saving values.
383
+ *
384
+ * @return bool
385
+ * Success.
386
+ */
387
+ abstract protected function execute();
388
+
389
+ /**
390
+ * Returns a definition of the form fields.
391
+ *
392
+ * This should NOT include any:
393
+ * - Submit or cancel buttons. These are often added by the web shop software
394
+ * in their specific way.
395
+ * - Tokens, form-id's or other (hidden) fields used by the web shop software
396
+ * to protect against certain attacks or to facilitate internal form
397
+ * processing.
398
+ *
399
+ * This is a recursive, keyed array defining each form field. The key defines
400
+ * the name of the form field, to be used for the name, and possibly id,
401
+ * attribute. The values are a keyed array, that can have the following keys:
402
+ * - type: (required, string) fieldset, text, email, password, date, textarea,
403
+ * select, radio, checkbox, markup.
404
+ * - label: (string) human readable label.
405
+ * - description: (string) human readable help text.
406
+ * - value: (string) the value for the form field.
407
+ * - attributes: (array) keyed array with other - possibly html5 - attributes
408
+ * to be rendered. Possible keys include:
409
+ * - size
410
+ * - class
411
+ * - required: (bool) whether the field is required.
412
+ * - fields: (array) If the type = 'fieldset', this value defines the fields
413
+ * (and possibly sub fieldsets) of the fieldset.
414
+ * - options: (array) If the type = checkbox, select or radio, this value
415
+ * contains the options as a keyed array, the keys being the value to
416
+ * submit if that choice is selected and the value being the label to show.
417
+ *
418
+ * Do NOT override this method, instead override getFieldDefinitions().
419
+ *
420
+ * @return array[]
421
+ * The definition of the form.
422
+ */
423
+ public function getFields()
424
+ {
425
+ if (empty($this->fields)) {
426
+ $this->fields = $this->getFieldDefinitions();
427
+ }
428
+ return $this->fields;
429
+ }
430
+
431
+ /**
432
+ * Internal version of getFields();
433
+ *
434
+ * Internal method, do not call directly.
435
+ * DO override this method not getFields().
436
+ *
437
+ * @return array[]
438
+ * The definition of the form.
439
+ */
440
+ abstract protected function getFieldDefinitions();
441
+
442
+ /**
443
+ * Returns a list of field ids/keys appearing in the form.
444
+ *
445
+ * @return array
446
+ * Array of key names appearing in the form, keyed by these names.
447
+ */
448
+ protected function getKeys()
449
+ {
450
+ return $this->getFieldKeys($this->getFields());
451
+ }
452
+
453
+ /**
454
+ * Returns the keys of the fields in the given array.
455
+ *
456
+ * Internal method, do not call directly.
457
+ *
458
+ * @param array[] $fields
459
+ *
460
+ * @return array
461
+ * Array of key names, keyed by these names.
462
+ */
463
+ protected function getFieldKeys(array $fields)
464
+ {
465
+ $result = array();
466
+ foreach ($fields as $key => $field) {
467
+ $result[$key] = $key;
468
+ if ($field['type'] === 'fieldset') {
469
+ $result += $this->getFieldKeys($field['fields']);
470
+ }
471
+ }
472
+ return $result;
473
+ }
474
+
475
+ /**
476
+ * Helper method to copy a value from one array to another array.
477
+ *
478
+ * @param array $target
479
+ * @param string $key
480
+ * @param array $source
481
+ * @param string $sourceKey
482
+ *
483
+ * @return bool
484
+ * True if the value isset and thus has been copied, false otherwise,
485
+ */
486
+ protected function addIfIsset(array &$target, $key, array $source, $sourceKey = '')
487
+ {
488
+ if (empty($sourceKey)) {
489
+ $sourceKey = $key;
490
+ }
491
+ if (isset($source[$sourceKey])) {
492
+ $target[$key] = $source[$sourceKey];
493
+ return true;
494
+ }
495
+ return false;
496
+ }
497
+
498
+ /**
499
+ * Indicates whether the given key defines a checkbox field.
500
+ *
501
+ * This base implementation returns false. Override this method if your form
502
+ * has checkbox fields.
503
+ *
504
+ * @param string $key
505
+ * The name of the field.
506
+ *
507
+ * @return bool
508
+ * Whether the given key defines a checkbox field.
509
+ */
510
+ protected function isCheckboxKey($key)
511
+ {
512
+ return array_key_exists($key, $this->getCheckboxKeys());
513
+ }
514
+
515
+ /**
516
+ * Returns a list of the checkbox names for this form.
517
+ *
518
+ * This base implementation returns an empty array. Override this method if
519
+ * your form has checkbox fields.
520
+ *
521
+ * @return array
522
+ * An array with as keys the checkbox names of this form and as values the
523
+ * checkbox collection name the checkbox belongs to.
524
+ */
525
+ protected function getCheckboxKeys()
526
+ {
527
+ return array();
528
+ }
529
+
530
+ /**
531
+ * Returns a flat array of the posted values.
532
+ *
533
+ * As especially checkbox handling differs per web shop, often resulting in an
534
+ * array of checkbox values, this method returns a flattened version of the
535
+ * posted values.
536
+ *
537
+ * @return array
538
+ */
539
+ protected function getPostedValues()
540
+ {
541
+ $result = $_POST;
542
+
543
+ // Handle checkboxes.
544
+ foreach ($this->getCheckboxKeys() as $checkboxName => $collectionName) {
545
+ // Check if checkboxes are handled as an array of checkboxes.
546
+ if (isset($result[$collectionName]) && is_array($result[$collectionName])) {
547
+ // Extract the checked values.
548
+ $checkedValues = array_combine(array_values($result[$collectionName]), array_fill(0, count($result[$collectionName]), 1));
549
+ // Replace the array value with the checked values, unset first as the
550
+ // keys for the collection and (1 of the) checkboxes may be the same.
551
+ unset($result[$collectionName]);
552
+ $result += $checkedValues;
553
+ } // Check if checkbox keys are prepended with their collection name.
554
+ else if (isset($result["{$collectionName}_{$checkboxName}"])) {
555
+ $result[$checkboxName] = $result["{$collectionName}_{$checkboxName}"];
556
+ unset($result["{$collectionName}_{$checkboxName}"]);
557
+ }
558
+ }
559
+
560
+ return $result;
561
+ }
562
+
563
+ /**
564
+ * Returns the expected date format using a format as accepted by
565
+ * DateTime::createFromFormat().
566
+ *
567
+ * This default implementation assumes that the shop uses the same format
568
+ * options as PHP.
569
+ *
570
+ * @return string
571
+ */
572
+ public function getDateFormat()
573
+ {
574
+ return $this->getShopDateFormat();
575
+ }
576
+
577
+ /**
578
+ * Returns the expected date format using a format as accepted by the shop's
579
+ * date handling functions.
580
+ *
581
+ * This default implementation assumes that the shop does not provide its own
582
+ * date handling formats and settings.
583
+ *
584
+ * @return string
585
+ */
586
+ public function getShopDateFormat()
587
+ {
588
+ return 'Y-m-d';
589
+ }
590
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/FormRenderer.php ADDED
@@ -0,0 +1,813 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Provides form element rendering functionality. This basic implementation
6
+ * renders the elements as wrapped html input elements. To comply with shop
7
+ * specific styling, it is supposed to be overridden per shop that uses this
8
+ * way of rendering. for now those are: HikaShop/VirtueMart (Joomla), OpenCart,
9
+ * and WooCommerce (WordPress).
10
+ */
11
+ class FormRenderer
12
+ {
13
+ /** @var bool */
14
+ protected $html5 = true;
15
+
16
+ /** @var string */
17
+ protected $elementWrapperTag = 'div';
18
+
19
+ /** @var string */
20
+ protected $elementWrapperClass = 'form-element';
21
+
22
+ /** @var string */
23
+ protected $fieldsetWrapperTag = 'fieldset';
24
+
25
+ /** @var string */
26
+ protected $fieldsetWrapperClass = '';
27
+
28
+ /** @var string */
29
+ protected $legendWrapperTag = 'legend';
30
+
31
+ /** @var string */
32
+ protected $legendWrapperClass = '';
33
+
34
+ /** @var string */
35
+ protected $labelWrapperTag = '';
36
+
37
+ /** @var string */
38
+ protected $labelWrapperClass = '';
39
+
40
+ /** @var string */
41
+ protected $markupWrapperTag = 'div';
42
+
43
+ /** @var string */
44
+ protected $markupWrapperClass = 'message';
45
+
46
+ /** @var string */
47
+ protected $inputWrapperTag = '';
48
+
49
+ /** @var string */
50
+ protected $inputWrapperClass = '';
51
+
52
+ /** @var string */
53
+ protected $radioWrapperTag = 'div';
54
+
55
+ /** @var string */
56
+ protected $radioWrapperClass = 'radio';
57
+
58
+ /** @var string */
59
+ protected $radio1WrapperTag = '';
60
+
61
+ /** @var string */
62
+ protected $radio1WrapperClass = '';
63
+
64
+ /** @var string */
65
+ protected $checkboxWrapperTag = 'div';
66
+
67
+ /** @var string */
68
+ protected $checkboxWrapperClass = 'checkbox';
69
+
70
+ /** @var string */
71
+ protected $checkbox1WrapperTag = '';
72
+
73
+ /** @var string */
74
+ protected $checkbox1WrapperClass = '';
75
+
76
+ /** @var string */
77
+ protected $multiLabelTag = 'span';
78
+
79
+ /** @var string */
80
+ protected $multiLabelClass = 'label';
81
+
82
+ /** @var string */
83
+ protected $descriptionClass = 'description';
84
+
85
+ /** @var string */
86
+ protected $requiredMarkup = '<span class="required">*</span>';
87
+
88
+ /** @var Form */
89
+ protected $form;
90
+
91
+ /**
92
+ * Renders the form.
93
+ *
94
+ * @param \Siel\Acumulus\Helpers\Form $form
95
+ *
96
+ * @return string
97
+ */
98
+ public function render(Form $form)
99
+ {
100
+ $this->form = $form;
101
+ $this->form->addValues();
102
+ return $this->fields($this->form->getFields());
103
+ }
104
+
105
+ /**
106
+ * Renders a set of field definitions.
107
+ *
108
+ * @param array[] $fields
109
+ *
110
+ * @return string
111
+ */
112
+ protected function fields(array $fields)
113
+ {
114
+ $output = '';
115
+ foreach ($fields as $id => $field) {
116
+ if (!isset($field['id'])) {
117
+ $field['id'] = $id;
118
+ }
119
+ if (!isset($field['name'])) {
120
+ $field['name'] = $id;
121
+ }
122
+ $output .= $this->field($field);
123
+ }
124
+ return $output;
125
+ }
126
+
127
+ /**
128
+ * Renders 1 field definition (which may be a fieldset with multiple fields).
129
+ *
130
+ * @param array $field
131
+ *
132
+ * @return string
133
+ */
134
+ protected function field(array $field)
135
+ {
136
+ $output = '';
137
+ if (!isset($field['attributes'])) {
138
+ $field['attributes'] = array();
139
+ }
140
+ switch ($field['type']) {
141
+ case 'fieldset':
142
+ $output .= $this->renderFieldset($field);
143
+ break;
144
+ case 'markup':
145
+ $output .= $this->renderMarkup($field);
146
+ break;
147
+ default:
148
+ $output .= $this->renderField($field);
149
+ break;
150
+ }
151
+ return $output;
152
+ }
153
+
154
+ /**
155
+ * Renders a fieldset.
156
+ *
157
+ * @param array $field
158
+ *
159
+ * @return string
160
+ * The rendered fieldset.
161
+ */
162
+ protected function renderFieldset(array $field)
163
+ {
164
+ $output = '';
165
+ $output .= $this->fieldsetBegin($field);
166
+ $output .= $this->fields($field['fields']);
167
+ $output .= $this->fieldsetEnd($field);
168
+ return $output;
169
+ }
170
+
171
+ /**
172
+ * Outputs the beginning of a fieldset.
173
+ *
174
+ * @param array $field
175
+ *
176
+ * @return string
177
+ */
178
+ protected function fieldsetBegin(array $field)
179
+ {
180
+ $output = '';
181
+ $output .= $this->getWrapper('fieldset', $field['attributes']);
182
+ $output .= $this->getWrapper('legend', $field['attributes']);
183
+ $output .= $field['legend'];
184
+ $output .= $this->getWrapperEnd('legend');
185
+ if (!empty($field['description'])) {
186
+ $output .= $this->renderDescription($field['description']);
187
+ }
188
+ return $output;
189
+ }
190
+
191
+ /**
192
+ * Outputs the end of a fieldset.
193
+ *
194
+ * @param array $field
195
+ *
196
+ * @return string
197
+ */
198
+ protected function fieldsetEnd(
199
+ /** @noinspection PhpUnusedParameterInspection */
200
+ array $field
201
+ ) {
202
+ $output = '';
203
+ $output .= $this->getWrapperEnd('fieldset');
204
+ return $output;
205
+ }
206
+
207
+ /**
208
+ * Renders a form field including its label and description.
209
+ *
210
+ * @param array $field
211
+ *
212
+ * @return string
213
+ * Html for this form field.
214
+ */
215
+ protected function renderField(array $field)
216
+ {
217
+ $type = $field['type'];
218
+ $id = $field['id'];
219
+ $name = $field['name'];
220
+ $label = isset($field['label']) ? $field['label'] : '';
221
+ $value = isset($field['value']) ? $field['value'] : '';
222
+ $attributes = isset($field['attributes']) ? $field['attributes'] : array();
223
+ $description = isset($field['description']) ? $field['description'] : '';
224
+ $options = isset($field['options']) ? $field['options'] : array();
225
+
226
+ $output = '';
227
+
228
+ $labelAttributes = array();
229
+ if (!empty($attributes['required'])) {
230
+ $labelAttributes['required'] = $attributes['required'];
231
+ }
232
+
233
+ if ($type !== 'hidden') {
234
+ $output .= $this->getWrapper('element');
235
+ $output .= $this->renderLabel($label, $type !== 'radio' && $type !== 'checkbox' ? $id : null, $labelAttributes);
236
+ }
237
+ $output .= $this->renderElement($type, $id, $name, $value, $attributes, $options);
238
+ if ($type !== 'hidden') {
239
+ $output .= $this->renderDescription($description);
240
+ $output .= $this->getWrapperEnd('element');
241
+ }
242
+ return $output;
243
+ }
244
+
245
+ /**
246
+ * Renders a form field itself, ie without label and description.
247
+ *
248
+ * @param string $type
249
+ * @param string $id
250
+ * @param string $name
251
+ * @param string|int $value
252
+ * @param array $attributes
253
+ * @param array $options
254
+ *
255
+ * @return string
256
+ */
257
+ protected function renderElement($type, $id, $name, $value, array $attributes = array(), array $options = array())
258
+ {
259
+ switch ($type) {
260
+ case 'textarea':
261
+ return $this->textarea($id, $name, $value, $attributes);
262
+ case 'select':
263
+ case 'radio':
264
+ case 'checkbox':
265
+ return $this->$type($id, $name, $value, $options, $attributes);
266
+ default:
267
+ return $this->input($type, $id, $name, $value, $attributes);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Renders a descriptive help text.
273
+ *
274
+ * @param string $text
275
+ * @param string $tag
276
+ *
277
+ * @return string
278
+ * The rendered description.
279
+ */
280
+ protected function renderDescription($text, $tag = 'div')
281
+ {
282
+ $output = '';
283
+
284
+ // Help text.
285
+ if (!empty($text)) {
286
+ // Allow for links in the help text, so no filtering anymore.
287
+ //$output .= "<$tag class=\"{$this->descriptionClass}\">" . htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8') . "</$tag>";
288
+ $output .= "<$tag class=\"{$this->descriptionClass}\">$text</$tag>";
289
+ }
290
+
291
+ return $output;
292
+ }
293
+
294
+ /**
295
+ * Renders a label.
296
+ *
297
+ * @param string $text
298
+ * The label text.
299
+ * @param string|null $id
300
+ * The value of the for attribute. If null, not a label tag but a span with
301
+ * a class="label" will be rendered.
302
+ * @param array $attributes
303
+ * Any additional attributes to render for the label. The array is a keyed
304
+ * array, the keys being the attribute names, the values being the
305
+ * value of that attribute. If that value is an array it is rendered as a
306
+ * joined string of the values separated by a space (e.g. multiple classes).
307
+ * @param bool $wrapLabel
308
+ * Whether to wrap this label within the defined label wrapper tag.
309
+ *
310
+ * @return string The rendered label.
311
+ * The rendered label.
312
+ */
313
+ protected function renderLabel($text, $id = null, array $attributes = array(), $wrapLabel = true)
314
+ {
315
+ $output = '';
316
+
317
+ $attributes = $this->addLabelAttributes($attributes, $id);
318
+
319
+ // Tag around main labels.
320
+ if ($wrapLabel) {
321
+ $output .= $this->getWrapper('label', $attributes);
322
+ }
323
+
324
+ // Label.
325
+ $required = !empty($attributes['required']) ? $this->requiredMarkup : '';
326
+ $tag = empty($id) ? $this->multiLabelTag : 'label';
327
+ $output .= '<' . $tag . $this->renderAttributes($attributes) . '>' . htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8') . $required . '</' . $tag . '>';
328
+
329
+ // Tag around labels.
330
+ if ($wrapLabel) {
331
+ $output .= $this->getWrapperEnd('label');
332
+ }
333
+ return $output;
334
+ }
335
+
336
+ /**
337
+ * Renders an input field.
338
+ *
339
+ * @param string $type
340
+ * The input type, required.
341
+ * @param string $id
342
+ * The id attribute of the text field, required.
343
+ * @param string $name
344
+ * The name attribute of the text field, required.
345
+ * @param string $value
346
+ * The initial value of the text field.
347
+ * @param array $attributes
348
+ * Any additional attributes to render for the text field, think of size or
349
+ * maxlength. The array is a keyed array, the keys being the attribute
350
+ * names, the values being the value of that attribute. If that value is an
351
+ * array it is rendered as a joined string of the values separated by a
352
+ * space (e.g. multiple classes).
353
+ *
354
+ * @return string
355
+ * The rendered text field.
356
+ */
357
+ protected function input($type, $id, $name, $value = '', array $attributes = array())
358
+ {
359
+ $output = '';
360
+
361
+ // Tag around input element.
362
+ $output .= $this->getWrapper('input');
363
+
364
+ $attributes = $this->addAttribute($attributes, 'type', $type);
365
+ $attributes = $this->addAttribute($attributes, 'id', $id);
366
+ $attributes = $this->addAttribute($attributes, 'name', $name);
367
+ $attributes = $this->addAttribute($attributes, 'value', $value);
368
+ $output .= '<input' . $this->renderAttributes($attributes) . '/>';
369
+
370
+ // Tag around input element.
371
+ $output .= $this->getWrapperEnd('input');
372
+
373
+ return $output;
374
+ }
375
+
376
+ /**
377
+ * Renders a textarea field.
378
+ *
379
+ * @param string $id
380
+ * The id attribute of the text field, required.
381
+ * @param string $name
382
+ * The name attribute of the text field, required.
383
+ * @param string $value
384
+ * The initial value of the text field.
385
+ * @param array $attributes
386
+ * Any additional attributes to render for the text field, think of size or
387
+ * maxlength. The array is a keyed array, the keys being the attribute
388
+ * names, the values being the value of that attribute. If that value is an
389
+ * array it is rendered as a joined string of the values separated by a
390
+ * space (e.g. multiple classes).
391
+ *
392
+ * @return string The rendered textarea field.
393
+ * The rendered textarea field.
394
+ */
395
+ protected function textarea($id, $name, $value = '', array $attributes = array())
396
+ {
397
+ $output = '';
398
+
399
+ // Tag around input element.
400
+ $output .= $this->getWrapper('input');
401
+
402
+ $attributes = $this->addAttribute($attributes, 'id', $id);
403
+ $attributes = $this->addAttribute($attributes, 'name', $name);
404
+ $output .= '<textarea' . $this->renderAttributes($attributes) . '>';
405
+ $output .= htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8');
406
+ $output .= '</textarea>';
407
+
408
+ // Tag around input element.
409
+ $output .= $this->getWrapperEnd('input');
410
+
411
+ return $output;
412
+ }
413
+
414
+ /**
415
+ * Renders a text field (input tag with type="text").
416
+ *
417
+ * @param string $id
418
+ * The id attribute of the text field, required.
419
+ * @param string $name
420
+ * The name attribute of the text field, required.
421
+ * @param string $value
422
+ * The initial value of the text field.
423
+ * @param array $attributes
424
+ * Any additional attributes to render for the text field, think of size or
425
+ * maxlength. The array is a keyed array, the keys being the attribute
426
+ * names, the values being the value of that attribute. If that value is an
427
+ * array it is rendered as a joined string of the values separated by a
428
+ * space (e.g. multiple classes).
429
+ *
430
+ * @return string
431
+ * The rendered text field.
432
+ *
433
+ * @deprecated
434
+ */
435
+ protected function text($id, $name, $value = '', array $attributes = array())
436
+ {
437
+ return $this->input('text', $id, $name, $value, $attributes);
438
+ }
439
+
440
+ /**
441
+ * Renders a password field (input tag with type="password").
442
+ *
443
+ * @param string $id
444
+ * The id attribute of the password field, required.
445
+ * @param string $name
446
+ * The name attribute of the password field, required.
447
+ * @param string $value
448
+ * The initial value of the field.
449
+ * @param array $attributes
450
+ * Any additional attributes to render for this field. The array is a keyed
451
+ * array, the keys being the attribute names, the values being the value of
452
+ * that attribute. If that value is an array it is rendered as a joined
453
+ * string of the values separated by a space (e.g. multiple classes).
454
+ *
455
+ * @return string
456
+ * The rendered field.
457
+ *
458
+ * @deprecated
459
+ */
460
+ protected function password($id, $name, $value = '', array $attributes = array())
461
+ {
462
+ return $this->input('password', $id, $name, $value, $attributes);
463
+ }
464
+
465
+ /**
466
+ * Renders a select element.
467
+ *
468
+ * @param string $id
469
+ * The id attribute of the select, required.
470
+ * @param string $name
471
+ * The name attribute of the select, required.
472
+ * @param mixed|null $selected
473
+ * The selected value, null if no value has to be set to selected.
474
+ * @param array $options
475
+ * The list of options as a keyed array, the keys being the value attribute
476
+ * of the option tag, the values being the text within the option tag.
477
+ * @param array $attributes
478
+ * Any additional attributes to render for the select tag, think of disabled.
479
+ * The array is a keyed array, the keys being the attribute names, the
480
+ * values being the value of that attribute. If that value is an array it is
481
+ * rendered as a joined string of the values separated by a space (e.g.
482
+ * multiple classes).
483
+ *
484
+ * @return string The rendered select element.
485
+ * The rendered select element.
486
+ */
487
+ protected function select($id, $name, $selected, array $options, array $attributes = array())
488
+ {
489
+ $output = '';
490
+
491
+ // Tag around select element: same as for an input element.
492
+ $output .= $this->getWrapper('input');
493
+
494
+ // Select tag.
495
+ $attributes = array_merge(array('id' => $id, 'name' => $name), $attributes);
496
+ $output .= '<select' . $this->renderAttributes($attributes) . '>';
497
+
498
+ // Options.
499
+ foreach ($options as $value => $text) {
500
+ $optionAttributes = array('value' => $value);
501
+ if ($this->compareValues($selected, $value)) {
502
+ $optionAttributes['selected'] = true;
503
+ }
504
+ $output .= '<option' . $this->renderAttributes($optionAttributes) . '>' . htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8') . '</option>';
505
+ }
506
+
507
+ // End tag.
508
+ $output .= '</select>';
509
+ // Tag around select element.
510
+ $output .= $this->getWrapperEnd('input');
511
+
512
+ return $output;
513
+ }
514
+
515
+ /**
516
+ * Renders a list of radio buttons (input tag with type="radio").
517
+ *
518
+ * @param string $id
519
+ * The id attribute for all the radio buttons, required.
520
+ * @param string $name
521
+ * The name attribute for all the radio buttons, required.
522
+ * @param mixed|null $selected
523
+ * The selected value, null if no value has to be set to selected.
524
+ * @param array $options
525
+ * The list of radio buttons as a keyed array, the keys being the value
526
+ * attribute of the radio button, the values being the label of the radio
527
+ * button.
528
+ * @param array $attributes
529
+ * Any additional attributes to render on the div tag. The array is a keyed
530
+ * array, the keys being the attribute names, the values being the value of
531
+ * that attribute. If that value is an array it is rendered as a joined
532
+ * string of the values separated by a space (e.g. multiple classes).
533
+ *
534
+ * @return string The rendered radio buttons.
535
+ * The rendered radio buttons.
536
+ */
537
+ protected function radio($id, $name, $selected, array $options, array $attributes = array())
538
+ {
539
+ $output = '';
540
+
541
+ // Handling of required attribute: may appear on on all radio buttons with
542
+ // the same name.
543
+ $required = !empty($attributes['required']);
544
+ unset($attributes['required']);
545
+
546
+ // Tag(s) around radio buttons.
547
+ $output .= $this->getWrapper('input', $attributes);
548
+ $output .= $this->getWrapper('radio', $attributes);
549
+
550
+ // Radio buttons.
551
+ foreach ($options as $value => $text) {
552
+ $radioAttributes = $this->getRadioAttributes($id, $name, $value);
553
+ $radioAttributes = $this->addAttribute($radioAttributes, 'required', $required);
554
+ if ($this->compareValues($selected, $value)) {
555
+ $radioAttributes['checked'] = true;
556
+ }
557
+ $output .= $this->getWrapper('radio1');
558
+ $output .= '<input' . $this->renderAttributes($radioAttributes) . '>';
559
+ $output .= $this->renderLabel($text, $radioAttributes['id'], array(), false);
560
+ $output .= $this->getWrapperEnd('radio1');
561
+ }
562
+
563
+ // End tag.
564
+ $output .= $this->getWrapperEnd('radio');
565
+ $output .= $this->getWrapperEnd('input');
566
+
567
+ return $output;
568
+ }
569
+
570
+ /**
571
+ * Renders a list of checkboxes (input tag with type="checkbox") enclosed in a
572
+ * div.
573
+ *
574
+ * @param string $id
575
+ * The id prefix attribute for the checkboxes, required.
576
+ * @param string $name
577
+ * The name attribute for all the checkboxes, required. When rendering
578
+ * multiple checkboxes, use a name that ends with [] for easy PHP processing.
579
+ * @param array $selected
580
+ * The selected values.
581
+ * @param array $options
582
+ * The list of checkboxes as a keyed array, the keys being the value
583
+ * attribute of the checkbox, the values being the label of the checkbox.
584
+ * @param array $attributes
585
+ * Any additional attributes to render on the div tag. The array is a keyed
586
+ * array, the keys being the attribute names, the values being the value of
587
+ * that attribute. If that value is an array it is rendered as a joined
588
+ * string of the values separated by a space (e.g. multiple classes).
589
+ *
590
+ * @return string The rendered checkboxes.
591
+ * The rendered checkboxes.
592
+ */
593
+ protected function checkbox($id, $name, array $selected, array $options, array $attributes = array())
594
+ {
595
+ $output = '';
596
+
597
+ // Div tag.
598
+ unset($attributes['required']);
599
+ $output .= $this->getWrapper('input', $attributes);
600
+ $output .= $this->getWrapper('checkbox', $attributes);
601
+
602
+ // Checkboxes.
603
+ foreach ($options as $value => $text) {
604
+ $checkboxAttributes = $this->getCheckboxAttributes($id, $name, $value);
605
+ if (in_array($value, $selected)) {
606
+ $checkboxAttributes['checked'] = true;
607
+ }
608
+ $output .= $this->getWrapper('checkbox1');
609
+ $output .= '<input' . $this->renderAttributes($checkboxAttributes) . '>';
610
+ $output .= $this->renderLabel($text, $checkboxAttributes['id'], array(), false);
611
+ $output .= $this->getWrapperEnd('checkbox1');
612
+ }
613
+
614
+ // End tag.
615
+ $output .= $this->getWrapperEnd('checkbox');
616
+ $output .= $this->getWrapperEnd('input');
617
+
618
+ return $output;
619
+ }
620
+
621
+ /**
622
+ * Renders a markup (free format output) element.
623
+ *
624
+ * @param array $field
625
+ *
626
+ * @return string
627
+ * The rendered markup.
628
+ */
629
+ protected function renderMarkup(array $field)
630
+ {
631
+ $output = '';
632
+ $output .= $this->getWrapper('markup');
633
+ $output .= $field['value'];
634
+ $output .= $this->getWrapperEnd('markup');
635
+ return $output;
636
+ }
637
+
638
+ /**
639
+ * @param string $type
640
+ * @param array $attributes
641
+ *
642
+ * @return string
643
+ */
644
+ protected function getWrapper($type, array $attributes = array())
645
+ {
646
+ $tag = "{$type}WrapperTag";
647
+ $class = "{$type}WrapperClass";
648
+ $output = '';
649
+ if (!empty($this->$tag)) {
650
+ if (!empty($this->$class)) {
651
+ $attributes = $this->addAttribute($attributes, 'class', $this->$class);
652
+ }
653
+ $output .= "<{$this->$tag}";
654
+ $output .= $this->renderAttributes($attributes);
655
+ $output .= '>';
656
+ }
657
+ return $output;
658
+ }
659
+
660
+ /**
661
+ * @param string $type
662
+ *
663
+ * @return string
664
+ */
665
+ protected function getWrapperEnd($type)
666
+ {
667
+ $tag = "{$type}WrapperTag";
668
+ $output = '';
669
+ if (!empty($this->$tag)) {
670
+ $output .= "</{$this->$tag}>";
671
+ }
672
+ return $output;
673
+ }
674
+
675
+ /**
676
+ * Renders a list of attributes.
677
+ *
678
+ * @param array $attributes
679
+ *
680
+ * @return string
681
+ * html string with the rendered attributes and 1 space in front of it.
682
+ */
683
+ protected function renderAttributes(array $attributes)
684
+ {
685
+ $attributeString = '';
686
+ foreach ($attributes as $key => $value) {
687
+ if (is_array($value)) {
688
+ $value = implode(' ', $value);
689
+ }
690
+ // Skip attributes that are not to be set (required, disabled, ...).
691
+ if ($value !== false) {
692
+ $attributeString .= ' ' . htmlspecialchars($key, ENT_NOQUOTES, 'UTF-8');
693
+ // HTML5: do not add a value to boolean attributes.
694
+ // HTML4: add the name of the key as value for the attribute.
695
+ if (!$this->html5 && $value === true) {
696
+ $value = $key;
697
+ }
698
+ if ($value !== true) {
699
+ $attributeString .= '="' . htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8') . '"';
700
+ }
701
+ }
702
+ }
703
+ return $attributeString;
704
+ }
705
+
706
+ /**
707
+ * @param array $attributes
708
+ * The array of attributes to add the value to.
709
+ * @param string $attribute
710
+ * The name of the attribute to add.
711
+ * @param string $value
712
+ * The value of the attribute to add.
713
+ * @param bool|null $multiple
714
+ * Allow multiple values for the given attribute. By default this is
715
+ * only allowed for the class attribute.
716
+ *
717
+ * @return array
718
+ * The set of attributes with the value added.
719
+ */
720
+ protected function addAttribute(array $attributes, $attribute, $value, $multiple = null)
721
+ {
722
+ // Do add false and 0, but not an empty string or null.
723
+ if ($value !== null && $value !== '') {
724
+ if ($multiple === null) {
725
+ $multiple = $attribute === 'class';
726
+ }
727
+
728
+ if ($multiple) {
729
+ // Multiple values allowed: set or add, not overwriting.
730
+ if (isset($attributes[$attribute])) {
731
+ // Assure it is an array, not a scalar
732
+ $attributes[$attribute] = (array) $attributes[$attribute];
733
+ } else {
734
+ // Set as an empty array
735
+ $attributes[$attribute] = array();
736
+ }
737
+ // Now we know for sure that it is an array, add it.
738
+ $attributes[$attribute][] = $value;
739
+ } else {
740
+ // Single value: just set, possibly overwriting.
741
+ $attributes[$attribute] = $value;
742
+ }
743
+ }
744
+ return $attributes;
745
+ }
746
+
747
+ /**
748
+ * @param array $attributes
749
+ * @param string $id
750
+ *
751
+ * @return array
752
+ */
753
+ protected function addLabelAttributes(array $attributes, $id)
754
+ {
755
+ $attributes = $this->addAttribute($attributes, 'for', $id);
756
+ if (empty($id)) {
757
+ $attributes = $this->addAttribute($attributes, 'class', $this->multiLabelClass);
758
+ }
759
+ return $attributes;
760
+ }
761
+
762
+ /**
763
+ * @param string $id
764
+ * @param string $name
765
+ * @param string $value
766
+ *
767
+ * @return array
768
+ */
769
+ protected function getCheckboxAttributes(
770
+ /** @noinspection PhpUnusedParameterInspection */
771
+ $id, $name, $value
772
+ ) {
773
+ $checkboxAttributes = array(
774
+ 'type' => 'checkbox',
775
+ 'id' => "{$name}_{$value}",
776
+ 'name' => $value,
777
+ 'value' => 1,
778
+ );
779
+ return $checkboxAttributes;
780
+ }
781
+
782
+ /**
783
+ * @param string $id
784
+ * @param string $name
785
+ * @param string $value
786
+ *
787
+ * @return array
788
+ */
789
+ protected function getRadioAttributes($id, $name, $value)
790
+ {
791
+ $radioAttributes = array(
792
+ 'type' => 'radio',
793
+ 'id' => "{$id}_{$value}",
794
+ 'name' => $name,
795
+ 'value' => $value,
796
+ );
797
+ return $radioAttributes;
798
+ }
799
+
800
+ /**
801
+ * Compares an option and a value to see if this option should be "selected".
802
+ *
803
+ * @param string|int|array $value
804
+ * @param string|int $optionValue
805
+ *
806
+ * @return bool
807
+ * If this option equals the value.
808
+ */
809
+ protected function compareValues($value, $optionValue)
810
+ {
811
+ return is_array($value) ? in_array((string) $optionValue, $value) : (string) $optionValue === (string) $value;
812
+ }
813
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Log.php ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ use Siel\Acumulus\Web\ConfigInterface;
5
+
6
+ /**
7
+ * Defines a logger class. This class is supposed to be overridden by shop
8
+ * specific classes to integrate with the shop specific way of logging.
9
+ */
10
+ class Log
11
+ {
12
+ const None = 0;
13
+ const Error = 1;
14
+ const Warning = 2;
15
+ const Notice = 3;
16
+ const Debug = 4;
17
+ const NotYetSet = 5;
18
+
19
+ /** @var Log */
20
+ static protected $instance = null;
21
+
22
+ /**
23
+ * Returns an instance of the log class (or web shop specific child class).
24
+ *
25
+ * @return Log
26
+ */
27
+ public static function getInstance()
28
+ {
29
+ return static::$instance;
30
+ }
31
+
32
+ /** @var int */
33
+ protected $logLevel;
34
+
35
+ /** @var ConfigInterface */
36
+ protected $config;
37
+
38
+ /**
39
+ * Log constructor.
40
+ *
41
+ * @param ConfigInterface $config
42
+ */
43
+ public function __construct($config)
44
+ {
45
+ // Start with logging everything. Soon after the creation of this log object
46
+ // the log level should be set based on the configuration.
47
+ $this->logLevel = static::NotYetSet;
48
+ $this->config = $config;
49
+ static::$instance = $this;
50
+ }
51
+
52
+ /**
53
+ * Gets the actual log level.
54
+ *
55
+ * @return int
56
+ */
57
+ public function getLogLevel()
58
+ {
59
+ // To support lazy load of the config, the log level is not yet set until
60
+ // actually needed.
61
+ if ($this->logLevel === static::NotYetSet && $this->config !== null) {
62
+ $this->logLevel = $this->config->getLogLevel();
63
+ }
64
+ return $this->logLevel;
65
+ }
66
+
67
+ /**
68
+ * Sets the log level, eg. based on configuration.
69
+ *
70
+ * @param int $logLevel
71
+ */
72
+ public function setLogLevel($logLevel)
73
+ {
74
+ $this->logLevel = $logLevel;
75
+ }
76
+
77
+ /**
78
+ * Returns a textual representation of the severity.
79
+ *
80
+ * @param int $severity
81
+ * One of the constants of this class.
82
+ *
83
+ * @return string
84
+ * A textual representation of the severity.
85
+ */
86
+ protected function getSeverityString($severity)
87
+ {
88
+ switch ($severity) {
89
+ case Log::Error:
90
+ return 'Error';
91
+ case Log::Warning:
92
+ return 'Warning';
93
+ case Log::Notice:
94
+ return 'Notice';
95
+ case Log::Debug:
96
+ default:
97
+ return 'Debug';
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Formats and logs the message if the severity equals or is worse than the
103
+ * current log level.
104
+ *
105
+ * Formatting involves:
106
+ * - calling vsprintf() if $args is not empty
107
+ * - adding "Acumulus {version} {severity}: " in front of the message.
108
+ *
109
+ * @param int $severity
110
+ * @param string $message
111
+ * @param array $args
112
+ *
113
+ * @return string
114
+ * The full formatted message whether it got logged or not.
115
+ */
116
+ public function log($severity, $message, array $args = array())
117
+ {
118
+ if ($this->getLogLevel() >= $severity) {
119
+ if (count($args) > 0) {
120
+ $message = vsprintf($message, $args);
121
+ }
122
+ $message = sprintf('Acumulus: %s', $message);
123
+ $this->write($message, $severity);
124
+ }
125
+ return $message;
126
+ }
127
+
128
+ /**
129
+ * Logs a debug message
130
+ *
131
+ * @param string $message,...
132
+ * The message to log, optionally followed by arguments. If there are
133
+ * arguments the $message is passed through vsprintf().
134
+ *
135
+ * @return string
136
+ * The full formatted message whether it got logged or not.
137
+ */
138
+ public function debug($message)
139
+ {
140
+ $args = func_get_args();
141
+ array_shift($args);
142
+ return $this->log(Log::Debug, $message, $args);
143
+ }
144
+
145
+ /**
146
+ * Logs a notice.
147
+ *
148
+ * @param string $message,...
149
+ * The message to log, optionally followed by arguments. If there are
150
+ * arguments the $message is passed through vsprintf().
151
+ *
152
+ * @return string
153
+ * The full formatted message whether it got logged or not.
154
+ */
155
+ public function notice($message)
156
+ {
157
+ $args = func_get_args();
158
+ array_shift($args);
159
+ return $this->log(Log::Notice, $message, $args);
160
+ }
161
+
162
+ /**
163
+ * Logs a warning.
164
+ *
165
+ * @param string $message,...
166
+ * The message to log, optionally followed by arguments. If there are
167
+ * arguments the $message is passed through vsprintf().
168
+ *
169
+ * @return string
170
+ * The full formatted message whether it got logged or not.
171
+ */
172
+ public function warning($message)
173
+ {
174
+ $args = func_get_args();
175
+ array_shift($args);
176
+ return $this->log(Log::Warning, $message, $args);
177
+ }
178
+
179
+ /**
180
+ * Logs an error message.
181
+ *
182
+ * @param string $message,...
183
+ * The message to log, optionally followed by arguments. If there are
184
+ * arguments the $message is passed through vsprintf().
185
+ *
186
+ * @return string
187
+ * The full formatted message whether it got logged or not.
188
+ */
189
+ public function error($message)
190
+ {
191
+ $args = func_get_args();
192
+ array_shift($args);
193
+ return $this->log(Log::Error, $message, $args);
194
+ }
195
+
196
+ /**
197
+ * Writes the message to the actual log sink.
198
+ *
199
+ * This base implementation sends the message to error_log(). Override if the
200
+ * web shop offers its own log mechanism.
201
+ *
202
+ * @param string $message
203
+ * @param int $severity
204
+ */
205
+ protected function write($message, $severity)
206
+ {
207
+ $message = sprintf('%s - %s', $this->getSeverityString($severity), $message);
208
+ error_log($message);
209
+ }
210
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/MailTranslations.php ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Contains translations for mails.
6
+ */
7
+ class MailTranslations extends TranslationCollection
8
+ {
9
+ protected $nl = array(
10
+ // Mails.
11
+ 'mail_sender_name' => 'Uw webwinkel',
12
+ 'message_no_invoice' => 'niet aangemaakt in Acumulus',
13
+ 'mail_subject_errors' => 'Fouten bij het verzenden van een factuur naar Acumulus',
14
+ 'mail_subject_warnings' => 'Waarschuwingen bij het verzenden van een factuur naar Acumulus',
15
+ 'mail_subject_debug' => 'Factuur verzonden naar Acumulus',
16
+ 'mail_text_errors' => <<<LONGSTRING
17
+ Bij het verzenden van een factuur naar Acumulus zijn er foutmeldingen terug
18
+ gestuurd. Het versturen is niet gelukt. U dient de factuur aan te passen en
19
+ nogmaals te versturen of deze handmatig aan te maken in Acumulus.
20
+ LONGSTRING
21
+ ,
22
+ 'mail_html_errors' => <<<LONGSTRING
23
+ <p>Bij het verzenden van een factuur naar Acumulus zijn er foutmeldingen terug
24
+ gestuurd. Het versturen is niet gelukt. U dient de factuur aan te passen en
25
+ nogmaals te versturen of deze handmatig aan te maken in Acumulus.</p>
26
+ LONGSTRING
27
+ ,
28
+ 'mail_text_warnings' => <<<LONGSTRING
29
+ Bij het verzenden van een factuur naar Acumulus zijn er waarschuwingen terug
30
+ gestuurd. De factuur is aangemaakt in Acumulus, maar u dient deze te
31
+ controleren op correctheid.
32
+ LONGSTRING
33
+ ,
34
+ 'mail_html_warnings' => <<<LONGSTRING
35
+ <p>Bij het verzenden van een factuur naar Acumulus zijn er waarschuwingen terug
36
+ gestuurd. De factuur is aangemaakt in Acumulus, maar u dient deze te
37
+ controleren op correctheid.</p>
38
+ LONGSTRING
39
+ ,
40
+ 'mail_text_debug' => <<<LONGSTRING
41
+ Onderstaande factuur is succesvol naar Acumulus verstuurd. Normaal gesproken
42
+ krijgt u daar geen bercht van, maar omdat u dit zo heeft ingesteld krijgt u
43
+ hier nu toch bericht van.
44
+ LONGSTRING
45
+ ,
46
+ 'mail_html_debug' => <<<LONGSTRING
47
+ <p>Onderstaande factuur is succesvol naar Acumulus verstuurd. Normaal gesproken
48
+ krijgt u daar geen bercht van, maar omdat u dit zo heeft ingesteld krijgt u
49
+ hier nu toch bericht van.</p>
50
+ LONGSTRING
51
+ ,
52
+ 'mail_text' => <<<LONGSTRING
53
+ {status_specific_text}
54
+
55
+ (Webshop){invoice_source_type}: {invoice_source_reference}
56
+ (Acumulus) factuur: {acumulus_invoice_id}
57
+ Verzendstatus: {status} {status_message}.
58
+
59
+ Berichten:
60
+ {messages_text}
61
+
62
+ Meer informatie over eventueel vermeldde foutcodes kunt u vinden op
63
+ https://apidoc.sielsystems.nl/node/16.
64
+ LONGSTRING
65
+ ,
66
+ 'mail_html' => <<<LONGSTRING
67
+ {status_specific_html}
68
+ <table>
69
+ <tr><td>(Webshop){invoice_source_type}:</td><td>{invoice_source_reference}</td></tr>
70
+ <tr><td>(Acumulus) factuur:</td><td>{acumulus_invoice_id}</td></tr>
71
+ <tr><td>Verzendstatus:</td><td>{status} {status_message}.</td></tr>
72
+ </table>
73
+ <p>Berichten:<br>
74
+ {messages_html}</p>
75
+ <p>Meer informatie over eventueel vermeldde foutcodes kunt u vinden op
76
+ <a href="https://apidoc.sielsystems.nl/node/16">Acumulus - API documentation: exit and warning codes</a>.</p>
77
+ LONGSTRING
78
+ ,
79
+ );
80
+
81
+ protected $en = array(
82
+ 'mail_sender_name' => 'Your web store',
83
+ 'message_no_invoice' => 'not created in Acumulus',
84
+ 'mail_subject_errors' => 'Errors on sending an invoice to Acumulus',
85
+ 'mail_subject_warnings' => 'Warnings on sending an invoice to Acumulus',
86
+ 'mail_subject_debug' => 'Invoice sent to Acumulus',
87
+ 'mail_text_errors' => <<<LONGSTRING
88
+ On sending an invoice to Acumulus, some errors occurred. The invoice has NOT
89
+ been created. You will have to manually create the invoice in Acumulus or
90
+ adapt it in your web shop and resend it to Acumulus.
91
+ LONGSTRING
92
+ ,
93
+ 'mail_html_errors' => <<<LONGSTRING
94
+ <p>On sending an invoice to Acumulus, some errors occurred. The invoice has NOT
95
+ been created. You will have to manually create the invoice in Acumulus or
96
+ adapt it in your web shop and resend it to Acumulus.
97
+ </p>
98
+ LONGSTRING
99
+ ,
100
+ 'mail_text_warnings' => <<<LONGSTRING
101
+ On sending an invoice to Acumulus, some warnings occurred, but the invoice
102
+ has been created. However, we advice you to check the invoice in Acumulus
103
+ for correctness.
104
+ LONGSTRING
105
+ ,
106
+ 'mail_html_warnings' => <<<LONGSTRING
107
+ <p>On sending an invoice to Acumulus, some warnings occurred, but the invoice
108
+ has been created. However, we advice you to check the invoice in Acumulus for
109
+ correctness.</p>
110
+ LONGSTRING
111
+ ,
112
+ 'mail_text_debug' => <<<LONGSTRING
113
+ The invoice below has successfully been sent to Acumulus. Normally you won't
114
+ receive a message saying so, but because you configured so, you do receive
115
+ this message now.
116
+ LONGSTRING
117
+ ,
118
+ 'mail_html_debug' => <<<LONGSTRING
119
+ <p>The invoice below has successfully been sent to Acumulus. Normally you won't
120
+ receive a message saying so, but because you configured so, you do receive
121
+ this message now.</p>
122
+ LONGSTRING
123
+ ,
124
+ 'mail_text' => <<<LONGSTRING
125
+ {status_specific_text}
126
+
127
+ (Webshop){invoice_source_type}: {invoice_source_reference}
128
+ (Acumulus) invoice: {acumulus_invoice_id}
129
+ Send status: {status} {status_message}.
130
+
131
+ Messages:
132
+ {messages_text}
133
+
134
+ At https://apidoc.sielsystems.nl/node/16 you can find more information
135
+ regarding any error codes mentioned above.
136
+ LONGSTRING
137
+ ,
138
+ 'mail_html' => <<<LONGSTRING
139
+ {status_specific_html}
140
+ <table>
141
+ <tr><td>(Webshop){invoice_source_type}:</td><td>{invoice_source_reference}</td></tr>
142
+ <tr><td>(Acumulus) invoice:</td><td>{acumulus_invoice_id}</td></tr>
143
+ <tr><td>Send status:</td><td>{status} {status_message}.</td></tr>
144
+ </table>
145
+ <p>Messages:<br>
146
+ {messages_html}</p>
147
+ <p>At <a href="https://apidoc.sielsystems.nl/node/16">Acumulus - API documentation: exit and warning codes</a>
148
+ you can find more information regarding any error codes mentioned above.</p>
149
+ LONGSTRING
150
+ ,
151
+
152
+ );
153
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Mailer.php ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ use Siel\Acumulus\Web\ConfigInterface;
5
+ use Siel\Acumulus\Web\Service;
6
+
7
+ /**
8
+ * Class Mailer allows to send mails. This class should be overridden per shop
9
+ * to use the shop provided mailing features.
10
+ */
11
+ abstract class Mailer
12
+ {
13
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
14
+ protected $translator;
15
+
16
+ /** @var \Siel\Acumulus\Web\Service */
17
+ protected $service;
18
+
19
+ /**
20
+ * @param \Siel\Acumulus\Web\ConfigInterface $config
21
+ * @param TranslatorInterface $translator
22
+ * @param \Siel\Acumulus\Web\Service $service
23
+ */
24
+ public function __construct(ConfigInterface $config, TranslatorInterface $translator, Service $service)
25
+ {
26
+ $this->config = $config;
27
+ $this->service = $service;
28
+
29
+ $this->translator = $translator;
30
+ $translations = new MailTranslations();
31
+ $this->translator->add($translations);
32
+ }
33
+
34
+ /**
35
+ * Helper method to translate strings.
36
+ *
37
+ * @param string $key
38
+ * The key to get a translation for.
39
+ *
40
+ * @return string
41
+ * The translation for the given key or the key itself if no translation
42
+ * could be found.
43
+ */
44
+ protected function t($key)
45
+ {
46
+ return $this->translator->get($key);
47
+ }
48
+
49
+ /**
50
+ * Sends an email with the results of a sent invoice.
51
+ *
52
+ * The mail is sent to the shop administrator (emailonerror setting).
53
+ *
54
+ * @param array $result
55
+ * @param string[] $messages
56
+ * @param string $invoiceSourceType
57
+ * @param string $invoiceSourceReference
58
+ *
59
+ * @return bool
60
+ * Success.
61
+ */
62
+ abstract public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference);
63
+
64
+ /**
65
+ * Returns the mail from name.
66
+ *
67
+ * @return string
68
+ */
69
+ protected function getFromName()
70
+ {
71
+ return $this->t('mail_sender_name');
72
+ }
73
+
74
+ /**
75
+ * Returns the mail to address.
76
+ *
77
+ * @return string
78
+ */
79
+ protected function getToAddress()
80
+ {
81
+ $credentials = $this->config->getCredentials();
82
+ if (isset($credentials['emailonerror'])) {
83
+ return $credentials['emailonerror'];
84
+ }
85
+ if (method_exists($this->config, 'getHostName')) {
86
+ return 'webshop@' . $this->config->getHostName();
87
+ }
88
+ return 'you@example.com';
89
+ }
90
+
91
+ /**
92
+ * Returns the subject for the mail.
93
+ *
94
+ * The subject depends on the result status.
95
+ *
96
+ * @param array $result
97
+ *
98
+ * @return string
99
+ */
100
+ protected function getSubject(array $result)
101
+ {
102
+ switch ($result['status']) {
103
+ case ConfigInterface::Status_Exception:
104
+ case ConfigInterface::Status_Errors:
105
+ return $this->t('mail_subject_errors');
106
+ case ConfigInterface::Status_Warnings:
107
+ return $this->t('mail_subject_warnings');
108
+ case ConfigInterface::Status_Success:
109
+ default:
110
+ return $this->t('mail_subject_debug');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Returns the subject for the mail.
116
+ *
117
+ * The subject depends on the result status.
118
+ *
119
+ * @param array $result
120
+ *
121
+ * @return string
122
+ */
123
+ protected function getStatusSpecificBody(array $result)
124
+ {
125
+ $texts = array();
126
+ switch ($result['status']) {
127
+ case ConfigInterface::Status_Exception:
128
+ case ConfigInterface::Status_Errors:
129
+ $texts['text'] = $this->t('mail_text_errors');
130
+ $texts['html'] = $this->t('mail_html_errors');
131
+ break;
132
+ case ConfigInterface::Status_Warnings:
133
+ $texts['text'] = $this->t('mail_text_warnings');
134
+ $texts['html'] = $this->t('mail_html_warnings');
135
+ break;
136
+ case ConfigInterface::Status_Success:
137
+ default:
138
+ $texts['text'] = $this->t('mail_text_debug');
139
+ $texts['html'] = $this->t('mail_html_debug');
140
+ break;
141
+ }
142
+ return $texts;
143
+ }
144
+
145
+ /**
146
+ * Returns the mail body as text and as html.
147
+ *
148
+ * @param array $result
149
+ * @param string[] $messages
150
+ * @param string $invoiceSourceType
151
+ * @param string $invoiceSourceReference
152
+ *
153
+ * @return array
154
+ * An array with keys text and html.
155
+ */
156
+ protected function getBody(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
157
+ {
158
+ $bodyTexts = $this->getStatusSpecificBody($result);
159
+ $replacements = array(
160
+ '{invoice_source_type}' => $this->t($invoiceSourceType),
161
+ '{invoice_source_reference}' => $invoiceSourceReference,
162
+ '{acumulus_invoice_id}' => isset($result['invoice']['invoicenumber']) ? $result['invoice']['invoicenumber'] : $this->t('message_no_invoice'),
163
+ '{status}' => $result['status'],
164
+ '{status_message}' => $this->service->getStatusText($result['status']),
165
+ '{status_specific_text}' => $bodyTexts['text'],
166
+ '{status_specific_html}' => $bodyTexts['html'],
167
+ '{messages_text}' => $this->service->messagesToText($messages),
168
+ '{messages_html}' => $this->service->messagesToHtml($messages),
169
+ );
170
+ $text = $this->t('mail_text');
171
+ $text = strtr($text, $replacements);
172
+ $html = $this->t('mail_html');
173
+ $html = strtr($html, $replacements);
174
+ return array('text' => $text, 'html' => $html);
175
+ }
176
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Number.php ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Class Number contains features to work with numbers, especially amounts.
6
+ */
7
+ class Number
8
+ {
9
+ /**
10
+ * Returns the range within which the result of the division should fall given
11
+ * the precision range for the 2 numbers to divide.
12
+ *
13
+ * @param float $numerator
14
+ * @param float $denominator
15
+ * @param float $precisionNumerator
16
+ * @param float $precisionDenominator
17
+ *
18
+ * @return array
19
+ * Array of floats with keys min, max and calculated.
20
+ */
21
+ static public function getDivisionRange($numerator, $denominator, $precisionNumerator = 0.01, $precisionDenominator = 0.01)
22
+ {
23
+ // The actual value can be half the precision lower or higher.
24
+ $numeratorHalfRange = $precisionNumerator / 2.0;
25
+ $denominatorHalfRange = $precisionDenominator / 2.0;
26
+
27
+ // The min values should be closer to 0 then the value.
28
+ // The max values should be further from 0 then the value.
29
+ if ($numerator < 0.0) {
30
+ $numeratorHalfRange = -$numeratorHalfRange;
31
+ }
32
+ $minNumerator = $numerator - $numeratorHalfRange;
33
+ $maxNumerator = $numerator + $numeratorHalfRange;
34
+
35
+ if ($denominator < 0.0) {
36
+ $denominatorHalfRange = -$denominatorHalfRange;
37
+ }
38
+ $minDenominator = $denominator - $denominatorHalfRange;
39
+ $maxDenominator = $denominator + $denominatorHalfRange;
40
+
41
+ // We get the min value of the division by dividing the minimum numerator by
42
+ // the maximum denominator and vice versa.
43
+ $min = $minNumerator / $maxDenominator;
44
+ $max = $maxNumerator / $minDenominator;
45
+ $calculated = $numerator / $denominator;
46
+
47
+ return array('min' => $min, 'calculated' => $calculated, 'max' => $max);
48
+ }
49
+
50
+ /**
51
+ * Helper method to do a float comparison.
52
+ *
53
+ * @param float $f1
54
+ * @param float $f2
55
+ * @param float $maxDiff
56
+ *
57
+ * @return bool
58
+ * True if the the floats are "equal", i.e. do not differ more than the
59
+ * specified maximum difference.
60
+ */
61
+ static public function floatsAreEqual($f1, $f2, $maxDiff = 0.005)
62
+ {
63
+ return abs((float) $f2 - (float) $f1) < $maxDiff;
64
+ }
65
+
66
+ /**
67
+ * indicates if a float is to be considered zero.
68
+ *
69
+ * This is a wrapper around floatsAreEqual() for the often used case where
70
+ * an amount is checked for being 0.0.
71
+ *
72
+ * @param $f1
73
+ * @param float $maxDiff
74
+ *
75
+ * @return bool
76
+ */
77
+ static public function isZero($f1, $maxDiff = 0.001)
78
+ {
79
+ return static::floatsAreEqual($f1, 0.0, $maxDiff);
80
+ }
81
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Requirements.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Defines and checks the requirements for this library. Used on install.
6
+ */
7
+ class Requirements
8
+ {
9
+ /**
10
+ * Checks if the requirements for the environment are met.
11
+ *
12
+ * @return string[]
13
+ * An array with messages regarding missing requirements, empty if all
14
+ * requirements are met. The keys are the translations keys, the values are
15
+ * the English translations.
16
+ */
17
+ static public function check()
18
+ {
19
+ $result = array();
20
+
21
+ // PHP 5.3 is a requirement as well because we use namespaces. But as the
22
+ // parser will already have failed fatally before we get here, it makes no
23
+ // sense to check here.
24
+ if (!extension_loaded('curl')) {
25
+ $result['message_error_req_curl'] = 'The CURL PHP extension needs to be activated on your server for this module to work.';
26
+ }
27
+ if (!extension_loaded('simplexml')) {
28
+ $result['message_error_req_xml'] = 'The SimpleXML extension needs to be activated on your server for the Acumulus module to be able to work with the XML format.';
29
+ }
30
+ if (!extension_loaded('dom')) {
31
+ $result['message_error_req_dom'] = 'The DOM PHP extension needs to be activated on your server for the Acumulus mpdule to work.';
32
+ }
33
+
34
+ return $result;
35
+ }
36
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/TranslationCollection.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Represents a collection of translated texts.
6
+ *
7
+ * Most web shops offer their own language handling, but to prevent redoing all
8
+ * the translations in the web shop specific way, a simple general way is
9
+ * defined.
10
+ */
11
+ class TranslationCollection
12
+ {
13
+ /**
14
+ * Returns a set of translations for the given language, completed with Dutch
15
+ * translations if no translation for the given language for some key was
16
+ * defined.
17
+ *
18
+ * @param string $language
19
+ *
20
+ * @return array
21
+ * A keyed array with translations.
22
+ */
23
+ public function get($language)
24
+ {
25
+ $result = array();
26
+ if (isset($this->nl)) {
27
+ $result = array_merge($result, $this->nl);
28
+ }
29
+ if (isset($this->{$language})) {
30
+ $result = array_merge($result, $this->{$language});
31
+ }
32
+ return $result;
33
+ }
34
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/Translator.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * Translator provides a simple way of translating texts.
6
+ */
7
+ class Translator implements TranslatorInterface
8
+ {
9
+ /** @var string */
10
+ protected $language;
11
+
12
+ /** @var array */
13
+ protected $translations;
14
+
15
+ /**
16
+ * @param string $language
17
+ * The 2 character language code.
18
+ */
19
+ public function __construct($language)
20
+ {
21
+ $this->language = $language;
22
+ $this->translations = array();
23
+ }
24
+
25
+ /**
26
+ * Instructs this translator to use the translations.
27
+ *
28
+ * @param \Siel\Acumulus\Helpers\TranslationCollection $translationCollection
29
+ * The translations to use
30
+ */
31
+ public function add(TranslationCollection $translationCollection)
32
+ {
33
+ $this->translations = array_merge($this->translations, $translationCollection->get($this->getLanguage()));
34
+ }
35
+
36
+ /**
37
+ * @inheritdoc
38
+ */
39
+ public function getLanguage()
40
+ {
41
+ return $this->language;
42
+ }
43
+
44
+ /**
45
+ * @inheritdoc
46
+ */
47
+ public function get($key)
48
+ {
49
+ return (isset($this->translations[$key]) ? $this->translations[$key] : $key);
50
+ }
51
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Helpers/TranslatorInterface.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Helpers;
3
+
4
+ /**
5
+ * TranslatorInterface defines an interface to retrieve language dependent texts.
6
+ * Most web shops offer their own language handling, but to prevent redoing all
7
+ * the translations in the web shop specific way, a simple generic way is
8
+ * defined here and in the base translator class.
9
+ */
10
+ interface TranslatorInterface
11
+ {
12
+ /**
13
+ * Returns the current (2 character) language (code).
14
+ *
15
+ * @return string
16
+ */
17
+ public function getLanguage();
18
+
19
+ /**
20
+ * Returns the string in the current language for the given key.
21
+ *
22
+ * @param string $key
23
+ * The key to look up.
24
+ *
25
+ * @return string
26
+ * Return in order of being available:
27
+ * - The string in the current language for the given key.
28
+ * - The string in dutch for the given key.
29
+ * - The key itself.
30
+ */
31
+ public function get($key);
32
+
33
+ /**
34
+ * Instructs this translator to use this collection of translations.
35
+ *
36
+ * @param \Siel\Acumulus\Helpers\TranslationCollection $translationCollection
37
+ * The translations to use.
38
+ */
39
+ public function add(TranslationCollection $translationCollection);
40
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Completor.php ADDED
@@ -0,0 +1,786 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\Countries;
5
+ use Siel\Acumulus\Helpers\Number;
6
+ use Siel\Acumulus\Helpers\TranslatorInterface;
7
+ use Siel\Acumulus\Web\Service;
8
+
9
+ /**
10
+ * The invoice completor class provides functionality to correct and complete
11
+ * invoices before sending them to Acumulus.
12
+ *
13
+ * This class:
14
+ * - Changes the customer into a fictitious client if set so in the config.
15
+ * - Validates the email address: the webservice does not allow an empty email
16
+ * address (but does allow a non provided email address).
17
+ * - Adds line totals. These are compared with what the shop advertises as order
18
+ * totals to see if an amount might be missing.
19
+ * - Adds the vat type based on inspection of the completed invoice.
20
+ * - Removes an empty shipping line if set to do so in the config.
21
+ *
22
+ * - Calls the invoice line completor.
23
+ * - Calls the strategy line completor.
24
+ *
25
+ * @package Siel\Acumulus
26
+ */
27
+ class Completor
28
+ {
29
+ const VatRateSource_Calculated_Corrected = 'calculated-corrected';
30
+ const VatRateSource_Completor_Completed = 'completor-completed';
31
+ const VatRateSource_Strategy_Completed = 'strategy-completed';
32
+
33
+ static $CorrectVatRateSources = array(
34
+ Creator::VatRateSource_Exact,
35
+ Creator::VatRateSource_Exact0,
36
+ self::VatRateSource_Calculated_Corrected,
37
+ self::VatRateSource_Completor_Completed,
38
+ );
39
+
40
+ /** @var \Siel\Acumulus\Invoice\ConfigInterface */
41
+ protected $config;
42
+
43
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
44
+ protected $translator;
45
+
46
+ /** @var \Siel\Acumulus\Web\Service */
47
+ protected $service;
48
+
49
+ /** @var \Siel\Acumulus\Helpers\Countries */
50
+ protected $countries;
51
+
52
+ /** @var array */
53
+ protected $messages;
54
+
55
+ /** @var array */
56
+ protected $invoice;
57
+
58
+ /** @var Source */
59
+ protected $source;
60
+
61
+ /**
62
+ * @var int[]
63
+ * The list of possible vat types, initially filled with possible vat types
64
+ * based on client country, invoiceHasLineWithVat(), is_company(), and the
65
+ * digital services setting. But then reduced by VAT rates we find on the
66
+ * order lines.
67
+ */
68
+ protected $possibleVatTypes;
69
+
70
+ /** @var array[] */
71
+ protected $possibleVatRates;
72
+
73
+ /** @var \Siel\Acumulus\Invoice\CompletorInvoiceLines */
74
+ protected $invoiceLineCompletor = null;
75
+
76
+ /** @var \Siel\Acumulus\Invoice\CompletorStrategyLines */
77
+ protected $strategyLineCompletor = null;
78
+
79
+ /** @var array */
80
+ protected $incompleteValues;
81
+
82
+ /**
83
+ * Constructor.
84
+ *
85
+ * @param \Siel\Acumulus\Invoice\ConfigInterface $config
86
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
87
+ * @param \Siel\Acumulus\Web\Service $service
88
+ */
89
+ public function __construct(ConfigInterface $config, TranslatorInterface $translator, Service $service)
90
+ {
91
+ $this->config = $config;
92
+
93
+ $this->translator = $translator;
94
+ $invoiceHelperTranslations = new Translations();
95
+ $this->translator->add($invoiceHelperTranslations);
96
+
97
+ $this->service = $service;
98
+ $this->countries = new Countries();
99
+
100
+ $this->invoiceLineCompletor = new CompletorInvoiceLines();
101
+ $this->strategyLineCompletor = new CompletorStrategyLines($translator);
102
+ }
103
+
104
+ /**
105
+ * Helper method to translate strings.
106
+ *
107
+ * @param string $key
108
+ * The key to get a translation for.
109
+ *
110
+ * @return string
111
+ * The translation for the given key or the key itself if no translation
112
+ * could be found.
113
+ */
114
+ protected function t($key)
115
+ {
116
+ return $this->translator->get($key);
117
+ }
118
+
119
+ /**
120
+ * Completes the invoice with default settings that do not depend on shop
121
+ * specific data.
122
+ *
123
+ * @param array $invoice
124
+ * The invoice to complete.
125
+ * @param Source $source
126
+ * The source object for which this invoice was created.
127
+ * @param array $messages
128
+ * A response structure where errors and warnings can be added. Any local
129
+ * messages will be added to arrays under the keys 'errors' and 'warnings'.
130
+ *
131
+ * @return array
132
+ * The completed invoice.
133
+ */
134
+ public function complete(array $invoice, Source $source, array &$messages)
135
+ {
136
+ $this->invoice = $invoice;
137
+ $this->source = $source;
138
+ $this->messages = &$messages;
139
+
140
+ $this->initPossibleVatTypes();
141
+ $this->initPossibleVatRates();
142
+
143
+ // Completes the invoice with default settings that do not depend on shop
144
+ // specific data.
145
+ $this->fictitiousClient();
146
+ $this->validateEmail();
147
+
148
+ // Complete lines as far as they can be completed om their own.
149
+ $this->invoice = $this->invoiceLineCompletor->complete($this->invoice, $this->possibleVatTypes, $this->possibleVatRates);
150
+
151
+ // Check if we are missing an amount and, if so, add a line for it.
152
+ $this->completeLineTotals();
153
+ $areTotalsEqual = $this->areTotalsEqual();
154
+ if (!$areTotalsEqual) {
155
+ $this->addMissingAmountLine($areTotalsEqual);
156
+ }
157
+
158
+ // Complete strategy lines: those lines that have to be completed based on
159
+ // the whole invoice.
160
+ $this->invoice = $this->strategyLineCompletor->complete($this->invoice, $this->source, $this->possibleVatTypes, $this->possibleVatRates);
161
+
162
+ // Fill in the VAT type, adding a warning if possible vat types are possible.
163
+ $this->completeVatType();
164
+
165
+ // Completes the invoice with settings or behaviour that might depend on the
166
+ // fact that the invoice lines have been completed.
167
+ $this->removeEmptyShipping();
168
+
169
+ return $this->invoice;
170
+ }
171
+
172
+ /**
173
+ * Initializes the list of possible vat types for this invoice.
174
+ *
175
+ * The list of possible vat types depends on:
176
+ * - whether there are lines with vat or if all lines appear vat free.
177
+ * - the country of the client.
178
+ * - optionally, the date of the invoice.
179
+ */
180
+ protected function initPossibleVatTypes()
181
+ {
182
+ $possibleVatTypes = array();
183
+ $shopSettings = $this->config->getShopSettings();
184
+ $digitalServices = $shopSettings['digitalServices'];
185
+ $vatFreeProducts = $shopSettings['vatFreeProducts'];
186
+
187
+ if (!empty($this->invoice['customer']['invoice']['vattype'])) {
188
+ // If shop specific code or an event handler has already set the vat type,
189
+ // we obey so.
190
+ $possibleVatTypes[] = $this->invoice['customer']['invoice']['vattype'];
191
+ } else {
192
+ if (!$this->invoiceHasLineWithVat()) {
193
+ // No VAT at all: National/EU reversed vat, only vat free products, or
194
+ // no vat (rest of world).
195
+ if ($this->isNl()) {
196
+ // Can it be VAT free products (e.g. education)? Digital services are
197
+ // never VAT free, nor are there any if set so.
198
+ if ($digitalServices !== ConfigInterface::DigitalServices_Only && $vatFreeProducts !== ConfigInterface::VatFreeProducts_No) {
199
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
200
+ }
201
+ // National reversed VAT: not really supported, but it is possible.
202
+ if ($this->isCompany()) {
203
+ $possibleVatTypes[] = ConfigInterface::VatType_NationalReversed;
204
+ }
205
+ } else if ($this->isEu()) {
206
+ // U reversed VAT.
207
+ if ($this->isCompany()) {
208
+ $possibleVatTypes[] = ConfigInterface::VatType_EuReversed;
209
+ }
210
+ // Can it be VAT free products (e.g. education)? Digital services are
211
+ // never VAT free, nor are there any if set so.
212
+ if ($digitalServices !== ConfigInterface::DigitalServices_Only && $vatFreeProducts !== ConfigInterface::VatFreeProducts_No) {
213
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
214
+ }
215
+ } else if ($this->isOutsideEu()) {
216
+ $possibleVatTypes[] = ConfigInterface::VatType_RestOfWorld;
217
+ }
218
+
219
+ if (empty($possibleVatTypes)) {
220
+ // Warning + fall back.
221
+ $this->messages['warnings'][] = array(
222
+ 'code' => '',
223
+ 'codetag' => '',
224
+ 'message' => $this->t('message_warning_no_vat'),
225
+ );
226
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
227
+ $possibleVatTypes[] = ConfigInterface::VatType_EuReversed;
228
+ $possibleVatTypes[] = ConfigInterface::VatType_NationalReversed;
229
+ $this->invoice['customer']['invoice']['concept'] = ConfigInterface::Concept_Yes;
230
+ }
231
+ } else {
232
+ // NL or EU Foreign vat.
233
+ if ($digitalServices === ConfigInterface::DigitalServices_No) {
234
+ // No electronic services are sold: can only be dutch VAT.
235
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
236
+ } else {
237
+ if ($this->isEu() && $this->getInvoiceDate() >= '2015-01-01') {
238
+ if ($digitalServices !== ConfigInterface::DigitalServices_Only) {
239
+ // Also normal goods are sold, so dutch VAT still possible.
240
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
241
+ }
242
+ // As of 2015, electronic services should be taxed with the rates of
243
+ // the clients' country. And they might be sold in the shop.
244
+ $possibleVatTypes[] = ConfigInterface::VatType_ForeignVat;
245
+ } else {
246
+ // Not EU or before 2015-01-01: special regulations for electronic
247
+ // services were not yet active: dutch VAT only.
248
+ $possibleVatTypes[] = ConfigInterface::VatType_National;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ $this->possibleVatTypes = $possibleVatTypes;
254
+ }
255
+
256
+ /**
257
+ * Initializes the list of possible vat rates.
258
+ *
259
+ * The possible vat rats depend on:
260
+ * - the possible vat types.
261
+ * - optionally, the date of the invoice.
262
+ * - optionally, the country of the client.
263
+ *
264
+ * @return array
265
+ * Array with possible vat rates. a vat rate being an array with keys
266
+ * vatrate and vattype. This to be able to retrieve to which vat type a vat
267
+ * rate belongs and to allow for the same vat rate to be valid for multiple
268
+ * vat types.
269
+ */
270
+ protected function initPossibleVatRates()
271
+ {
272
+ $possibleVatRates = array();
273
+ foreach ($this->possibleVatTypes as $vatType) {
274
+ switch ($vatType) {
275
+ case ConfigInterface::VatType_National:
276
+ case ConfigInterface::VatType_MarginScheme:
277
+ default:
278
+ $vatTypeVatRates = $this->getVatRates('nl');
279
+ break;
280
+ case ConfigInterface::VatType_NationalReversed:
281
+ case ConfigInterface::VatType_EuReversed:
282
+ $vatTypeVatRates = array(0);
283
+ break;
284
+ case ConfigInterface::VatType_RestOfWorld:
285
+ $vatTypeVatRates = array(-1);
286
+ break;
287
+ case ConfigInterface::VatType_ForeignVat:
288
+ $vatTypeVatRates = $this->getVatRates($this->invoice['customer']['countrycode']);
289
+ break;
290
+ }
291
+ $vatTypeVatRates = array_map(function ($vatRate) use ($vatType) {
292
+ return array('vatrate' => $vatRate, 'vattype' => $vatType);
293
+ }, $vatTypeVatRates);
294
+ $possibleVatRates = array_merge($possibleVatRates, $vatTypeVatRates);
295
+ }
296
+ $this->possibleVatRates = $possibleVatRates;
297
+ }
298
+
299
+ /**
300
+ * Anonymize customer if set so. We don't do this for business clients, only
301
+ * consumers.
302
+ */
303
+ protected function fictitiousClient()
304
+ {
305
+ $customerSettings = $this->config->getCustomerSettings();
306
+ if (!$customerSettings['sendCustomer'] && empty($this->invoice['customer']['companyname1']) && empty($this->invoice['customer']['vatnumber'])) {
307
+ unset($this->invoice['customer']['type']);
308
+ unset($this->invoice['customer']['companyname1']);
309
+ unset($this->invoice['customer']['companyname2']);
310
+ unset($this->invoice['customer']['fullname']);
311
+ unset($this->invoice['customer']['salutation']);
312
+ unset($this->invoice['customer']['address1']);
313
+ unset($this->invoice['customer']['address2']);
314
+ unset($this->invoice['customer']['postalcode']);
315
+ unset($this->invoice['customer']['city']);
316
+ unset($this->invoice['customer']['countrycode']);
317
+ unset($this->invoice['customer']['vatnumber']);
318
+ unset($this->invoice['customer']['telephone']);
319
+ unset($this->invoice['customer']['fax']);
320
+ unset($this->invoice['customer']['bankaccountnumber']);
321
+ unset($this->invoice['customer']['mark']);
322
+ $this->invoice['customer']['email'] = $customerSettings['genericCustomerEmail'];
323
+ $this->invoice['customer']['overwriteifexists'] = 0;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Validates the email address of the invoice.
329
+ *
330
+ * - The email address may not be empty but may be left out though.
331
+ * - Multiple, comma separated, email addresses are not allowed.
332
+ * - Display names (My Name <my.name@example.com>) are not allowed.
333
+ */
334
+ protected function validateEmail()
335
+ {
336
+ // Check email address.
337
+ if (empty($this->invoice['customer']['email'])) {
338
+ unset($this->invoice['customer']['email']);
339
+ } else {
340
+ $email = $this->invoice['customer']['email'];
341
+ $at = strpos($email, '@');
342
+ // Comma (,) used as separator?
343
+ $comma = strpos($email, ',', $at);
344
+ if ($at < $comma) {
345
+ $email = trim(substr($email, 0, $comma));
346
+ }
347
+ // Semicolon (;) used as separator?
348
+ $semicolon = strpos($email, ';', $at);
349
+ if ($at < $semicolon) {
350
+ $email = trim(substr($email, 0, $semicolon));
351
+ }
352
+
353
+ // Display name used in single remaining address?
354
+ if (preg_match('/^(.+?)<([^>]+)>$/', $email, $matches)) {
355
+ $email = trim($matches[2]);
356
+ }
357
+ $this->invoice['customer']['email'] = $email;
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Calculates the total amount and vat amount for the invoice lines and adds
363
+ * these to the fields meta-lines-amount and meta-lines-vatamount.
364
+ */
365
+ protected function completeLineTotals()
366
+ {
367
+ $linesAmount = 0.0;
368
+ $linesAmountInc = 0.0;
369
+ $linesVatAmount = 0.0;
370
+ $this->incompleteValues = array();
371
+
372
+ $invoiceLines = $this->invoice['customer']['invoice']['line'];
373
+ foreach ($invoiceLines as $line) {
374
+ if (isset($line['meta-line-price'])) {
375
+ $linesAmount += $line['meta-line-price'];
376
+ } else if (isset($line['unitprice'])) {
377
+ $linesAmount += $line['quantity'] * $line['unitprice'];
378
+ } else {
379
+ $this->incompleteValues['meta-lines-amount'] = 'meta-lines-amount';
380
+ }
381
+
382
+ if (isset($line['meta-line-priceinc'])) {
383
+ $linesAmountInc += $line['meta-line-priceinc'];
384
+ } else if (isset($line['unitpriceinc'])) {
385
+ $linesAmountInc += $line['quantity'] * $line['unitpriceinc'];
386
+ } else {
387
+ $this->incompleteValues['meta-lines-amountinc'] = 'meta-lines-amountinc';
388
+ }
389
+
390
+ if (isset($line['meta-line-vatamount'])) {
391
+ $linesVatAmount += $line['meta-line-vatamount'];
392
+ } else if (isset($line['vatamount'])) {
393
+ $linesVatAmount += $line['quantity'] * $line['vatamount'];
394
+ } else {
395
+ $this->incompleteValues['meta-lines-vatamount'] = 'meta-lines-vatamount';
396
+ }
397
+ }
398
+
399
+ $this->invoice['customer']['invoice']['meta-lines-amount'] = $linesAmount;
400
+ $this->invoice['customer']['invoice']['meta-lines-amountinc'] = $linesAmountInc;
401
+ $this->invoice['customer']['invoice']['meta-lines-vatamount'] = $linesVatAmount;
402
+ if (!empty($this->incompleteValues)) {
403
+ sort($this->incompleteValues);
404
+ $this->invoice['customer']['invoice']['meta-lines-incomplete'] = implode(',', $this->incompleteValues);
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Compares the invoice totals metadata with the line totals metadata.
410
+ *
411
+ * If any of the 3 values are equal we do consider the totals to be equal
412
+ * (except for a 0 VAT amount (for reversed VAT invoices)). This because in
413
+ * many cases 1 or 2 of the 3 values are either incomplete or incorrect.
414
+ *
415
+ * @todo: if 1 is correct but other not, that would be an indication of an error: warn?
416
+ * @return bool|null
417
+ * True if the totals are equal, false if not equal, null if undecided (all
418
+ * 3 values are incomplete).
419
+ */
420
+ protected function areTotalsEqual()
421
+ {
422
+ $invoice = $this->invoice['customer']['invoice'];
423
+ if (!in_array('meta-lines-amount', $this->incompleteValues) && Number::floatsAreEqual($invoice['meta-invoice-amount'], $invoice['meta-lines-amount'], 0.05)) {
424
+ return true;
425
+ }
426
+ if (!in_array('meta-lines-amountinc', $this->incompleteValues) && Number::floatsAreEqual($invoice['meta-invoice-amountinc'], $invoice['meta-lines-amountinc'], 0.05)) {
427
+ return true;
428
+ }
429
+ if (!in_array('meta-lines-vatamount', $this->incompleteValues)
430
+ && Number::floatsAreEqual($invoice['meta-invoice-vatamount'], $invoice['meta-lines-vatamount'], 0.05)
431
+ && !Number::isZero($invoice['meta-invoice-vatamount'])
432
+ ) {
433
+ return true;
434
+ }
435
+ return count($this->incompleteValues) === 3 ? null : false;
436
+ }
437
+
438
+ /**
439
+ * Adds an invoice line if the total amount (meta-invoice-amount) is not
440
+ * matching the total amount of the lines.
441
+ *
442
+ * This can happen if:
443
+ * - we missed a fee that is stored in custom fields
444
+ * - a manual adjustment
445
+ * - an error in the logic or data as provided by the webshop.
446
+ *
447
+ * However, we can only add this line if we have at least 2 complete values,
448
+ * that is, there are no strategy lines,
449
+ *
450
+ * @param bool|null $areTotalsEqualResult
451
+ * Result of areTotalsEqual() (false or null)
452
+ */
453
+ protected function addMissingAmountLine($areTotalsEqualResult)
454
+ {
455
+ $invoice = &$this->invoice['customer']['invoice'];
456
+ if (!in_array('meta-lines-amount', $this->incompleteValues)) {
457
+ $missingAmount = $invoice['meta-invoice-amount'] - $invoice['meta-lines-amount'];
458
+ }
459
+ if (!in_array('meta-lines-amountinc', $this->incompleteValues)) {
460
+ $missingAmountInc = $invoice['meta-invoice-amountinc'] - $invoice['meta-lines-amountinc'];
461
+ }
462
+ if (!in_array('meta-lines-vatamount', $this->incompleteValues)) {
463
+ $missingVatAmount = $invoice['meta-invoice-vatamount'] - $invoice['meta-lines-vatamount'];
464
+ }
465
+
466
+ if (count($this->incompleteValues) <= 1) {
467
+ if (!isset($missingAmount)) {
468
+ /** @noinspection PhpUndefinedVariableInspection */
469
+ $missingAmount = $missingAmountInc - $missingVatAmount;
470
+ }
471
+ if (!isset($missingVatAmount)) {
472
+ /** @noinspection PhpUndefinedVariableInspection */
473
+ $missingVatAmount = $missingAmountInc - $missingAmount;
474
+ }
475
+
476
+ $settings = $this->config->getInvoiceSettings();
477
+ if ($settings['addMissingAmountLine']) {
478
+ if ($this->source->getType() === Source::CreditNote) {
479
+ $product = $this->t('refund_adjustment');
480
+ } else if ($missingAmount < 0.0) {
481
+ $product = $this->t('discount_adjustment');
482
+ } else {
483
+ $product = $this->t('fee_adjustment');
484
+ }
485
+ $countLines = count($invoice['line']);
486
+ $line = array(
487
+ 'product' => $product,
488
+ 'quantity' => 1,
489
+ 'unitprice' => $missingAmount,
490
+ 'vatamount' => $missingVatAmount,
491
+ ) + Creator::getVatRangeTags($missingVatAmount, $missingAmount, $countLines * 0.02, $countLines * 0.02)
492
+ + array(
493
+ 'meta-line-type' => Creator::LineType_Corrector,
494
+ );
495
+ // Correct and add this line.
496
+ if ($line['meta-vatrate-source'] === Creator::VatRateSource_Calculated) {
497
+ $line = $this->invoiceLineCompletor->correctVatRateByRange($line);
498
+ }
499
+ $invoice['line'][] = $line;
500
+ } else {
501
+ // Add some diagnostic info to the message sent.
502
+ // @todo: this could/should be turned into a warning (after some testing).
503
+ /** @noinspection PhpUndefinedVariableInspection */
504
+ $invoice['meta-missing-amount'] = "Ex: $missingAmount, Inc: $missingAmountInc, VAT: $missingVatAmount";
505
+ }
506
+ } else {
507
+ if ($areTotalsEqualResult === false) {
508
+ // Due to lack of information, we cannot add a missing line, even though
509
+ // we know we are missing something ($areTotalsEqualResult is false, not
510
+ // null). Add some diagnostic info to the message sent.
511
+ // @todo: this could/should be turned into a warning (after some testing).
512
+ $invoice['meta-missing-amount'] = array();
513
+ if (isset($missingAmount)) {
514
+ $invoice['meta-missing-amount'][] = "Ex: $missingAmount";
515
+ }
516
+ if (isset($missingAmountInc)) {
517
+ $invoice['meta-missing-amount'][] = "Inc: $missingAmountInc";
518
+ }
519
+ if (isset($missingVatAmount)) {
520
+ $invoice['meta-missing-amount'][] = "VAT: $missingVatAmount";
521
+ }
522
+ $invoice['meta-missing-amount'] = implode(', ', $invoice['meta-missing-amount']);
523
+ }
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Fills the vattype field of the invoice.
529
+ *
530
+ * This method (and class) is aware of:
531
+ * - The setting digitalServices.
532
+ * - The country of the client.
533
+ * - Whether the client is a company.
534
+ * - The actual VAT rates on the day of the order.
535
+ *
536
+ * So to start with, any list of (possible) vat types is based on the above.
537
+ * Furthermore this method is aware of:
538
+ * - The fact that orders do not have to be split over different vat types,
539
+ * but that invoices should be split if both national and foreign VAT rates
540
+ * appear on the order.
541
+ * - The fact that the vat type may be indeterminable if EU countries have VAT
542
+ * rates in common with NL and the settings indicate that this shop sells
543
+ * products in both vat type categories.
544
+ *
545
+ * If multiple vat types are possible, the invoice is sent as concept, so that
546
+ * it may be edited in Acumulus.
547
+ */
548
+ protected function completeVatType()
549
+ {
550
+ // If shop specific code or an event handler has already set the vat type,
551
+ // we don't change it.
552
+ if (empty($this->invoice['customer']['invoice']['vattype'])) {
553
+
554
+ $possibleVatTypes = $this->getPossibleVatTypesByCorrectVatRates();
555
+ if (empty($possibleVatTypes)) {
556
+ $this->messages['warnings'][] = array(
557
+ 'code' => '',
558
+ 'codetag' => '',
559
+ 'message' => $this->t('message_warning_no_vattype'),
560
+ );
561
+ $this->invoice['customer']['invoice']['vattype'] = reset($this->possibleVatTypes);
562
+ $this->invoice['customer']['invoice']['meta-vattypes-possible'] = implode(',', $this->possibleVatTypes);
563
+ $this->invoice['customer']['invoice']['concept'] = ConfigInterface::Concept_Yes;
564
+ } else if (count($possibleVatTypes) === 1) {
565
+ // Pick the first and only vat type.
566
+ $this->invoice['customer']['invoice']['vattype'] = reset($possibleVatTypes);
567
+ } else {
568
+ // Get the intersection of possible vat types per line.
569
+ $vatTypesOnAllLines = $this->getVatTypesAppearingOnAllLines();
570
+ if (empty($vatTypesOnAllLines)) {
571
+ // We must split.
572
+ $message = 'message_warning_multiple_vattype_must_split';
573
+ // Pick the first and hopefully correct vat type, but ...
574
+ $this->invoice['customer']['invoice']['vattype'] = reset($possibleVatTypes);
575
+ } else {
576
+ // We may have to split.
577
+ $message = 'message_warning_multiple_vattype_may_split';
578
+ // Pick the first vat type that appears on all lines, but ...
579
+ $this->invoice['customer']['invoice']['vattype'] = reset($vatTypesOnAllLines);
580
+ }
581
+
582
+ // But add a message and meta info.
583
+ $this->messages['warnings'][] = array(
584
+ 'code' => '',
585
+ 'codetag' => '',
586
+ 'message' => $this->t($message),
587
+ );
588
+ $this->invoice['customer']['invoice']['concept'] = ConfigInterface::Concept_Yes;
589
+ $this->invoice['customer']['invoice']['meta-vattypes-possible'] = implode(',', $possibleVatTypes);
590
+ }
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Returns a list of possible vat types based on possible vat types for all
596
+ * lines with a "correct" vat rate.
597
+ *
598
+ * If that results in 1 vat type, that will be the vat type for the invoice,
599
+ * otherwise a warning will be issued.
600
+ *
601
+ * This method may return multiple vat types because:
602
+ * - If vat types share equal vat rates we cannot make a choice (e.g. NL and
603
+ * BE high VAT rates are equal).
604
+ * - If the invoice ought to be split into multiple invoices because multiple
605
+ * vat regimes apply (digital services and normal goods) (e.g. both the FR
606
+ * 20% high rate and the NL 21% high rate appear on the invoice).
607
+ *
608
+ * @return int[]
609
+ * List of possible vat type for this invoice (keyed by the vat types).
610
+ */
611
+ protected function getPossibleVatTypesByCorrectVatRates()
612
+ {
613
+ // We only want to process correct vat rates.
614
+ // Define vat types that do know a zero rate.
615
+ $zeroRateVatTypes = array(
616
+ ConfigInterface::VatType_National,
617
+ ConfigInterface::VatType_NationalReversed,
618
+ ConfigInterface::VatType_EuReversed,
619
+ ConfigInterface::VatType_RestOfWorld,
620
+ );
621
+
622
+ // We keep track of vat types found per appearing vat rate.
623
+ // The intersection of these sets should result in the new, hopefully
624
+ // smaller list, of possible vat types.
625
+ $invoiceVatTypes = array();
626
+ foreach ($this->invoice['customer']['invoice']['line'] as &$line) {
627
+ if (in_array($line['meta-vatrate-source'], static::$CorrectVatRateSources)) {
628
+ // We ignore "0" vat rates (0 and -1).
629
+ if ($line['vatrate'] > 0) {
630
+ $lineVatTypes = array();
631
+ foreach ($this->possibleVatRates as $vatRateInfo) {
632
+ if ($vatRateInfo['vatrate'] == $line['vatrate']) {
633
+ // Ensure that the values remain unique by keying them.
634
+ $invoiceVatTypes[$vatRateInfo['vattype']] = $vatRateInfo['vattype'];
635
+ $lineVatTypes[$vatRateInfo['vattype']] = $vatRateInfo['vattype'];
636
+ }
637
+ }
638
+ } else {
639
+ // Reduce the possible vat types to those that do know a zero rate.
640
+ $lineVatTypes = array_intersect($this->possibleVatTypes, $zeroRateVatTypes);
641
+ foreach ($lineVatTypes AS $lineVatType) {
642
+ $invoiceVatTypes[$lineVatType] = $lineVatType;
643
+ }
644
+ }
645
+ $line['meta-vattypes-possible'] = implode(',', $lineVatTypes);
646
+ }
647
+ }
648
+
649
+ return $invoiceVatTypes;
650
+ }
651
+
652
+
653
+ /**
654
+ * Returns a list of vat types that are possible for all lines of the invoice.
655
+ *
656
+ * @return int[]
657
+ */
658
+ protected function getVatTypesAppearingOnAllLines()
659
+ {
660
+ $result = null;
661
+ foreach ($this->invoice['customer']['invoice']['line'] as $line) {
662
+ $lineVatTypes = explode(',', $line['meta-vattypes-possible']);
663
+ if ($result === null) {
664
+ // 1st line.
665
+ $result = $lineVatTypes;
666
+ } else {
667
+ $result = array_intersect($result, $lineVatTypes);
668
+ }
669
+ }
670
+ return $result;
671
+ }
672
+
673
+ /**
674
+ * Removes an empty shipping line (if so configured).
675
+ */
676
+ protected function removeEmptyShipping()
677
+ {
678
+ $invoiceSettings = $this->config->getInvoiceSettings();
679
+ if ($invoiceSettings['removeEmptyShipping']) {
680
+ $this->invoice['customer']['invoice']['line'] = array_filter($this->invoice['customer']['invoice']['line'],
681
+ function ($line) {
682
+ return $line['meta-line-type'] !== Creator::LineType_Shipping || !Number::isZero($line['unitprice']);
683
+ });
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Returns whether the invoice has at least 1 line with a non-0 vat rate.
689
+ *
690
+ * 0 (VAT free/reversed VAT) and -1 (no VAT) are valid 0-vat rates.
691
+ * As vatrate may be null, the vatamount value is also checked.
692
+ *
693
+ * @return bool
694
+ */
695
+ protected function invoiceHasLineWithVat()
696
+ {
697
+ $isLineWithVat = false;
698
+ foreach ($this->invoice['customer']['invoice']['line'] as $line) {
699
+ if (!empty($line['vatrate'])) {
700
+ if (!Number::isZero($line['vatrate']) && !Number::floatsAreEqual($line['vatrate'], -1.0)) {
701
+ $isLineWithVat = true;
702
+ break;
703
+ }
704
+ } else if (!empty($line['vatamount']) && !Number::isZero($line['vatamount'])) {
705
+ $isLineWithVat = true;
706
+ break;
707
+ }
708
+ }
709
+ return $isLineWithVat;
710
+ }
711
+
712
+ /**
713
+ * Helper method to get vat info for the current invoice from the Acumulus API.
714
+ *
715
+ * The vat rates as they were at the invoice date are retrieved.
716
+ *
717
+ * @param string $countryCode
718
+ * The country to fetch the vat rates for.
719
+ *
720
+ * @return float[]
721
+ * Actual type will be string[].
722
+ *
723
+ * @see \Siel\Acumulus\Web\Service::getVatInfo().
724
+ */
725
+ protected function getVatRates($countryCode)
726
+ {
727
+ $date = $this->getInvoiceDate();
728
+ $vatInfo = $this->service->getVatInfo($countryCode, $date);
729
+ // PHP5.5: array_column($vatInfo['vatinfo'], 'vatrate');
730
+ $result = array_unique(array_map(function ($vatInfo1) {
731
+ return $vatInfo1['vatrate'];
732
+ }, $vatInfo['vatinfo']));
733
+ return $result;
734
+ }
735
+
736
+ /**
737
+ * Returns the invoice date in the iso yyyy-mm-dd format.
738
+ *
739
+ * @return string
740
+ */
741
+ protected function getInvoiceDate()
742
+ {
743
+ $date = !empty($this->invoice['customer']['invoice']['issuedate']) ? $this->invoice['customer']['invoice']['issuedate'] : date('Y-m-d');
744
+ return $date;
745
+ }
746
+
747
+ /**
748
+ * Wrapper around Countries::isNl().
749
+ *
750
+ * @return bool
751
+ */
752
+ protected function isNl()
753
+ {
754
+ return $this->countries->isNl($this->invoice['customer']['countrycode']);
755
+ }
756
+
757
+ /**
758
+ * Wrapper around Countries::isEu().
759
+ *
760
+ * @return bool
761
+ */
762
+ protected function isEu()
763
+ {
764
+ return $this->countries->isEu($this->invoice['customer']['countrycode']);
765
+ }
766
+
767
+ /**
768
+ * Returns whether the client is located outside the EU.
769
+ *
770
+ * @return bool
771
+ */
772
+ protected function isOutsideEu()
773
+ {
774
+ return !$this->isNl() && !$this->isEu();
775
+ }
776
+
777
+ /**
778
+ * Returns whether the client is a company with a vat number.
779
+ *
780
+ * @return bool
781
+ */
782
+ protected function isCompany()
783
+ {
784
+ return !empty($this->invoice['customer']['vatnumber']);
785
+ }
786
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorInvoiceLines.php ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+
6
+ /**
7
+ * The invoice lines completor class provides functionality to correct and
8
+ * complete invoice lines before sending them to Acumulus.
9
+ *
10
+ * This class:
11
+ * - Adds required but missing fields on the invoice lines.
12
+ * - Validates (and correct rounding errors of) vat rates using the VAT rate
13
+ * lookup webservice call.
14
+ * - Adds vat rates to 0 price lines (with a 0 price and thus 0 vat, not all
15
+ * web shops can fill in a vat rate).
16
+ *
17
+ * @package Siel\Acumulus
18
+ */
19
+ class CompletorInvoiceLines
20
+ {
21
+ /** @var array[] */
22
+ protected $invoice;
23
+
24
+ /** @var array[] */
25
+ protected $invoiceLines;
26
+
27
+ /**
28
+ * @var int[]
29
+ * The list of possible vat types, initially filled with possible vat types
30
+ * based on client country, invoiceHasLineWithVat(), is_company(), and the
31
+ * digital services setting. But then reduced by VAT rates we find on the
32
+ * order lines.
33
+ */
34
+ protected $possibleVatTypes;
35
+
36
+ /** @var array[] */
37
+ protected $possibleVatRates;
38
+
39
+ /**
40
+ * Constructor.
41
+ */
42
+ public function __construct()
43
+ {
44
+ }
45
+
46
+ /**
47
+ * Completes the invoice with default settings that do not depend on shop
48
+ * specific data.
49
+ *
50
+ * @param array $invoice
51
+ * The invoice to complete.
52
+ * @param int[] $possibleVatTypes
53
+ * @param array[] $possibleVatRates
54
+ * A response structure where errors and warnings can be added. Any local
55
+ * messages will be added to arrays under the keys 'errors' and 'warnings'.
56
+ *
57
+ * @return array The completed invoice.
58
+ * The completed invoice.
59
+ */
60
+ public function complete(array $invoice, array $possibleVatTypes, array $possibleVatRates)
61
+ {
62
+ $this->invoice = $invoice;
63
+ $this->invoiceLines = &$this->invoice['customer']['invoice']['line'];
64
+ $this->possibleVatTypes = $possibleVatTypes;
65
+ $this->possibleVatRates = $possibleVatRates;
66
+
67
+ $this->completeInvoiceLines();
68
+
69
+ return $this->invoice;
70
+ }
71
+
72
+ /**
73
+ * Completes the invoice lines.
74
+ */
75
+ protected function completeInvoiceLines()
76
+ {
77
+ $this->completeLineRequiredData();
78
+ $this->correctCalculatedVatRates();
79
+ $this->addVatRateTo0PriceLines();
80
+ $this->completeLineMetaData();
81
+ }
82
+
83
+ /**
84
+ * Completes the fields that are required by the rest of this completor phase.
85
+ *
86
+ * The creator filled in the fields that are directly available from the
87
+ * shops' data store. This method completes (if not filled in):
88
+ * - unitprice.
89
+ */
90
+ protected function completeLineRequiredData()
91
+ {
92
+ $invoiceLines = &$this->invoice['customer']['invoice']['line'];
93
+ foreach ($invoiceLines as &$line) {
94
+ $calculatedFields = isset($line['meta-calculated-fields'])
95
+ ? (is_array($line['meta-calculated-fields']) ? $line['meta-calculated-fields'] : explode(',', $line['meta-calculated-fields']))
96
+ : array();
97
+
98
+ if (!isset($line['unitprice'])) {
99
+ if (isset($line['unitpriceinc'])) {
100
+ if (isset($line['vatamount'])) {
101
+ $line['unitprice'] = $line['unitpriceinc'] - $line['vatamount'];
102
+ $calculatedFields[] = 'unitprice';
103
+ } else if (isset($line['vatrate']) && in_array($line['meta-vatrate-source'], Completor::$CorrectVatRateSources)) {
104
+ $line['unitprice'] = $line['unitpriceinc'] / (100.0 + $line['vatrate']) * 100.0;
105
+ $calculatedFields[] = 'unitprice';
106
+ }
107
+ }
108
+ }
109
+
110
+ if (!empty($calculatedFields)) {
111
+ $line['meta-calculated-fields'] = implode(',', $calculatedFields);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Try to correct 'calculated' vat rates for rounding errors by matching them
118
+ * with possible vatRates
119
+ */
120
+ protected function correctCalculatedVatRates()
121
+ {
122
+ foreach ($this->invoiceLines as &$line) {
123
+ if (!empty($line['meta-vatrate-source']) && $line['meta-vatrate-source'] === Creator::VatRateSource_Calculated) {
124
+ $line = $this->correctVatRateByRange($line);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Checks and corrects a 'calculated' vat rate to an allowed vat rate.
131
+ *
132
+ * The meta-vatrate-source must be Creator::VatRateSource_Calculated.
133
+ *
134
+ * The check is done on comparing allowed vat rates with the meta-vatrate-min
135
+ * and meta-vatrate-max values. If only 1 match is found that will be used.
136
+ *
137
+ * If multiple matches are found with all equal rates - e.g. Dutch and Belgium
138
+ * 21% - the vat rate will be corrected, but the VAT Type will remain
139
+ * undecided.
140
+ *
141
+ * This method is public to allow a 2nd call to just this method for a single
142
+ * line added after a 1st round of correcting. Do not use unless
143
+ * $this->possibleVAtRates has been initialized
144
+ *
145
+ * @param array $line
146
+ * A line with a calculated vat rate.
147
+ *
148
+ * @return array
149
+ * The line with a corrected vat rate.
150
+ */
151
+ public function correctVatRateByRange(array $line)
152
+ {
153
+ $matchedVatRates = array();
154
+ foreach ($this->possibleVatRates as $vatRate) {
155
+ if ($vatRate['vatrate'] >= $line['meta-vatrate-min'] && $vatRate['vatrate'] <= $line['meta-vatrate-max']) {
156
+ $matchedVatRates[] = $vatRate;
157
+ }
158
+ }
159
+
160
+ $vatRate = $this->getUniqueVatRate($matchedVatRates);
161
+ if ($vatRate === null || $vatRate === false) {
162
+ // We remove the calculated vatrate
163
+ // @todo: pick closest or pick just 1?
164
+ unset($line['vatrate']);
165
+ $line['meta-vatrate-matches'] = $vatRate === null
166
+ ? 'none'
167
+ : array_reduce($matchedVatRates, function ($carry, $item) {
168
+ return $carry . ($carry === '' ? '' : ',') . $item['vatrate'] . '(' . $item['vattype'] . ')';
169
+ }, '');
170
+ if (!empty($line['meta-strategy-split'])) {
171
+ // Give the strategy phase a chance to correct this line.
172
+ $line['meta-vatrate-source'] = Creator::VatRateSource_Strategy;
173
+ }
174
+ } else {
175
+ $line['vatrate'] = $vatRate;
176
+ $line['meta-vatrate-source'] = Completor::VatRateSource_Calculated_Corrected;
177
+ }
178
+ return $line;
179
+ }
180
+
181
+ /**
182
+ * Determines if all (matched) vat rates are equal.
183
+ *
184
+ * @param array $matchedVatRates
185
+ *
186
+ * @return float|FALSE|NULL
187
+ * If all vat rates are equal that vat rate, null if $matchedVatRates is
188
+ * empty, false otherwise (multiple but different vat rates).
189
+ */
190
+ protected function getUniqueVatRate(array $matchedVatRates)
191
+ {
192
+ $result = array_reduce($matchedVatRates, function ($carry, $matchedVatRate) {
193
+ if ($carry === null) {
194
+ // 1st item: return its vat rate.
195
+ return $matchedVatRate['vatrate'];
196
+ } else if ($carry == $matchedVatRate['vatrate']) {
197
+ // Note that in PHP: '21' == '21.0000' returns true. So using == works.
198
+ // Vat rate equals all previous vat rates: return that vat rate.
199
+ return $carry;
200
+ } else {
201
+ // Vat rate does not match previous vat rates or carry is already false,
202
+ // return false.
203
+ return false;
204
+ }
205
+ }, null);
206
+ return $result;
207
+ }
208
+
209
+ /**
210
+ * Completes lines with free items (price = 0) by giving them the maximum tax
211
+ * rate that appears in the other lines.
212
+ */
213
+ protected function addVatRateTo0PriceLines()
214
+ {
215
+ // Get appearing vat rates and their frequency.
216
+ $vatRates = $this->getAppearingVatRates();
217
+
218
+ // Get the highest vat rate.
219
+ $maxVatRate = -1.0;
220
+ foreach ($vatRates as $vatRate => $frequency) {
221
+ if ((float) $vatRate > $maxVatRate) {
222
+ $maxVatRate = (float) $vatRate;
223
+ }
224
+ }
225
+
226
+ foreach ($this->invoiceLines as &$line) {
227
+ if ($line['meta-vatrate-source'] === Creator::VatRateSource_Completor && $line['vatrate'] === null && Number::isZero($line['unitprice'])) {
228
+ $line['vatrate'] = $maxVatRate;
229
+ $line['meta-vatrate-source'] = Completor::VatRateSource_Completor_Completed;
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Returns a list of vat rates that actually appear in the invoice.
236
+ *
237
+ * @return array
238
+ * An array with the vat rates as key and the number of times they appear in
239
+ * the invoice lines as value.
240
+ */
241
+ protected function getAppearingVatRates()
242
+ {
243
+ $vatRates = array();
244
+ foreach ($this->invoiceLines as $line) {
245
+ if (isset($line['vatrate'])) {
246
+ if (isset($vatRates[$line['vatrate']])) {
247
+ $vatRates[$line['vatrate']]++;
248
+ } else {
249
+ $vatRates[$line['vatrate']] = 1;
250
+ }
251
+ }
252
+ }
253
+ return $vatRates;
254
+ }
255
+
256
+ /**
257
+ * Completes each (non-strategy) line with missing (meta) info.
258
+ *
259
+ * All non strategy lines have unitprice and vatrate filled in and should by
260
+ * now have correct(ed) VAT rates. In some shops these non strategy lines may
261
+ * have a meta-line-discount-vatamount or meta-line-discount-amountinc field,
262
+ * that can be used with the SplitKnownDiscountLine strategy. Complete (if
263
+ * missing):
264
+ * - unitpriceinc
265
+ * - vatamount
266
+ * - meta-line-discount-amountinc (if meta-line-discount-vatamount is
267
+ * available).
268
+ */
269
+ protected function completeLineMetaData()
270
+ {
271
+ $invoiceLines = &$this->invoice['customer']['invoice']['line'];
272
+ foreach ($invoiceLines as &$line) {
273
+ $calculatedFields = isset($line['meta-calculated-fields'])
274
+ ? (is_array($line['meta-calculated-fields']) ? $line['meta-calculated-fields'] : explode(',', $line['meta-calculated-fields']))
275
+ : array();
276
+
277
+ if (in_array($line['meta-vatrate-source'], Completor::$CorrectVatRateSources)) {
278
+ if (!isset($line['unitpriceinc'])) {
279
+ $line['unitpriceinc'] = $line['unitprice'] / 100.0 * (100.0 + $line['vatrate']);
280
+ $calculatedFields[] = 'unitpriceinc';
281
+ }
282
+
283
+ if (!isset($line['vatamount'])) {
284
+ $line['vatamount'] = $line['vatrate'] / 100.0 * $line['unitprice'];
285
+ $calculatedFields[] = 'vatamount';
286
+ }
287
+
288
+ if (isset($line['meta-line-discount-vatamount']) && !isset($line['meta-line-discount-amountinc'])) {
289
+ $line['meta-line-discount-amountinc'] = $line['meta-line-discount-vatamount'] / $line['vatrate'] * (100 + $line['vatrate']);
290
+ $calculatedFields[] = 'meta-line-discount-amountinc';
291
+ }
292
+
293
+ if (!empty($calculatedFields)) {
294
+ $line['meta-calculated-fields'] = implode(',', $calculatedFields);
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/ApplySameVatRate.php ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
6
+
7
+ /**
8
+ * Class ApplySameVatRate implements a vat completor strategy by applying the
9
+ * same vat rate to each line to complete.
10
+ *
11
+ * It also tries a vat rate of 0%. If that works, the system might be
12
+ * misconfigured or we have prepaid vouchers, but as we have to follow the
13
+ * system anyway, we will return it as is.
14
+ *
15
+ * Current known usages:
16
+ * - ???
17
+ */
18
+ class ApplySameVatRate extends CompletorStrategyBase
19
+ {
20
+ /** @var int This strategy should be tried (as one of the) first. */
21
+ static public $tryOrder = 0;
22
+
23
+ /**
24
+ * {@inheritdoc}
25
+ */
26
+ protected function execute()
27
+ {
28
+ // Try all vat rates.
29
+ foreach ($this->possibleVatRates as $vatRate) {
30
+ $vatRate = $vatRate['vatrate'];
31
+ if ($this->tryVatRate($vatRate)) {
32
+ return true;
33
+ }
34
+ }
35
+
36
+ // Try with a 0 tax rate as prepaid vouchers have 0 vat rate this might be a
37
+ // valid situation if the only lines to complete are voucher lines.
38
+ return $this->tryVatRate(0.0);
39
+ }
40
+
41
+ /**
42
+ * Tries 1 of the possible vat rates.
43
+ *
44
+ * @param float $vatRate
45
+ *
46
+ * @return bool
47
+ */
48
+ protected function tryVatRate($vatRate)
49
+ {
50
+ $this->description = "ApplySameVatRate($vatRate)";
51
+ $this->completedLines = array();
52
+ $vatAmount = 0.0;
53
+ foreach ($this->lines2Complete as $line2Complete) {
54
+ $vatAmount += $this->completeLine($line2Complete, $vatRate);
55
+ }
56
+
57
+ // If the vat totals are equal, the strategy worked.
58
+ // We allow for a reasonable margin, as rounding errors may add up.
59
+ return Number::floatsAreEqual($vatAmount, $this->vat2Divide, 0.04);
60
+ }
61
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/Fail.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
5
+
6
+ /**
7
+ * FailStrategy implements a complete strategy that always fails.
8
+ */
9
+ class Fail extends CompletorStrategyBase {
10
+ /** @var int This strategy should be executed last. */
11
+ static public $tryOrder = PHP_INT_MAX;
12
+
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ public function execute() {
17
+ $this->description = "Fail";
18
+ $this->completedLines = $this->lines2Complete;
19
+ return true;
20
+ }
21
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitKnownDiscountLine.php ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\Completor;
6
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
7
+
8
+ /**
9
+ * Class SplitKnownDiscountLine implements a vat completor strategy by using the
10
+ * 'meta-line-discount-amountinc' tags to split a discount line over several
11
+ * lines with different vat rates as it may be considered as the total discount
12
+ * over multiple products that may have different vat rates.
13
+ *
14
+ * Preconditions:
15
+ * - lines2Complete contains 1 line that may be split.
16
+ * - There should be other lines that have a 'meta-line-discount-amountinc' tag
17
+ * and an exact vat rate, and these amounts must add up to the amount of the
18
+ * line that is to be split.
19
+ * - This strategy should be executed early as it is as sure win and can even
20
+ * be used as a partial solution.
21
+ *
22
+ * Strategy:
23
+ * The amounts in the lines that have a 'meta-line-discount-amountinc' tag are
24
+ * summed by their vat rates and these "discount amounts per vat rate" are used
25
+ * to create the lines that replace the single discount line.
26
+ *
27
+ * Current usages:
28
+ * - Magento
29
+ * - @todo PrestaShop (if no discount on shipping and other fees this might work
30
+ * but will only be needed when we actually have multiple vat rates, otherwise
31
+ * it is a simple corrector.
32
+ */
33
+ class SplitKnownDiscountLine extends CompletorStrategyBase
34
+ {
35
+ /**
36
+ * @var int
37
+ * This strategy should be tried last before the fail strategy as there
38
+ * are chances of returning a wrong true result.
39
+ */
40
+ static public $tryOrder = 1;
41
+
42
+ /** @var array[] */
43
+ protected $splitLines;
44
+
45
+ /** @var array */
46
+ protected $splitLine;
47
+
48
+ /** @var float */
49
+ protected $knownDiscountAmountInc;
50
+
51
+ /** @var float */
52
+ protected $knownDiscountVatAmount;
53
+
54
+ /** @var float[] */
55
+ protected $discountsPerVatRate;
56
+
57
+ /**
58
+ * {@inheritdoc}
59
+ */
60
+ protected function init()
61
+ {
62
+ $splitLines = array();
63
+ foreach ($this->lines2Complete as $line2Complete) {
64
+ if (isset($line2Complete['meta-strategy-split']) && $line2Complete['meta-strategy-split']) {
65
+ $splitLines[] = $line2Complete;
66
+ }
67
+ }
68
+ if (count($splitLines) === 1) {
69
+ $this->splitLine = reset($splitLines);
70
+ }
71
+
72
+ $this->discountsPerVatRate = array();
73
+ $this->knownDiscountAmountInc = 0.0;
74
+ $this->knownDiscountVatAmount = 0.0;
75
+ foreach ($this->invoice['customer']['invoice']['line'] as $line) {
76
+ if (isset($line['meta-line-discount-amountinc']) && in_array($line['meta-vatrate-source'], Completor::$CorrectVatRateSources)) {
77
+ $this->knownDiscountAmountInc += $line['meta-line-discount-amountinc'];
78
+ $this->knownDiscountVatAmount += $line['meta-line-discount-amountinc'] / (100.0 + $line['vatrate']) * $line['vatrate'];
79
+ $vatRate = sprintf('%.3f', $line['vatrate']);
80
+ if (isset($this->discountsPerVatRate[$vatRate])) {
81
+ $this->discountsPerVatRate[$vatRate] += $line['meta-line-discount-amountinc'];
82
+ } else {
83
+ $this->discountsPerVatRate[$vatRate] = $line['meta-line-discount-amountinc'];
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * {@inheritdoc}
91
+ */
92
+ protected function checkPreconditions()
93
+ {
94
+ $result = false;
95
+ if (isset($this->splitLine)) {
96
+ if ((isset($this->splitLine['unitprice']) && Number::floatsAreEqual($this->splitLine['unitprice'], $this->knownDiscountAmountInc - $this->knownDiscountVatAmount))
97
+ || (isset($this->splitLine['unitpriceinc']) && Number::floatsAreEqual($this->splitLine['unitpriceinc'], $this->knownDiscountAmountInc))
98
+ ) {
99
+ $result = true;
100
+ }
101
+ }
102
+ return $result;
103
+ }
104
+
105
+ /**
106
+ * {@inheritdoc}
107
+ */
108
+ public function execute()
109
+ {
110
+ return $this->splitDiscountLine();
111
+ }
112
+
113
+ /**
114
+ * @return bool
115
+ */
116
+ protected function splitDiscountLine()
117
+ {
118
+ $this->description = "SplitKnownDiscountLine({$this->knownDiscountAmountInc}, {$this->knownDiscountVatAmount})";
119
+ $this->completedLines = array();
120
+ foreach ($this->discountsPerVatRate as $vatRate => $discountAmountInc) {
121
+ $line = $this->splitLine;
122
+ $line['product'] = "{$line['product']} ($vatRate%)";
123
+ $line['unitpriceinc'] = $discountAmountInc;
124
+ $this->completeLine($line, $vatRate);
125
+ }
126
+ return true;
127
+ }
128
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitLine.php ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
5
+ use Siel\Acumulus\Invoice\Creator;
6
+
7
+ /**
8
+ * Class SplitLine implements a vat completor strategy by recognizing that a
9
+ * (discount) line can have any vat rate between the minimum and maximum vat
10
+ * rate. This because the discount can be divided over multiple products that
11
+ * have different vat rates.
12
+ *
13
+ * Preconditions:
14
+ * - lines2Complete contains at least 1 line that may be split.
15
+ * - Exactly 2 vat rates that appear in the invoice, otherwise we can't compute
16
+ * 1 division.
17
+ * - This strategy should be executed after the tryAllVatRatePermutations, so we
18
+ * may assume that we *have to* split to arrive at a solution.
19
+ *
20
+ * Strategy:
21
+ * Other non completed lines, typically shipping and other fees, are all given
22
+ * the same vat rate and subsequently the then remaining vat to divide is split
23
+ * ove the slit lines.
24
+ *
25
+ * If there are multiple split lines, we cannot arrive at a correct division
26
+ * for all theses line separately, so we combine them into 1 discount line and
27
+ * split that line in 2.
28
+ *
29
+ * As this strategy has a lot of freedom it will probably succeed with the first
30
+ * try. Therefore we should start with the "most correct" vat rate for the fee
31
+ * lines, being the key component (hoofdbestanddeel). But as no known shop
32
+ * implements this, we start with the maximum rate (this is used by most shops)
33
+ * followed by the minimum rate but only if it is the key component.
34
+ *
35
+ * Possible improvements:
36
+ * - Instead of restricting us to vat rates that already appear on the invoice,
37
+ * we could also look at the possible vat rates that are part of the vat type
38
+ * for the invoice (given that the vat type has 2 vat rates).
39
+ * Example: all low vat products, but high vat shipping (because that is set
40
+ * as such, even if it would not be necessary in this case) and a discount
41
+ * over the whole order amount, thus products + shipping, and both the
42
+ * shipping and discount line are strategy lines, where the discount line is
43
+ * the split line.
44
+ * - Instead of restricting us to 1 vat type, we could check for all still
45
+ * possible vat types that have only 2 vat rates.
46
+ * - Instead of restricting us to appearing or possible vat rates we could try
47
+ * to assume that 1 line is a prepaid voucher and therefore vat free, and
48
+ * split just the other line(s).
49
+ *
50
+ * Current known usages:
51
+ * - ???
52
+ */
53
+ class SplitLine extends CompletorStrategyBase
54
+ {
55
+ /**
56
+ * @var int
57
+ * This strategy should be tried last before the fail strategy as there
58
+ * are chances of returning a wrong true result.
59
+ */
60
+ static public $tryOrder = 10;
61
+
62
+ /** @var array[] */
63
+ protected $splitLines;
64
+
65
+ /** @var float */
66
+ protected $splitLinesAmount;
67
+
68
+ /** @var array[] */
69
+ protected $otherLines;
70
+
71
+ /** @var float */
72
+ protected $otherLinesAmount;
73
+
74
+ /** @var float */
75
+ protected $nonStrategyAmount;
76
+
77
+ /** @var array */
78
+ protected $minVatRate;
79
+
80
+ /** @var array */
81
+ protected $maxVatRate;
82
+
83
+ /** @var array */
84
+ protected $keyComponent;
85
+
86
+ /**
87
+ * {@inheritdoc}
88
+ */
89
+ protected function init()
90
+ {
91
+ $this->splitLines = array();
92
+ $this->otherLines = array();
93
+ $this->otherLinesAmount = 0.0;
94
+ foreach ($this->lines2Complete as $line2Complete) {
95
+ if (!empty($line2Complete['meta-strategy-split'])) {
96
+ $this->splitLines[] = $line2Complete;
97
+ } else {
98
+ $this->otherLines[] = $line2Complete;
99
+ $this->otherLinesAmount += $line2Complete['unitprice'] * $line2Complete['quantity'];
100
+ }
101
+ }
102
+
103
+ $this->nonStrategyAmount = 0.0;
104
+ foreach ($this->invoice['customer']['invoice']['line'] as $line2Complete) {
105
+ if ($line2Complete['meta-vatrate-source'] !== Creator::VatRateSource_Strategy) {
106
+ $this->nonStrategyAmount += $line2Complete['unitprice'] * $line2Complete['quantity'];
107
+ }
108
+ }
109
+ $this->splitLinesAmount = $this->invoiceAmount - $this->nonStrategyAmount - $this->otherLinesAmount;
110
+
111
+ $this->minVatRate = $this->getVatBreakDownMinRate();
112
+ $this->maxVatRate = $this->getVatBreakDownMaxRate();
113
+ $this->keyComponent = $this->getVatBreakDownMaxAmount();
114
+ }
115
+
116
+ /**
117
+ * {@inheritdoc}
118
+ */
119
+ protected function checkPreconditions()
120
+ {
121
+ return count($this->vatBreakdown) === 2 && count($this->splitLines) >= 1;
122
+ }
123
+
124
+ /**
125
+ * {@inheritdoc}
126
+ */
127
+ public function execute()
128
+ {
129
+ if ($this->tryVatRate($this->maxVatRate['vatrate'])) {
130
+ return true;
131
+ }
132
+ if ($this->maxVatRate !== $this->keyComponent) {
133
+ if ($this->tryVatRate($this->keyComponent['vatrate'])) {
134
+ return true;
135
+ }
136
+ }
137
+
138
+ // Try a rate of 0% for all other lines.
139
+ //return $this->tryVatRate(0, $minRate, $maxRate);
140
+ return false;
141
+ }
142
+
143
+ /**
144
+ *
145
+ *
146
+ * @param float $vatRateForOtherLines
147
+ *
148
+ * @return bool
149
+ *
150
+ */
151
+ protected function tryVatRate($vatRateForOtherLines)
152
+ {
153
+ $this->description = "SplitLine($vatRateForOtherLines, {$this->minVatRate['vatrate']}, {$this->maxVatRate['vatrate']})";
154
+ $this->completedLines = array();
155
+ $otherVatAmount = 0.0;
156
+ foreach ($this->otherLines as $otherLine2Complete) {
157
+ $otherVatAmount += $this->completeLine($otherLine2Complete, $vatRateForOtherLines);
158
+ }
159
+ return $this->divideAmountOver2VatRates($this->vat2Divide - $otherVatAmount,
160
+ (float) $this->minVatRate['vatrate'] / 100.0,
161
+ (float) $this->maxVatRate['vatrate'] / 100.0);
162
+ }
163
+
164
+ /**
165
+ * Tries to split $this->splitLinesAmount over 2 lines with $lowVatRate and
166
+ * $highVatRate such that the vat amount for those 2 lines equals the
167
+ * Given an amount and a vat over that amount, split that amount over 2 given
168
+ * vat rates such that the total vat amount remains equal.
169
+ *
170
+ * Example €15,- with €2.40 vat and vat rates of 21% and 6% results in €10,-
171
+ * at 21% vat and €5,- at 6% vat.
172
+ *
173
+ * The math:
174
+ * 1) highAmount + LowAmount = Amount
175
+ * 2) highRate * highAmount + lowRate * lowAmount = VatAmount
176
+ *
177
+ * This results in:
178
+ * 1) highAmount = (vatAmount - Amount * lowRate) / (highRate - lowRate)
179
+ * 2) lowAmount = Amount - highAmount
180
+ *
181
+ * This may be considered successful if the sign of all 3 amounts is the same
182
+ * and both low and high amount are not 0 (this is a split strategy, not
183
+ * splitting but using 1 vat rate is tried by another strategy).
184
+ *
185
+ * @param float $splitVatAmount
186
+ * @param float $lowVatRate
187
+ * number between 0 and $highRate.
188
+ * @param float $highVatRate
189
+ * number between $lowRate and 1.
190
+ *
191
+ * @return bool
192
+ * Success.
193
+ */
194
+ protected function divideAmountOver2VatRates($splitVatAmount, $lowVatRate, $highVatRate)
195
+ {
196
+ // Divide the amount over the 2 vat rates, such that the sum of the divided
197
+ // amounts and the sum of the vat amounts equals the total amount and vat.
198
+ list($lowAmount, $highAmount) = $this->splitAmountOver2VatRates($this->splitLinesAmount, $splitVatAmount, $lowVatRate, $highVatRate);
199
+
200
+ // Dividing was possible if both amounts have the same sign.
201
+ if (($highAmount < -0.005 && $lowAmount < -0.005 && $this->splitLinesAmount < -0.005)
202
+ || ($highAmount > 0.005 && $lowAmount > 0.005 && $this->splitLinesAmount > 0.005)
203
+ ) {
204
+ // We split all lines by the same percentage.
205
+ $highPercentage = $highAmount / $this->splitLinesAmount;
206
+ $lowPercentage = $lowAmount / $this->splitLinesAmount;
207
+ foreach ($this->splitLines as $line) {
208
+ $splitLine = $line;
209
+ $splitLine['product'] .= ' ' . $highVatRate . '% ' . $this->t('vat');
210
+ if (isset($splitLine['unitprice'])) {
211
+ $splitLine['unitprice'] = $highPercentage * $splitLine['unitprice'];
212
+ }
213
+ if (isset($splitLine['unitpriceinc'])) {
214
+ $splitLine['unitpriceinc'] = $highPercentage * $splitLine['unitpriceinc'];
215
+ }
216
+ $this->completeLine($splitLine, $highVatRate);
217
+
218
+ $splitLine = $line;
219
+ $splitLine['product'] .= ' ' . $lowVatRate . '% ' . $this->t('vat');
220
+ if (isset($splitLine['unitprice'])) {
221
+ $splitLine['unitprice'] = $lowPercentage * $splitLine['unitprice'];
222
+ }
223
+ if (isset($splitLine['unitpriceinc'])) {
224
+ $splitLine['unitpriceinc'] = $lowPercentage * $splitLine['unitpriceinc'];
225
+ }
226
+ $this->completeLine($splitLine, $lowVatRate);
227
+ }
228
+ return true;
229
+ }
230
+
231
+ return false;
232
+ }
233
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/SplitNonMatchingLine.php ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
5
+
6
+ /**
7
+ * Class SplitNonMatchingLine implements a vat completor strategy by recognizing
8
+ * that a discount line (or ultra-correct shipping line) that does not match an
9
+ * allowed vat rate may have to be split over the vat rates that appear in the
10
+ * invoice. This because the discount can have been divided over multiple
11
+ * products that have different vat rates.
12
+ *
13
+ * Preconditions:
14
+ * - Exactly 2 VAT rates must be used in other lines.
15
+ * - lines2Complete may contain multiple lines that may be split.
16
+ *
17
+ * Note:
18
+ * - This strategy should be executed early as it is an almost sure win and is
19
+ * used as a partial solution for only some of the strategy lines.
20
+ *
21
+ * Strategy:
22
+ * Only lines that satisfy the following conditions are corrected:
23
+ * - To prevent "correcting" errors that lead to this non matching VAT rate,
24
+ * only lines that are marked with the value meta-strategy-split are
25
+ * corrected.
26
+ * - Each line2complete must have at least 2 of the values vatamount, unitprice,
27
+ * or unitpriceinc, such that it can be divided on its own.
28
+ * - They have gone through the correction pass, but no single matching VAT rate
29
+ * was found, so the value meta-vatrate-matches is set.
30
+ * These lines are split in such a way over the 2 allowed vat rates, that the
31
+ * values in the completed lines add up to the values in the line to be split.
32
+ *
33
+ * Current known usages:
34
+ * - PrestaShop (discount lines)
35
+ */
36
+ class SplitNonMatchingLine extends CompletorStrategyBase
37
+ {
38
+ /**
39
+ * @var int
40
+ * This strategy should be tried last before the fail strategy as there
41
+ * are chances of returning a wrong true result.
42
+ */
43
+ static public $tryOrder = 1;
44
+
45
+ /** @var array */
46
+ protected $minVatRate;
47
+
48
+ /** @var array */
49
+ protected $maxVatRate;
50
+
51
+ /**
52
+ * {@inheritdoc}
53
+ */
54
+ protected function init()
55
+ {
56
+ $this->linesCompleted = array();
57
+ }
58
+
59
+ /**
60
+ * {@inheritdoc}
61
+ */
62
+ protected function checkPreconditions()
63
+ {
64
+ $result = count($this->vatBreakdown) === 2;
65
+ return $result;
66
+ }
67
+
68
+ /**
69
+ * {@inheritdoc}
70
+ */
71
+ public function execute()
72
+ {
73
+ $this->minVatRate = $this->getVatBreakDownMinRate();
74
+ $this->maxVatRate = $this->getVatBreakDownMaxRate();
75
+ $this->description = sprintf('"SplitNonMatchingLine(%s, %s)', $this->minVatRate['vatrate'], $this->maxVatRate['vatrate']);
76
+ $result = false;
77
+ foreach ($this->lines2Complete as $key => $line2Complete) {
78
+ if (!empty($line2Complete['meta-strategy-split']) && !empty($line2Complete['meta-vatrate-matches'])) {
79
+ if ($this->splitNonMatchingLine($line2Complete)) {
80
+ $result = true;
81
+ $this->linesCompleted[] = $key;
82
+ }
83
+ }
84
+ }
85
+ return $result;
86
+ }
87
+
88
+ /**
89
+ * @param array $line
90
+ *
91
+ * @return bool
92
+ */
93
+ protected function splitNonMatchingLine(array $line)
94
+ {
95
+ list($lowAmount, $highAmount) = $this->splitAmountOver2VatRates($line['meta-line-price'],
96
+ $line['meta-line-priceinc'] - $line['meta-line-price'],
97
+ $this->minVatRate['vatrate'] / 100.0,
98
+ $this->maxVatRate['vatrate'] / 100.0);
99
+
100
+ // Dividing was possible if both amounts have the same sign.
101
+ if (($highAmount < -0.005 && $lowAmount < -0.005 && $line['meta-line-price'] < -0.005)
102
+ || ($highAmount > 0.005 && $lowAmount > 0.005 && $line['meta-line-price'] > 0.005)
103
+ ) {
104
+ $splitLine = $line;
105
+ $splitLine['product'] .= sprintf(' (%s%% %s)', $this->maxVatRate['vatrate'], $this->t('vat'));
106
+ $splitLine['unitprice'] = $highAmount;
107
+ unset($splitLine['unitpriceinc']);
108
+ $this->completeLine($splitLine, $this->maxVatRate['vatrate']);
109
+
110
+ $splitLine = $line;
111
+ $splitLine['product'] .= sprintf(' (%s%% %s)', $this->minVatRate['vatrate'], $this->t('vat'));
112
+ $splitLine['unitprice'] = $lowAmount;
113
+ unset($splitLine['unitpriceinc']);
114
+ $this->completeLine($splitLine, $this->minVatRate['vatrate']);
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategy/TryAllVatRatePermutations.php ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice\CompletorStrategy;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\CompletorStrategyBase;
6
+
7
+ /**
8
+ * Class TryAllTaxRatePermutations implements a vat completor strategy by trying
9
+ * all possible permutations of the vat rates for all possible vat types.
10
+ *
11
+ * Current known usages:
12
+ * - ???
13
+ */
14
+ class TryAllVatRatePermutations extends CompletorStrategyBase
15
+ {
16
+ /**
17
+ * @var int
18
+ * This strategy should be tried before the split strategy as that one will
19
+ * easily succeed.
20
+ */
21
+ static public $tryOrder = 25;
22
+
23
+ /** @var float[] */
24
+ protected $vatRates;
25
+
26
+ /** @var int */
27
+ protected $countLines;
28
+
29
+ /**
30
+ * {@inheritdoc}
31
+ */
32
+ public function execute()
33
+ {
34
+ $this->countLines = count($this->lines2Complete);
35
+
36
+ // Try without and with a 0 tax rate (prepaid vouchers have 0 vat rate, so
37
+ // discount lines may have 0 vat rate).
38
+ foreach (array(false, true) as $include0) {
39
+ foreach ($this->possibleVatTypes as $vatType) {
40
+ $this->setVatRates($vatType, $include0);
41
+ if ($this->tryAllPermutations(array())) {
42
+ return true;
43
+ }
44
+ }
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Initializes the array of vat rates to use for this permutation.
52
+ *
53
+ * @param float $vatType
54
+ * @param bool $include0
55
+ */
56
+ protected function setVatRates($vatType, $include0)
57
+ {
58
+ $this->vatRates = array();
59
+ foreach ($this->possibleVatRates as $vatRate) {
60
+ if ($vatRate['vattype'] === $vatType) {
61
+ $this->vatRates[] = $vatRate['vatrate'];
62
+ }
63
+ }
64
+ if ($include0) {
65
+ $this->vatRates[] = 0.0;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param array $permutation
71
+ *
72
+ * @return bool
73
+ */
74
+ protected function tryAllPermutations(array $permutation)
75
+ {
76
+ if (count($permutation) === $this->countLines) {
77
+ // Try this (complete) permutation.
78
+ return $this->try1Permutation($permutation);
79
+ } else {
80
+ // Complete this permutation recursively before we can try it.
81
+ $permutationIndex = count($permutation);
82
+ // Try all tax rates for the current line.
83
+ foreach ($this->vatRates as $taxRate => $amount) {
84
+ $permutation[$permutationIndex] = $taxRate;
85
+ if ($this->tryAllPermutations($permutation)) {
86
+ return true;
87
+ }
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * @param array $permutation
95
+ *
96
+ * @return bool
97
+ */
98
+ protected function try1Permutation(array $permutation)
99
+ {
100
+ $this->description = 'TryAllVatRatePermutations(' . implode(', ', $permutation) . ')';
101
+ $this->completedLines = array();
102
+ $vatAmount = 0.0;
103
+ $i = 0;
104
+ foreach ($this->lines2Complete as $line2Complete) {
105
+ $vatAmount += $this->completeLine($line2Complete, $permutation[$i]);
106
+ $i++;
107
+ }
108
+
109
+ // The strategy worked if the vat totals equals the vat to divide.
110
+ return Number::floatsAreEqual($vatAmount, $this->vat2Divide);
111
+ }
112
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategyBase.php ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\TranslatorInterface;
5
+
6
+ /**
7
+ * CompletorStrategyBase is the base class for all strategies that might
8
+ * be able to complete invoice lines by applying a strategy to divide a
9
+ * remaining vat amount over lines without a vat rate yet.
10
+ */
11
+ abstract class CompletorStrategyBase
12
+ {
13
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
14
+ protected $translator;
15
+
16
+ /** @var int Indication of the order of execution of the strategy. */
17
+ static $tryOrder = 50;
18
+
19
+ /** @var array[] */
20
+ protected $invoice;
21
+
22
+ /** @var array[] */
23
+ protected $possibleVatTypes;
24
+
25
+ /** @var array[] */
26
+ protected $possibleVatRates;
27
+
28
+ /** @var int[] */
29
+ protected $linesCompleted;
30
+
31
+ /** @var array[] */
32
+ protected $lines2Complete;
33
+
34
+ /** @var array[] */
35
+ protected $completedLines;
36
+
37
+ /** @var float */
38
+ protected $vat2Divide;
39
+
40
+ /** @var float */
41
+ protected $vatAmount;
42
+
43
+ /** @var float */
44
+ protected $invoiceAmount;
45
+
46
+ /** @var string */
47
+ protected $description = 'Not yet set';
48
+
49
+ /** @var array[] */
50
+ protected $vatBreakdown;
51
+
52
+ /** @var \Siel\Acumulus\Invoice\Source */
53
+ protected $source;
54
+
55
+ /**
56
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
57
+ * @param array $invoice
58
+ * @param array $possibleVatTypes
59
+ * @param array $possibleVatRates
60
+ * @param \Siel\Acumulus\Invoice\Source $source
61
+ */
62
+ public function __construct(TranslatorInterface $translator, array $invoice, array $possibleVatTypes, array $possibleVatRates, Source $source)
63
+ {
64
+ $this->translator = $translator;
65
+ $this->invoice = $invoice;
66
+ $this->possibleVatTypes = $possibleVatTypes;
67
+ $this->possibleVatRates = $possibleVatRates;
68
+ $this->source = $source;
69
+ $this->initAmounts();
70
+ $this->initLines2Complete();
71
+ $this->initVatBreakdown();
72
+ }
73
+
74
+ /**
75
+ * Helper method to translate strings.
76
+ *
77
+ * @param string $key
78
+ * The key to get a translation for.
79
+ *
80
+ * @return string
81
+ * The translation for the given key or the key itself if no translation
82
+ * could be found.
83
+ */
84
+ protected function t($key)
85
+ {
86
+ return $this->translator->get($key);
87
+ }
88
+
89
+ /**
90
+ * Returns the (parameterised) description of the latest tried strategy.
91
+ *
92
+ * @return string
93
+ */
94
+ public function getDescription()
95
+ {
96
+ return $this->description;
97
+ }
98
+
99
+ /**
100
+ * Returns the keys of the lines that are completed (and thus should be
101
+ * replaced by the completed lines).
102
+ *
103
+ * Should only be called after success.
104
+ *
105
+ * @return int[]
106
+ */
107
+ public function getLinesCompleted()
108
+ {
109
+ return $this->linesCompleted;
110
+ }
111
+
112
+ /**
113
+ * Returns the completed lines (that should replace the lines completed).
114
+ *
115
+ * Should only be called after success.
116
+ *
117
+ * @return array[]
118
+ */
119
+ public function getCompletedLines()
120
+ {
121
+ return $this->completedLines;
122
+ }
123
+
124
+ /**
125
+ * Initializes the amount properties.
126
+ *
127
+ * to be able to calculate the amounts, at least 2 of the 3 meta amounts
128
+ * meta-invoice-vatamount, meta-invoice-amountinc, or meta-invoice-amount must
129
+ * be known.
130
+ */
131
+ protected function initAmounts()
132
+ {
133
+ $invoicePart = &$this->invoice['customer']['invoice'];
134
+ $this->vatAmount = isset($invoicePart['meta-invoice-vatamount']) ? $invoicePart['meta-invoice-vatamount'] : $invoicePart['meta-invoice-amountinc'] - $invoicePart['meta-invoice-amount'];
135
+ $this->invoiceAmount = isset($invoicePart['meta-invoice-amount']) ? $invoicePart['meta-invoice-amount'] : $invoicePart['meta-invoice-amountinc'] - $invoicePart['meta-invoice-vatamount'];
136
+
137
+ // The vat amount to divide over the non completed lines is the total vat
138
+ // amount of the invoice minus all known vat amounts per line.
139
+ $this->vat2Divide = (float) $this->vatAmount;
140
+ foreach ($invoicePart['line'] as $line) {
141
+ if ($line['meta-vatrate-source'] !== Creator::VatRateSource_Strategy) {
142
+ // Deduct the vat amount from this line: if set, deduct it directly,
143
+ // otherwise calculate the vat amount using the vat rate and unit price.
144
+ if (isset($line['vatamount'])) {
145
+ $this->vat2Divide -= $line['vatamount'] * $line['quantity'];
146
+ } else {
147
+ $this->vat2Divide -= ($line['vatrate'] / 100.0) * $line['unitprice'] * $line['quantity'];
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Initializes $this->lines2Complete with all strategy lines.
155
+ */
156
+ protected function initLines2Complete()
157
+ {
158
+ $this->linesCompleted = array();
159
+ $this->lines2Complete = array();
160
+ foreach ($this->invoice['customer']['invoice']['line'] as $key => $line) {
161
+ if ($line['meta-vatrate-source'] === Creator::VatRateSource_Strategy) {
162
+ $this->linesCompleted[] = $key;
163
+ $this->lines2Complete[] = $line;
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Initializes $this->vatBreakdown with a breakdown of vat rates and amounts
170
+ * occurring on the invoice (and not being strategy lines).
171
+ */
172
+ protected function initVatBreakdown()
173
+ {
174
+ $this->vatBreakdown = array();
175
+ foreach ($this->invoice['customer']['invoice']['line'] as $line) {
176
+ if ($line['meta-vatrate-source'] !== Creator::VatRateSource_Strategy && isset($line['vatrate'])) {
177
+ $amount = $line['unitprice'] * $line['quantity'];
178
+ $vatAmount = $line['vatrate'] / 100.0 * $amount;
179
+ $vatRate = sprintf('%.3f', $line['vatrate']);
180
+ // Add amount to existing vatrate line or create a new line.
181
+ if (isset($this->vatBreakdown[$vatRate])) {
182
+ $breakdown = &$this->vatBreakdown[$vatRate];
183
+ $breakdown['vatamount'] += $vatAmount;
184
+ $breakdown['amount'] += $amount;
185
+ $breakdown['count']++;
186
+ } else {
187
+ $this->vatBreakdown[$vatRate] = array(
188
+ 'vatrate' => $vatRate,
189
+ 'vatamount' => $vatAmount,
190
+ 'amount' => $amount,
191
+ 'count' => 1,
192
+ );
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Returns the minimum vat rate on the invoice.
200
+ *
201
+ * @return array
202
+ * A vat rate overview (array with vatrate, vatamount, amount, count).
203
+ */
204
+ protected function getVatBreakDownMinRate()
205
+ {
206
+ $result = array('vatrate' => PHP_INT_MAX);
207
+ foreach ($this->vatBreakdown as $breakDown) {
208
+ if ($breakDown['vatrate'] < $result['vatrate']) {
209
+ $result = $breakDown;
210
+ }
211
+ }
212
+ return $result;
213
+ }
214
+
215
+ /**
216
+ * Returns the maximum vat rate on the invoice.
217
+ *
218
+ * @return array
219
+ * A vat rate overview (array with vatrate, vatamount, amount, count).
220
+ */
221
+ protected function getVatBreakDownMaxRate()
222
+ {
223
+ $result = array('vatrate' => -PHP_INT_MAX);
224
+ foreach ($this->vatBreakdown as $breakDown) {
225
+ if ($breakDown['vatrate'] > $result['vatrate']) {
226
+ $result = $breakDown;
227
+ }
228
+ }
229
+ return $result;
230
+ }
231
+
232
+ /**
233
+ * Returns the key component vat rate on the invoice (NL: hoofdbestanddeel).
234
+ *
235
+ * @return array
236
+ * A vat rate overview (array with vatrate, vatamount, amount, count).
237
+ */
238
+ protected function getVatBreakDownMaxAmount()
239
+ {
240
+ $result = array('amount' => -PHP_INT_MAX);
241
+ foreach ($this->vatBreakdown as $breakDown) {
242
+ if ($breakDown['amount'] > $result['amount']) {
243
+ $result = $breakDown;
244
+ }
245
+ }
246
+ return $result;
247
+ }
248
+
249
+ /**
250
+ * Applies the strategy to see if it results in a valid solution.
251
+ *
252
+ * @return bool
253
+ * Success.
254
+ */
255
+ public function apply()
256
+ {
257
+ $this->completedLines = array();
258
+ $this->init();
259
+ return $this->checkPreconditions() && $this->execute();
260
+ }
261
+
262
+ /**
263
+ * Strategy dependent initialization.
264
+ */
265
+ protected function init()
266
+ {
267
+ }
268
+
269
+ /**
270
+ * Some strategies can only be tried when some conditions are met, e.g. if
271
+ * there's only 1 line to complete.
272
+ *
273
+ * These checks are extracted from execute() and should be placed in this
274
+ * method instead.
275
+ *
276
+ * @return bool
277
+ * True if this strategy might be used, false otherwise.
278
+ */
279
+ protected function checkPreconditions()
280
+ {
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Tries to apply the strategy.
286
+ *
287
+ * A strategy might be parameterised, most likely by 1 or more vat rates, and
288
+ * thus involve multiple tries. The implementations of execute() should run
289
+ * all possible permutations and return true as soon as 1 permutation is
290
+ * successful.
291
+ *
292
+ * If the result is true, $this->$completedLines must contain the completed
293
+ * lines that will replace the lines to complete.
294
+ *
295
+ * @return bool
296
+ * true the strategy has been applied successfully, false otherwise.
297
+ */
298
+ abstract protected function execute();
299
+
300
+ /**
301
+ * Completes a line by filling in the given vat rate and calculating other
302
+ * possibly missing fields (vatamount, unitprice).
303
+ *
304
+ * @param $line2Complete
305
+ * @param $vatRate
306
+ *
307
+ * @return float
308
+ * The vat amount for the completed line.
309
+ */
310
+ protected function completeLine(&$line2Complete, $vatRate)
311
+ {
312
+ if (!isset($line2Complete['quantity'])) {
313
+ $line2Complete['quantity'] = 1;
314
+ }
315
+ $line2Complete['vatrate'] = $vatRate;
316
+ if (isset($line2Complete['unitprice'])) {
317
+ $line2Complete['vatamount'] = ($line2Complete['vatrate'] / 100.0) * $line2Complete['unitprice'];
318
+ } else { // isset($line2Complete['unitpriceinc'])
319
+ $line2Complete['vatamount'] = ($line2Complete['vatrate'] / (100.0 + $line2Complete['vatrate'])) * $line2Complete['unitpriceinc'];
320
+ $line2Complete['unitprice'] = $line2Complete['unitpriceinc'] - $line2Complete['vatamount'];
321
+ }
322
+ $this->completedLines[] = $line2Complete;
323
+ return $line2Complete['vatamount'] * $line2Complete['quantity'];
324
+ }
325
+
326
+ /**
327
+ * Splits $amount 9ex VAT) in 2 amounts, such that if the first amount is
328
+ * taxed with the $lowVatRate and the 2nd amount is taxed with the
329
+ * $highVatRate, the sum of the 2 vat amounts add up to the given vat amount.
330
+ *
331
+ * @param float $amount
332
+ * @param float $vatAmount
333
+ * @param float $lowVatRate
334
+ * @param float $highVatRate
335
+ *
336
+ * @return float[]
337
+ */
338
+ protected function splitAmountOver2VatRates($amount, $vatAmount, $lowVatRate, $highVatRate)
339
+ {
340
+ $highAmount = ($vatAmount - $amount * $lowVatRate) / ($highVatRate - $lowVatRate);
341
+ $lowAmount = $amount - $highAmount;
342
+ return array($lowAmount, $highAmount);
343
+ }
344
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/CompletorStrategyLines.php ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\TranslatorInterface;
5
+
6
+ /**
7
+ * The strategy lines completor class provides functionality to correct and
8
+ * complete invoice lines before sending them to Acumulus.
9
+ *
10
+ * This class:
11
+ * - Adds vat rates to lines that need a strategy to compute their vat rates.
12
+ *
13
+ * @package Siel\Acumulus
14
+ */
15
+ class CompletorStrategyLines
16
+ {
17
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
18
+ protected $translator;
19
+
20
+ /** @var array[] */
21
+ protected $invoice;
22
+
23
+ /** @var array[] */
24
+ protected $invoiceLines;
25
+
26
+ /** @var Source */
27
+ protected $source;
28
+
29
+ /**
30
+ * @var int[]
31
+ * The list of possible vat types, initially filled with possible vat types
32
+ * based on client country, invoiceHasLineWithVat(), is_company(), and the
33
+ * digital services setting. But then reduced by VAT rates we find on the
34
+ * order lines.
35
+ */
36
+ protected $possibleVatTypes;
37
+
38
+ /** @var array[] */
39
+ protected $possibleVatRates;
40
+
41
+ /**
42
+ * Constructor.
43
+ *
44
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
45
+ */
46
+ public function __construct(TranslatorInterface $translator)
47
+ {
48
+ $this->translator = $translator;
49
+ }
50
+
51
+ /**
52
+ * Completes the invoice with default settings that do not depend on shop
53
+ * specific data.
54
+ *
55
+ * @param array $invoice
56
+ * The invoice to complete.
57
+ * @param Source $source
58
+ * The source object for which this invoice was created.
59
+ * @param int[] $possibleVatTypes
60
+ * @param array[] $possibleVatRates
61
+ *
62
+ * @return array
63
+ * The completed invoice.
64
+ */
65
+ public function complete(array $invoice, Source $source, array $possibleVatTypes, array $possibleVatRates)
66
+ {
67
+ $this->invoice = $invoice;
68
+ $this->invoiceLines = &$this->invoice['customer']['invoice']['line'];
69
+ $this->source = $source;
70
+ $this->possibleVatTypes = $possibleVatTypes;
71
+ $this->possibleVatRates = $possibleVatRates;
72
+
73
+ $this->completeStrategyLines();
74
+ return $this->invoice;
75
+ }
76
+
77
+ /**
78
+ * Complete all lines that need a vat divide strategy to compute correct
79
+ * values.
80
+ */
81
+ protected function completeStrategyLines()
82
+ {
83
+ if ($this->invoiceHasStrategyLine()) {
84
+ $input = array_reduce($this->possibleVatRates, function ($carry, $item) {
85
+ return $carry . (empty($carry) ? '' : ',') . '[' . $item['vatrate'] . '%,' . $item['vattype'] . ']';
86
+ },
87
+ '');
88
+ $this->invoice['customer']['invoice']['meta-completor-strategy-input'] = "strategy input($input)";
89
+
90
+ $strategies = $this->getStrategyClasses();
91
+ foreach ($strategies as $strategyClass) {
92
+ /** @var CompletorStrategyBase $strategy */
93
+ $strategy = new $strategyClass($this->translator, $this->invoice, $this->possibleVatTypes, $this->possibleVatRates, $this->source);
94
+ if ($strategy->apply()) {
95
+ $this->replaceLinesCompleted($strategy->getLinesCompleted(), $strategy->getCompletedLines());
96
+ if (empty($this->invoice['customer']['invoice']['meta-completor-strategy-used'])) {
97
+ $this->invoice['customer']['invoice']['meta-completor-strategy-used'] = $strategy->getDescription();
98
+ } else {
99
+ $this->invoice['customer']['invoice']['meta-completor-strategy-used'] .= '; ' . $strategy->getDescription();
100
+ }
101
+ // Allow for partial solutions: a strategy may correct only some of
102
+ // the strategy lines and leave the rest up to other strategies.
103
+ if (!$this->invoiceHasStrategyLine()) {
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Returns whether the invoice has lines that are to be completed using a tax
113
+ * divide strategy.
114
+ *
115
+ * @return bool
116
+ */
117
+ protected function invoiceHasStrategyLine()
118
+ {
119
+ $result = false;
120
+ foreach ($this->invoiceLines as $line) {
121
+ if ($line['meta-vatrate-source'] === Creator::VatRateSource_Strategy
122
+ || (!empty($line['meta-strategy-split']) && isset($line['meta-vatrate-matches']))
123
+ ) {
124
+ $result = true;
125
+ break;
126
+ }
127
+ }
128
+ return $result;
129
+ }
130
+
131
+ /**
132
+ * Returns a list of strategy class names.
133
+ *
134
+ * @return string[]
135
+ */
136
+ protected function getStrategyClasses()
137
+ {
138
+ $result = array();
139
+
140
+ // For now hardcoded, but this can be turned into a discovery.
141
+ $namespace = '\Siel\Acumulus\Invoice\CompletorStrategy';
142
+ $result[] = "$namespace\\SplitNonMatchingLine";
143
+ $result[] = "$namespace\\SplitKnownDiscountLine";
144
+ $result[] = "$namespace\\ApplySameVatRate";
145
+ $result[] = "$namespace\\SplitLine";
146
+ $result[] = "$namespace\\TryAllVatRatePermutations";
147
+ $result[] = "$namespace\\Fail";
148
+
149
+ return $result;
150
+ }
151
+
152
+ /**
153
+ * Replaces all completed strategy lines with the given completed lines.
154
+ *
155
+ * @param int[] $linesCompleted
156
+ * @param array[] $completedLines
157
+ * An array of completed invoice lines to replace the strategy lines with.
158
+ */
159
+ protected function replaceLinesCompleted(array $linesCompleted, array $completedLines)
160
+ {
161
+ // Remove old strategy lines that are now completed.
162
+ $lines = array();
163
+ foreach ($this->invoice['customer']['invoice']['line'] as $key => $line) {
164
+ if (!in_array($key, $linesCompleted)) {
165
+ $lines[] = $line;
166
+ }
167
+ }
168
+
169
+ // And merge in the new completed ones.
170
+ foreach ($completedLines as &$completedLine) {
171
+ if ($completedLine['meta-vatrate-source'] === Creator::VatRateSource_Strategy) {
172
+ $completedLine['meta-vatrate-source'] = Completor::VatRateSource_Strategy_Completed;
173
+ }
174
+ }
175
+ $this->invoice['customer']['invoice']['line'] = array_merge($lines, $completedLines);
176
+ }
177
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/ConfigInterface.php ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ /**
5
+ * InvoiceConfigInterface defines an interface to store and retrieve invoice
6
+ * specific configuration values.
7
+ *
8
+ * Configuration is stored in the host environment (normally a web shop), this
9
+ * interface abstracts from how a specific web shop does so.
10
+ */
11
+ interface ConfigInterface
12
+ {
13
+ // Invoice API related constants.
14
+ const PaymentStatus_Due = 1;
15
+ const PaymentStatus_Paid = 2;
16
+
17
+ const Concept_No = 0;
18
+ const Concept_Yes = 1;
19
+
20
+ const InvoiceNrSource_ShopInvoice = 1;
21
+ const InvoiceNrSource_ShopOrder = 2;
22
+ const InvoiceNrSource_Acumulus = 3;
23
+
24
+ const InvoiceDate_InvoiceCreate = 1;
25
+ const InvoiceDate_OrderCreate = 2;
26
+ const InvoiceDate_Transfer = 3;
27
+
28
+ const OverwriteIfExists_No = 0;
29
+ const OverwriteIfExists_Yes = 1;
30
+
31
+ const ConfirmReading_No = 0;
32
+ const ConfirmReading_Yes = 1;
33
+
34
+ const DigitalServices_Unknown = 0;
35
+ const DigitalServices_Both = 1;
36
+ const DigitalServices_No = 2;
37
+ const DigitalServices_Only = 3;
38
+
39
+ const VatFreeProducts_Unknown = 0;
40
+ const VatFreeProducts_Both = 1;
41
+ const VatFreeProducts_No = 2;
42
+ const VatFreeProducts_Only = 3;
43
+
44
+ const VatType_National = 1;
45
+ const VatType_NationalReversed = 2;
46
+ const VatType_EuReversed = 3;
47
+ const VatType_RestOfWorld = 4;
48
+ const VatType_MarginScheme = 5;
49
+ const VatType_ForeignVat = 6;
50
+
51
+ /**
52
+ * Returns the set of settings related to the customer part of an invoice.
53
+ *
54
+ * @return array
55
+ * A keyed array with the keys:
56
+ * - defaultCustomerType
57
+ * - sendCustomer
58
+ * - genericCustomerEmail
59
+ * - overwriteIfExists
60
+ */
61
+ public function getCustomerSettings();
62
+
63
+ /**
64
+ * Returns the set of settings related to the invoice part of an invoice.
65
+ *
66
+ * @return array
67
+ * A keyed array with the keys:
68
+ * - digitalServices
69
+ * - defaultAccountNumber
70
+ * - defaultCostCenter
71
+ * - defaultInvoiceTemplate
72
+ * - defaultInvoicePaidTemplate
73
+ * - removeEmptyShipping
74
+ * - useMargin
75
+ */
76
+ public function getInvoiceSettings();
77
+
78
+
79
+ /**
80
+ * Returns the set of settings related to the shop characteristics that
81
+ * influence the invoice creation and completion
82
+ *
83
+ * @return array
84
+ * A keyed array with the keys:
85
+ * - invoiceNrSource
86
+ * - dateToUse
87
+ */
88
+ public function getShopSettings();
89
+
90
+ /**
91
+ * Returns the set of settings related to sending an email.
92
+ *
93
+ * @return array
94
+ * A keyed array with the keys:
95
+ * - emailAsPdf
96
+ * - emailBcc
97
+ * - emailFrom
98
+ * - subject
99
+ * - confirmReading
100
+ */
101
+ public function getEmailAsPdfSettings();
102
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Creator.php ADDED
@@ -0,0 +1,908 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Exception;
5
+ use Siel\Acumulus\Helpers\Countries;
6
+ use Siel\Acumulus\Helpers\Number;
7
+ use Siel\Acumulus\Helpers\TranslatorInterface;
8
+
9
+ /**
10
+ * Allows to create arrays in the Acumulus invoice structure from a web shop
11
+ * order or credit note. This array can than be sent to Acumulus using the
12
+ * Invoice:Add Acumulus API call.
13
+ *
14
+ * See https://apidoc.sielsystems.nl/content/invoice-add for the structure. In
15
+ * addition to the scheme as defined over there, additional keys or values are
16
+ * accepted:
17
+ * - all keys starting with meta- are used for debugging purposes.
18
+ * - other keys are used to complete and correct the invoice in the completor
19
+ * stage.
20
+ * - (PHP) null values indicates the absence of a value that should be there but
21
+ * could not be (easily) retrieved from the invoice source. These should be
22
+ * replaced with actual values in the completor stage as well.
23
+ *
24
+ * This base class:
25
+ * - Implements the basic break down into smaller actions that web shops should
26
+ * subsequently implement.
27
+ * - Provides helper methods for some recurring functionality.
28
+ */
29
+ abstract class Creator
30
+ {
31
+ const VatRateSource_Exact = 'exact';
32
+ const VatRateSource_Exact0 = 'exact-0';
33
+ const VatRateSource_Calculated = 'calculated';
34
+ const VatRateSource_Completor = 'completor';
35
+ const VatRateSource_Strategy = 'strategy';
36
+
37
+ const LineType_Shipping = 'shipping';
38
+ const LineType_PaymentFee = 'payment';
39
+ const LineType_GiftWrapping = 'gift';
40
+ const LineType_Manual = 'manual';
41
+ const LineType_Order = 'product';
42
+ const LineType_Discount = 'discount';
43
+ const LineType_Voucher = 'voucher';
44
+ const LineType_Other = 'other';
45
+ const LineType_Corrector = 'missing-amount-corrector';
46
+
47
+ /** @var \Siel\Acumulus\Shop\Config */
48
+ protected $config;
49
+
50
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
51
+ protected $translator;
52
+
53
+ /** @var \Siel\Acumulus\Helpers\Countries */
54
+ protected $countries;
55
+
56
+ /** @var array Resulting Acumulus invoice */
57
+ protected $invoice = array();
58
+
59
+ /** @var Source */
60
+ protected $invoiceSource;
61
+
62
+ /**
63
+ * Constructor.
64
+ *
65
+ * @param \Siel\Acumulus\Invoice\ConfigInterface $config
66
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
67
+ */
68
+ public function __construct(ConfigInterface $config, TranslatorInterface $translator)
69
+ {
70
+ $this->config = $config;
71
+
72
+ $this->translator = $translator;
73
+ $invoiceHelperTranslations = new Translations();
74
+ $this->translator->add($invoiceHelperTranslations);
75
+
76
+ $this->countries = new Countries();
77
+ }
78
+
79
+ /**
80
+ * Helper method to translate strings.
81
+ *
82
+ * @param string $key
83
+ * The key to get a translation for.
84
+ *
85
+ * @return string
86
+ * The translation for the given key or the key itself if no translation
87
+ * could be found.
88
+ */
89
+ protected function t($key)
90
+ {
91
+ return $this->translator->get($key);
92
+ }
93
+
94
+ /**
95
+ * Sets the source to create the invoice for.
96
+ *
97
+ * @param Source $invoiceSource
98
+ */
99
+ protected function setInvoiceSource($invoiceSource)
100
+ {
101
+ $this->invoiceSource = $invoiceSource;
102
+ if (!in_array($invoiceSource->getType(), array(Source::Order, Source::CreditNote))) {
103
+ $this->config->getLog()->error('Creator::setSource(): unknown source type %s', $this->invoiceSource->getType());
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Creates an Acumulus invoice from an order or credit note.
109
+ *
110
+ * @param Source $source
111
+ * The web shop order.
112
+ *
113
+ * @return array
114
+ * The acumulus invoice for this order.
115
+ */
116
+ public function create($source)
117
+ {
118
+ $this->setInvoiceSource($source);
119
+ $this->invoice = array();
120
+ $this->invoice['customer'] = $this->getCustomer();
121
+ $this->addCustomerDefaults();
122
+ $this->invoice['customer']['invoice'] = $this->getInvoice();
123
+ $this->addInvoiceDefaults();
124
+ $this->addInvoiceTotals();
125
+ $this->invoice['customer']['invoice']['line'] = $this->getInvoiceLines();
126
+ $this->addEmailAsPdf();
127
+ return $this->invoice;
128
+ }
129
+
130
+ /**
131
+ * Returns the 'customer' part of the invoice add structure.
132
+ *
133
+ * The following keys are allowed/expected by the API:
134
+ * - type: not needed, will be filled by the Completor
135
+ * - contactid: not expected: Acumulus id for this customer, in the absence of
136
+ * this value, the API uses
137
+ * - contactyourid: shop customer id
138
+ * - companyname1
139
+ * - companyname2
140
+ * - fullname
141
+ * - salutation
142
+ * - address1
143
+ * - address2
144
+ * - postalcode
145
+ * - city
146
+ * - countrycode
147
+ * - vatnumber
148
+ * - telephone
149
+ * - fax
150
+ * - email: used to identify clients.
151
+ * - overwriteifexists: not needed, will be filled by the Completor
152
+ * - bankaccountnumber
153
+ * - mark
154
+ *
155
+ * @return array
156
+ * A keyed array with the customer data.
157
+ */
158
+ abstract protected function getCustomer();
159
+
160
+ /**
161
+ * Completes the customer part with default settings that do not depend on
162
+ * shop specific data.
163
+ */
164
+ protected function addCustomerDefaults()
165
+ {
166
+ $customerSettings = $this->config->getCustomerSettings();
167
+ $this->addDefault($this->invoice['customer'], 'overwriteifexists', $customerSettings['overwriteIfExists'] ? ConfigInterface::OverwriteIfExists_Yes : ConfigInterface::OverwriteIfExists_No);
168
+ $this->addDefault($this->invoice['customer'], 'type', $customerSettings['defaultCustomerType']);
169
+ if (!empty($customerSettings['salutation'])) {
170
+ $this->invoice['customer']['salutation'] = $this->getSalutation($customerSettings['salutation']);
171
+ }
172
+ $this->addDefault($this->invoice['customer'], 'countrycode', 'nl');
173
+ $this->convertEuCountryCode();
174
+ $this->addDefault($this->invoice['customer'], 'country', $this->countries->getCountryName($this->invoice['customer']['countrycode']));
175
+ }
176
+
177
+ /**
178
+ * Returns the salutation based on the (customer) setting for the salutation.
179
+ *
180
+ * This base implementation will do token expansion by searching for tokens
181
+ * and calling the getProperty() method for each token found. So, normally it
182
+ * won't be necessary to override this method, as overriding getProperty will
183
+ * be more likely.
184
+ *
185
+ * @param string $salutation
186
+ *
187
+ * @return string
188
+ * The salutation for the customer of this order.
189
+ */
190
+ protected function getSalutation($salutation)
191
+ {
192
+ $salutation = preg_replace_callback('/\[#([^]]+)]/', array($this, 'salutationMatch'), $salutation);
193
+ return $salutation;
194
+ }
195
+
196
+ /**
197
+ * Callback for the preg_replace_callback call in Creator::getSalutation().
198
+ *
199
+ * This base implementation call the getProperty() method with the part of the
200
+ * match between the #s, so, normally it won't be necessary to override this
201
+ * method, as overriding getProperty will be more likely.
202
+ *
203
+ * @param array $matches
204
+ *
205
+ * @return string
206
+ * The salutation for the customer of this order.
207
+ */
208
+ protected function salutationMatch($matches)
209
+ {
210
+ return $this->searchProperty($matches[1]);
211
+ }
212
+
213
+ /**
214
+ * Searches for a property in the various shop specific arrays and/or objects.
215
+ *
216
+ * This base implementation only looks up in the shop source object or array,
217
+ * but that may be insufficient if the customer data is in a separate object
218
+ * or array.
219
+ *
220
+ * @param string $property
221
+ *
222
+ * @return string
223
+ *
224
+ */
225
+ protected function searchProperty($property)
226
+ {
227
+ return $this->getProperty($property, $this->invoiceSource->getSource());
228
+ }
229
+
230
+ /**
231
+ * Looks up a property in the web shop specific order object/array.
232
+ *
233
+ * This default implementation looks for the property in the following ways:
234
+ * If the passed parameter is an array:
235
+ * - looking up the property as key.
236
+ * If the passed parameter is an object:
237
+ * - Looking up the property by name (as existing property or via __get).
238
+ * - Calling the get{Property} getter.
239
+ * - Calling the {property}() method (as existing method or via __call).
240
+ *
241
+ * Override if the property name or getter method is constructed differently.
242
+ *
243
+ * @param string $property
244
+ * @param object|array $object
245
+ *
246
+ * @return string
247
+ * The value for the property of the given name or the empty string if not
248
+ * available.
249
+ */
250
+ protected function getProperty($property, $object)
251
+ {
252
+ $value = '';
253
+ if (is_array($object)) {
254
+ if (isset($object[$property])) {
255
+ $value = $object[$property];
256
+ }
257
+ } else {
258
+ if (isset($object->$property)) {
259
+ $value = $object->$property;
260
+ } else if (method_exists($object, $property)) {
261
+ $value = $object->$property();
262
+ } else {
263
+ $method = 'get' . ucfirst($property);
264
+ if (method_exists($object, $method)) {
265
+ $value = $object->$method();
266
+ } else if (method_exists($object, '__get')) {
267
+ @$value = $object->$property;
268
+ } else if (method_exists($object, '__call')) {
269
+ try {
270
+ $value = @$object->$property();
271
+ } catch (Exception $e) {
272
+ }
273
+ if (empty($value)) {
274
+ try {
275
+ $value = $object->$method();
276
+ } catch (Exception $e) {
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ return $value;
283
+ }
284
+
285
+ /**
286
+ * Wrapper around Countries::convertEuCountryCod().
287
+ *
288
+ * Converts EU country codes to their ISO equivalent:
289
+ * - Change UK to GB
290
+ * - Change EL to GR.
291
+ *
292
+ * This could/should be server side, but for now it is done client side.
293
+ */
294
+ protected function convertEuCountryCode()
295
+ {
296
+ $this->invoice['customer']['countrycode'] = $this->countries->convertEuCountryCode($this->invoice['customer']['countrycode']);
297
+ }
298
+
299
+ /**
300
+ * Returns the 'invoice' part of the invoice add structure.
301
+ *
302
+ * The following keys are allowed/expected by the API:
303
+ * - concept: not needed, will be filled by the Completor
304
+ * - number
305
+ * - vattype: not needed, will be filled by the Completor
306
+ * - issuedate
307
+ * - costcenter: not needed, will be filled by the Completor
308
+ * - accountnumber: not needed, will be filled by the Completor
309
+ * - paymentstatus
310
+ * - paymentdate
311
+ * - description
312
+ * - template: not needed, will be filled by the Completor
313
+ *
314
+ * Additional keys (not recognised by the API but used by the Completor or
315
+ * for support and debugging purposes):
316
+ * - meta-invoice-amount: the total invoice amount excluding VAT.
317
+ * - meta-invoice-amountinc: the total invoice amount including VAT.
318
+ * - meta-invoice-vatamount: the total vat amount for the invoice.
319
+ * - meta-lines-amount: the total invoice amount excluding VAT.
320
+ * - meta-lines-vatamount: the total vat amount for the invoice.
321
+ *
322
+ * Extending classes should normally not have to override this method, but
323
+ * should instead implement getInvoiceNumber(), getInvoiceDate(),
324
+ * getPaymentState(), getPaymentDate(), and, optionally, getDescription().
325
+ *
326
+ * @return array
327
+ * A keyed array with the invoice data (without the invoice lines).
328
+ */
329
+ protected function getInvoice()
330
+ {
331
+ $result = array();
332
+
333
+ $shopSettings = $this->config->getShopSettings();
334
+
335
+ $sourceToUse = $shopSettings['invoiceNrSource'];
336
+ if ($sourceToUse != ConfigInterface::InvoiceNrSource_Acumulus) {
337
+ $result['number'] = $this->getInvoiceNumber($sourceToUse);
338
+ }
339
+
340
+ $dateToUse = $shopSettings['dateToUse'];
341
+ if ($dateToUse != ConfigInterface::InvoiceDate_Transfer) {
342
+ $result['issuedate'] = $this->getInvoiceDate($dateToUse);
343
+ }
344
+
345
+ // Payment status and date.
346
+ $result['paymentstatus'] = $this->getPaymentState();
347
+ if ($result['paymentstatus'] === ConfigInterface::PaymentStatus_Paid) {
348
+ $this->addIfNotEmpty($result, 'paymentdate', $this->getPaymentDate());
349
+ }
350
+
351
+ $result['description'] = $this->getDescription();
352
+
353
+ return $result;
354
+ }
355
+
356
+ /**
357
+ * Completes the invoice part with default settings that do not depend on
358
+ * shop specific data.
359
+ */
360
+ protected function addInvoiceDefaults()
361
+ {
362
+ $invoiceSettings = $this->config->getInvoiceSettings();
363
+ $invoice = &$this->invoice['customer']['invoice'];
364
+ $this->addDefault($invoice, 'concept', ConfigInterface::Concept_No);
365
+ $this->addDefault($invoice, 'accountnumber', $invoiceSettings['defaultAccountNumber']);
366
+ $this->addDefault($invoice, 'costcenter', $invoiceSettings['defaultCostCenter']);
367
+ if (isset($invoice['paymentstatus'])
368
+ && $invoice['paymentstatus'] == ConfigInterface::PaymentStatus_Paid
369
+ // 0 = empty = use same invoice template as for non paid invoices
370
+ && $invoiceSettings['defaultInvoicePaidTemplate'] != 0
371
+ ) {
372
+ $this->addDefault($invoice, 'template', $invoiceSettings['defaultInvoicePaidTemplate']);
373
+ } else {
374
+ $this->addDefault($invoice, 'template', $invoiceSettings['defaultInvoiceTemplate']);
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Returns the number to use as invoice number.
380
+ *
381
+ * @param int $invoiceNumberSource
382
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceNrSource_ShopInvoice or
383
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceNrSource_ShopOrder.
384
+ *
385
+ * @return string
386
+ * The number to use as "invoice number" on the invoice, may contain a
387
+ * prefix.
388
+ */
389
+ abstract protected function getInvoiceNumber($invoiceNumberSource);
390
+
391
+ /**
392
+ * Returns the date to use as invoice date.
393
+ *
394
+ * @param int $dateToUse
395
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_InvoiceCreate or
396
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_OrderCreate
397
+ *
398
+ * @return string
399
+ * Date to send to Acumulus as the invoice date: yyyy-mm-dd.
400
+ */
401
+ protected function getInvoiceDate($dateToUse)
402
+ {
403
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
404
+ }
405
+
406
+ /**
407
+ * Returns whether the order has been paid or not.
408
+ *
409
+ * @return int
410
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Paid or
411
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Due
412
+ */
413
+ protected function getPaymentState()
414
+ {
415
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
416
+ }
417
+
418
+ /**
419
+ * Returns the payment date.
420
+ *
421
+ * The payment date is defined as the date on which the status changed from a
422
+ * non-paid state to a paid state. If there are multiple state changes, the
423
+ * last one is taken.
424
+ *
425
+ * @return string|null
426
+ * The payment date (yyyy-mm-dd) or null if the order has not been paid yet.
427
+ */
428
+ protected function getPaymentDate()
429
+ {
430
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
431
+ }
432
+
433
+ /**
434
+ * Returns the description for this invoice.
435
+ *
436
+ * This default implementation returns something like "Order 123".
437
+ *
438
+ * @return string
439
+ * Description of this invoice
440
+ */
441
+ protected function getDescription()
442
+ {
443
+ return ucfirst($this->t($this->invoiceSource->getType())) . ' ' . $this->invoiceSource->getReference();
444
+ }
445
+
446
+ /**
447
+ * Adds metadata about invoice totals to the invoice.
448
+ */
449
+ protected function addInvoiceTotals()
450
+ {
451
+ $this->invoice['customer']['invoice'] += $this->getInvoiceTotals();
452
+ $this->completeInvoiceTotals();
453
+ }
454
+
455
+ /**
456
+ * Returns an array with the totals fields.
457
+ *
458
+ * All total fields are optional but may be used or even expected by the
459
+ * Completor or are used for support and debugging purposes.
460
+ *
461
+ * This default implementation returns an empty array. Override to provide the
462
+ * values.
463
+ *
464
+ * @return array
465
+ * An array with the following possible keys:
466
+ * - meta-invoice-amount: the total invoice amount excluding VAT.
467
+ * - meta-invoice-amountinc: the total invoice amount including VAT.
468
+ * - meta-invoice-vatamount: the total vat amount for the invoice.
469
+ * - meta-invoice-vat: a vat breakdown per vat rate.
470
+ */
471
+ abstract protected function getInvoiceTotals();
472
+
473
+ /**
474
+ * Completes the set of invoice totals as set by getInvoiceTotals.
475
+ *
476
+ * Most shops only provide 2 out of these 3 in their data, so we calculate the
477
+ * 3rd.
478
+ */
479
+ protected function completeInvoiceTotals()
480
+ {
481
+ $invoice = &$this->invoice['customer']['invoice'];
482
+ if (!isset($invoice['meta-invoice-amount'])) {
483
+ $invoice['meta-invoice-amount'] = $invoice['meta-invoice-amountinc'] - $invoice['meta-invoice-vatamount'];
484
+ $invoice['meta-invoice-calculated'] = 'meta-invoice-amount';
485
+ }
486
+ if (!isset($invoice['meta-invoice-amountinc'])) {
487
+ $invoice['meta-invoice-amountinc'] = $invoice['meta-invoice-amount'] + $invoice['meta-invoice-vatamount'];
488
+ $invoice['meta-invoice-calculated'] = 'meta-invoice-amountinc';
489
+ }
490
+ if (!isset($invoice['meta-invoice-vatamount'])) {
491
+ $invoice['meta-invoice-vatamount'] = $invoice['meta-invoice-amountinc'] - $invoice['meta-invoice-amount'];
492
+ $invoice['meta-invoice-calculated'] = 'meta-invoice-vatamount';
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Returns the 'invoice''line' part of the invoice add structure.
498
+ *
499
+ * Each invoice line is a keyed array.
500
+ * The following keys are allowed/expected by the API:
501
+ * -itemnumber
502
+ * -product
503
+ * -unitprice
504
+ * -vatrate
505
+ * -quantity
506
+ * -costprice: optional, only for margin products
507
+ *
508
+ * Additional keys (not recognised by the API but used by the Completor or
509
+ * for support and debugging purposes):
510
+ * - unitpriceinc: the price of the item per unit including VAT.
511
+ * - vatamount: the amount of vat per unit.
512
+ * - meta-vatrate-source: the source for the vatrate value. Can be one of:
513
+ * - exact: should be an existing VAT rate.
514
+ * - calculated: should be close to an existing VAT rate, but may contain a
515
+ * rounding error.
516
+ * - completor: to be filled in by the completor.
517
+ * - strategy: to be filled in by a tax divide strategy. This may lead to
518
+ * this line being split into multiple lines.
519
+ * - meta-vatrate-min: the minimum value for the vat rate, based on the
520
+ * precision of the numbers used to calculate the vat rate.
521
+ * - meta-vatrate-max: the maximum value for the vat rate, based on the
522
+ * precision of the numbers used to calculate the vat rate.
523
+ * - meta-strategy-split: boolean that indicates if this line may be split
524
+ * into multiple lines to divide vat.
525
+ * - meta-line-price: the total price for this line excluding VAT.
526
+ * - meta-line-priceinc: the total price for this line including VAT.
527
+ * - meta-line-vatamount: the amount of VAT for the whole line.
528
+ * - meta-line-type: the type of line (order, shipping, discount, etc.)
529
+ * (*) = these are not yet used.
530
+ *
531
+ * These keys can be used to complete missing values or to assist in
532
+ * correcting rounding errors in values that are present.
533
+ *
534
+ * The base CompletorInvoiceLines expects;
535
+ * - meta-vatrate-source to be filled
536
+ * - If meta-vatrate-source = exact: no other keys expected
537
+ * - If meta-vatrate-source = calculated: meta-vatrate-min and
538
+ * meta-vatrate-max are expected to be filled. These values should come
539
+ * from calling the helper method getDivisionRange() with the values used to
540
+ * calculate the vat rate and their precision.
541
+ * - If meta-vatrate-source = completor: vatrate should be null and unitprice
542
+ * should be 0. The completor will typically fill vatrate with the highest
543
+ * or most appearing vat rate, looking at the exact and calculated (after
544
+ * correcting them for rounding errors) vat rates.
545
+ * - If meta-vatrate-source = strategy: vat rate should be null and either
546
+ * unitprice or unitpriceinc should be filled wit a non-0 amount (typically
547
+ * a negative amount as this is mostly used for spreading discounts over tax
548
+ * rates). Moreover, on the invoice level meta-invoice-amount and
549
+ * meta-invoice-vatamount should be filled in. The completor will use a tax
550
+ * divide strategy to arrive at valid values for the missing fields.
551
+ *
552
+ * Extending classes should normally not have to override this method, but
553
+ * should instead implement getItemLines(), getManualLines(),
554
+ * getGiftWrappingLine(), getShippingLine(), getPaymentFeeLine(),
555
+ * and getDiscountLines().
556
+ *
557
+ * @return array[]
558
+ * A non keyed array with all invoice lines.
559
+ */
560
+ protected function getInvoiceLines()
561
+ {
562
+ $itemLines = $this->getItemLines();
563
+ $itemLines = $this->addLineType($itemLines, static::LineType_Order);
564
+
565
+ $feeLines = $this->getFeeLines();
566
+
567
+ $discountLines = $this->getDiscountLines();
568
+ $discountLines = $this->addLineType($discountLines, static::LineType_Discount);
569
+
570
+ $manualLines = $this->getManualLines();
571
+ $manualLines = $this->addLineType($manualLines, static::LineType_Manual);
572
+
573
+ $result = array_merge($itemLines, $feeLines, $discountLines, $manualLines);
574
+ return $result;
575
+ }
576
+
577
+ /**
578
+ * Returns the item/product lines of the order.
579
+ *
580
+ * @return array[]
581
+ * Array of item line arrays.
582
+ */
583
+ protected function getItemLines()
584
+ {
585
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
586
+ }
587
+
588
+ /**
589
+ * Returns all the fee lines for the order.
590
+ *
591
+ * @return array[]
592
+ * An array of fee line arrays
593
+ */
594
+ protected function getFeeLines()
595
+ {
596
+ $result = array();
597
+
598
+ $line = $this->getGiftWrappingLine();
599
+ if ($line) {
600
+ $line['meta-line-type'] = static::LineType_GiftWrapping;
601
+ $result[] = $line;
602
+ }
603
+
604
+ $line = $this->getShippingLine();
605
+ if ($line) {
606
+ $line['meta-line-type'] = static::LineType_Shipping;
607
+ $result[] = $line;
608
+ }
609
+
610
+ $line = $this->getPaymentFeeLine();
611
+ if ($line) {
612
+ $line['meta-line-type'] = static::LineType_PaymentFee;
613
+ $result[] = $line;
614
+ }
615
+
616
+ return $result;
617
+ }
618
+
619
+ /**
620
+ * Returns the gift wrapping costs line.
621
+ *
622
+ * This base implementation return an empty array: no gift wrapping.
623
+ *
624
+ * @return array
625
+ * A line array, empty if there is no gift wrapping fee line.
626
+ */
627
+ protected function getGiftWrappingLine()
628
+ {
629
+ return array();
630
+ }
631
+
632
+ /**
633
+ * Returns the shipping costs line.
634
+ *
635
+ * To be able to produce a packing slip, a shipping line should normally be
636
+ * added, even for free shipping.
637
+ *
638
+ * @return array
639
+ * A line array, empty if there is no shipping fee line.
640
+ */
641
+ protected function getShippingLine()
642
+ {
643
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
644
+ }
645
+
646
+ /**
647
+ * Returns the payment fee line.
648
+ *
649
+ * This base implementation returns an empty array: no payment fee line.
650
+ *
651
+ * @return array
652
+ * A line array, empty if there is no payment fee line.
653
+ */
654
+ protected function getPaymentFeeLine()
655
+ {
656
+ return array();
657
+ }
658
+
659
+ /**
660
+ * Returns any applied discounts and partial payments.
661
+ *
662
+ * @return array[]
663
+ * An array of discount line arrays.
664
+ */
665
+ protected function getDiscountLines()
666
+ {
667
+ return $this->callSourceTypeSpecificMethod(__FUNCTION__, func_get_args());
668
+ }
669
+
670
+ /**
671
+ * Returns any manual lines.
672
+ *
673
+ * Manual lines may appear on credit notes to overrule amounts as calculated
674
+ * by the system. E.g. discounts applied on items should be taken into
675
+ * account when refunding (while the system did not or does not know if the
676
+ * discount also applied to that product), shipping costs may be returned
677
+ * except for the handling costs, etc.
678
+ *
679
+ * @return array[]
680
+ * An array of manual line arrays, may be empty.
681
+ */
682
+ protected function getManualLines()
683
+ {
684
+ return array();
685
+ }
686
+
687
+ /**
688
+ * Adds an emailaspdf section if enabled.
689
+ */
690
+ protected function addEmailAsPdf()
691
+ {
692
+ $emailAsPdfSettings = $this->config->getEmailAsPdfSettings();
693
+ if ($emailAsPdfSettings['emailAsPdf'] && !empty($this->invoice['customer']['email'])) {
694
+ $emailasPdf = array();
695
+ $emailasPdf['emailto'] = $this->invoice['customer']['email'];
696
+ $this->addDefault($emailasPdf, 'emailbcc', $emailAsPdfSettings['emailBcc']);
697
+ $this->addDefault($emailasPdf, 'emailfrom', $emailAsPdfSettings['emailFrom']);
698
+ $this->addDefault($emailasPdf, 'subject', strtr($emailAsPdfSettings['subject'], array(
699
+ '[#b]' => $this->invoiceSource->getReference(),
700
+ '[#f]' => isset($this->invoice['customer']['invoice']['number']) ? $this->invoice['customer']['invoice']['number'] : '',
701
+ )));
702
+ $invoice['customer']['invoice']['emailaspdf']['confirmreading'] = $emailAsPdfSettings['confirmReading'] ? ConfigInterface::ConfirmReading_Yes : ConfigInterface::ConfirmReading_No;
703
+
704
+ $this->invoice['customer']['invoice']['emailaspdf'] = $emailasPdf;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Returns whether the margin scheme may be used.
710
+ *
711
+ * @return bool
712
+ */
713
+ protected function allowMarginScheme()
714
+ {
715
+ $invoiceSettings = $this->config->getInvoiceSettings();
716
+ return $invoiceSettings['useMargin'];
717
+ }
718
+
719
+ /**
720
+ * Helper method to add a value from an array or object only if it is set and
721
+ * not empty.
722
+ *
723
+ * @param array $targetArray
724
+ * @param string $targetKey
725
+ * @param array|object $source
726
+ * @param string $sourceKey
727
+ *
728
+ * @return bool
729
+ * Whether the array value or object property is set and not empty and thus
730
+ * has been added.
731
+ */
732
+ protected function addIfSetAndNotEmpty(array &$targetArray, $targetKey, $source, $sourceKey)
733
+ {
734
+ if (is_array($source)) {
735
+ if (!empty($source[$sourceKey])) {
736
+ $targetArray[$targetKey] = $source[$sourceKey];
737
+ return true;
738
+ }
739
+ } else {
740
+ if (!empty($source->$sourceKey)) {
741
+ $targetArray[$targetKey] = $source->$sourceKey;
742
+ return true;
743
+ }
744
+ }
745
+ return false;
746
+ }
747
+
748
+ /**
749
+ * Helper method to add a non-empty value to an array.
750
+ *
751
+ * @param array $array
752
+ * @param string $key
753
+ * @param mixed $value
754
+ *
755
+ * @return bool
756
+ * True if the value was not empty and thus has been added, false otherwise,
757
+ */
758
+ protected function addIfNotEmpty(array &$array, $key, $value)
759
+ {
760
+ if (!empty($value)) {
761
+ $array[$key] = $value;
762
+ return true;
763
+ }
764
+ return false;
765
+ }
766
+
767
+ /**
768
+ * Helper method to add a value to an array even if it is not set.
769
+ *
770
+ * @param array $array
771
+ * @param string $key
772
+ * @param mixed $value
773
+ * @param mixed $default
774
+ *
775
+ * @return bool
776
+ * True if the value was not empty and thus has been added, false if the
777
+ * default has been added.,
778
+ */
779
+ protected function addEmpty(array &$array, $key, $value, $default = '')
780
+ {
781
+ if (!empty($value)) {
782
+ $array[$key] = $value;
783
+ return true;
784
+ } else {
785
+ $array[$key] = $default;
786
+ return false;
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Helper method to add a default (without overwriting) value to an array.
792
+ *
793
+ * @param array $array
794
+ * @param string $key
795
+ * @param mixed $value
796
+ *
797
+ * @return bool
798
+ * Whether the default was added.
799
+ */
800
+ protected function addDefault(array &$array, $key, $value)
801
+ {
802
+ if (empty($array[$key]) && !empty($value)) {
803
+ $array[$key] = $value;
804
+ return true;
805
+ }
806
+ return false;
807
+ }
808
+
809
+ /**
810
+ * Adds a meta-line-type tag to the lines.
811
+ *
812
+ * @param array $itemLines
813
+ * @param string $lineType
814
+ *
815
+ * @return array
816
+ *
817
+ */
818
+ protected function addLineType(array $itemLines, $lineType)
819
+ {
820
+ foreach ($itemLines as &$itemLine) {
821
+ $itemLine['meta-line-type'] = $lineType;
822
+ }
823
+ return $itemLines;
824
+ }
825
+
826
+ /**
827
+ * Returns the range in which the vat rate will lie.
828
+ *
829
+ * If a webshop does not store the vat rates used in the order, we must
830
+ * calculate them using a (product) price and the vat on it. But as webshops
831
+ * often store these numbers rounded to cents, the vat rate calculation
832
+ * becomes imprecise. Therefore we compute the range in which it will lie and
833
+ * will let the Completor do a comparison with the actual vat rates that an
834
+ * order can have (one of the Dutch or, for electronic services, other EU
835
+ * country VAT rates).
836
+ *
837
+ * - If $denominator = 0 the vatrate will be set to NULL and the Completor may
838
+ * try to get this line listed under the correct vat rate.
839
+ * - If $numerator = 0 the vatrate will be set to 0 and be treated as if it
840
+ * is an exact vat rate, not a vat range
841
+ *
842
+ * @param float $numerator
843
+ * @param float $denominator
844
+ * @param float $precisionNumerator
845
+ * @param float $precisionDenominator
846
+ *
847
+ * @return array
848
+ * Array with keys vatrate, meta-vatrate-min, meta-vatrate-max, and
849
+ * meta-vatrate-source.
850
+ */
851
+ public static function getVatRangeTags($numerator, $denominator, $precisionNumerator = 0.01, $precisionDenominator = 0.01)
852
+ {
853
+ if (Number::isZero($denominator, 0.0001)) {
854
+ return array(
855
+ 'vatrate' => null,
856
+ 'meta-vatrate-source' => static::VatRateSource_Completor,
857
+ );
858
+ } else if (Number::isZero($numerator, 0.0001)) {
859
+ return array(
860
+ 'vatrate' => 0,
861
+ 'vatamount' => $numerator,
862
+ 'meta-vatrate-source' => static::VatRateSource_Exact0,
863
+ );
864
+ } else {
865
+ $range = Number::getDivisionRange($numerator, $denominator, $precisionNumerator, $precisionDenominator);
866
+ return array(
867
+ 'vatrate' => 100.0 * $range['calculated'],
868
+ 'vatamount' => $numerator,
869
+ 'meta-vatrate-min' => 100.0 * $range['min'],
870
+ 'meta-vatrate-max' => 100.0 * $range['max'],
871
+ 'meta-vatrate-source' => static::VatRateSource_Calculated,
872
+ );
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Returns the sign to use for amounts that are always defined as a positive
878
+ * number, also on credit notes.
879
+ *
880
+ * @return float
881
+ * 1 for orders, -1 for credit notes.
882
+ */
883
+ protected function getSign()
884
+ {
885
+ return (float) ($this->invoiceSource->getType() === Source::CreditNote ? -1 : 1);
886
+ }
887
+
888
+ /**
889
+ * Calls a method constructed of the method name and the source type.
890
+ *
891
+ * If the implementation/override of a method depends on the type of invoice
892
+ * source it might be better to implement 1 method per source type. This
893
+ * method calls such a method assuming it is named {method}{source-type}.
894
+ * Example: getLineItem($line) would be very different for an order versus a
895
+ * credit note: do not override the base method but implement 2 new methods
896
+ * getLineItemOrder($line) and getLineItemCreditNote($line).
897
+ *
898
+ * @param string $method
899
+ * @param array $args
900
+ *
901
+ * @return mixed|void
902
+ */
903
+ protected function callSourceTypeSpecificMethod($method, $args = array())
904
+ {
905
+ $method .= $this->invoiceSource->getType();
906
+ return call_user_func_array(array($this, $method), $args);
907
+ }
908
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Source.php ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace Siel\Acumulus\Invoice;
4
+
5
+ /**
6
+ * Wraps a source for an invoice, typically an order or a credit note.
7
+ *
8
+ * By defining a wrapper around orders from a specific web shop we can more
9
+ * easily (and in a type safe way) pass them around.
10
+ */
11
+ abstract class Source
12
+ {
13
+ // Invoice source type constants.
14
+ const Order = 'Order';
15
+ const CreditNote = 'CreditNote';
16
+ const Other = 'Other';
17
+
18
+ /** @var string */
19
+ protected $type;
20
+
21
+ /** @var int */
22
+ protected $id;
23
+
24
+ /** @var array|object */
25
+ protected $source;
26
+
27
+ /**
28
+ * @param string $type
29
+ * @param int|string|array|object $idOrSource
30
+ */
31
+ public function __construct($type, $idOrSource)
32
+ {
33
+ $this->type = $type;
34
+ if (is_scalar($idOrSource)) {
35
+ $this->id = $idOrSource;
36
+ $this->setSource();
37
+ } else {
38
+ $this->source = $idOrSource;
39
+ $this->setId();
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Returns the type of the wrapped source.
45
+ *
46
+ * @return string
47
+ * One of the Source constants Source::Order or Source::CreditNote.
48
+ */
49
+ public function getType()
50
+ {
51
+ return $this->type;
52
+ }
53
+
54
+ /**
55
+ * Sets the source based on type and id.
56
+ *
57
+ * Should either be overridden or both setSourceOrder() and
58
+ * setSourceCreditNote() should be implemented.
59
+ */
60
+ protected function setSource()
61
+ {
62
+ return $this->callTypeSpecificMethod(__FUNCTION__);
63
+ }
64
+
65
+ /**
66
+ * Returns the source for an invoice, either an order or a credit note object.
67
+ *
68
+ * @return array|object
69
+ */
70
+ public function getSource()
71
+ {
72
+ return $this->source;
73
+ }
74
+
75
+ /**
76
+ * Sets the id based on type and source.
77
+ *
78
+ * Should either be overridden or both setIdOrder() and
79
+ * setIdCreditNote() should be implemented.
80
+ */
81
+ protected function setId()
82
+ {
83
+ return $this->callTypeSpecificMethod(__FUNCTION__);
84
+ }
85
+
86
+ /**
87
+ * Returns the internal id of this invoice source.
88
+ *
89
+ * @return int|string
90
+ * The internal id.
91
+ */
92
+ public function getId()
93
+ {
94
+ return $this->id;
95
+ }
96
+
97
+ /**
98
+ * Returns the user facing reference for this invoice source.
99
+ *
100
+ * Should be overridden when this is not the internal id.
101
+ *
102
+ * @return string|int
103
+ * The user facing id for this invoice source. This is not necessarily the
104
+ * internal id!
105
+ */
106
+ public function getReference()
107
+ {
108
+ return $this->getId();
109
+ }
110
+
111
+ /**
112
+ * Returns the status for this invoice source.
113
+ *
114
+ * Should either be overridden or both getStatusOrder() and
115
+ * getStatusCreditNote() should be implemented.
116
+ *
117
+ * @return mixed
118
+ * The status for this invoice source.
119
+ */
120
+ public function getStatus()
121
+ {
122
+ return $this->callTypeSpecificMethod(__FUNCTION__);
123
+ }
124
+
125
+ /**
126
+ * Calls a method that typically depends on the type of invoice source.
127
+ *
128
+ * @param string $method
129
+ *
130
+ * @return void|mixed
131
+ */
132
+ protected function callTypeSpecificMethod($method)
133
+ {
134
+ $method .= $this->getType();
135
+ return $this->$method();
136
+ }
137
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Invoice/Translations.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for classes in the \Siel\Acumulus\Invoice namespace.
8
+ */
9
+ class Translations extends TranslationCollection
10
+ {
11
+ protected $nl = array(
12
+ Source::Order => 'bestelling',
13
+ Source::CreditNote => 'creditnota',
14
+ Source::Other => 'overig',
15
+
16
+ 'vat' => 'BTW',
17
+ 'inc_vat' => 'incl. BTW',
18
+ 'ex_vat' => 'excl. BTW',
19
+ 'shipping_costs' => 'Verzendkosten',
20
+ // @todo: try to better distinguish free shipping and pickup: for now only WC, HS PS do so
21
+ 'free_shipping' => 'Gratis verzending',
22
+ 'pickup' => 'Afhalen',
23
+ 'payment_costs' => 'Betalingskosten',
24
+ 'discount' => 'Korting',
25
+ 'discount_code' => 'Kortingscode',
26
+ 'coupon_code' => 'Cadeaubon',
27
+ 'used' => 'gebruikt',
28
+ 'gift_wrapping' => 'Cadeauverpakking',
29
+ 'fee' => 'Behandelkosten',
30
+ 'refund' => 'Terugbetaling',
31
+ 'refund_adjustment' => 'Aanpassing teruggaafbedrag',
32
+ 'fee_adjustment' => 'Kosten (soort onbekend)',
33
+ 'discount_adjustment' => 'Handmatige korting',
34
+
35
+ 'message_warning_no_vat' => 'De factuur bevat geen BTW en er kan niet bepaald worden welk factuurtype (https://wiki.acumulus.nl/index.php?page=127) van toepassing is. Daarom is de factuur als concept opgeslagen. Corrigeer de factuur in Acumulus en controleer uw BTW instellingen.',
36
+ 'message_warning_no_vattype' => 'Door een fout in uw instellingen of BTW tarieven, kan het factuurtype (https://wiki.acumulus.nl/index.php?page=127) niet bepaald worden. Daarom is de factuur is als concept opgeslagen. Corrigeer of completeer de factuur in Acumulus.',
37
+ 'message_warning_multiple_vattype_must_split' => 'De factuur heeft meerdere factuurtypes (https://wiki.acumulus.nl/index.php?page=127). Daarom is de factuur als concept opgeslagen. Splits de factuur in Acumulus en verdeel de regels over beide facturen gebaseerd op het BTW type waar de regel onder valt.',
38
+ 'message_warning_multiple_vattype_may_split' => 'De factuur kan meerdere factuurtypes hebben (https://wiki.acumulus.nl/index.php?page=127). Daarom is de factuur als concept opgeslagen. Controleer het BTW type van de factuur in Acumulus en corrigeer deze indien nodig, of splits de factuur en verdeel de regels over beide facturen gebaseerd op het BTW type waar de regel onder valt.',
39
+ );
40
+
41
+ protected $en = array(
42
+ Source::Order => 'order',
43
+ Source::CreditNote => 'credit note',
44
+ Source::Other => 'other',
45
+
46
+ 'vat' => 'VAT',
47
+ 'inc_vat' => 'incl. VAT',
48
+ 'ex_vat' => 'ex. VAT',
49
+ 'shipping_costs' => 'Shipping costs',
50
+ 'free_shipping' => 'Free shipping',
51
+ 'pickup' => 'In store pick-up',
52
+ 'payment_costs' => 'Payment fee',
53
+ 'discount' => 'Discount',
54
+ 'discount_code' => 'Coupon code',
55
+ 'coupon_code' => 'Voucher',
56
+ 'used' => 'used',
57
+ 'gift_wrapping' => 'Gift wrapping',
58
+ 'fee' => 'Order treatment costs',
59
+ 'refund' => 'Refund',
60
+ 'refund_adjustment' => 'Refund adjustment',
61
+ 'fee_adjustment' => 'Fee (unknown type)',
62
+ 'discount_adjustment' => 'Manual discount',
63
+
64
+ 'message_warning_no_vat' => 'The invoice has no VAT and it is not possible to determine the invoice type (https://wiki.acumulus.nl/index.php?page=127). The invoice has been saved as concept. Correct the invoice in acumulus and check your VAT settings.',
65
+ 'message_warning_no_vattype' => 'Due to an error in your settings, we cannot determine the invoice type for the invoice (https://wiki.acumulus.nl/index.php?page=127). The invoice has been saved as concept. Correct or complete the invoice in Acumulus.',
66
+ 'message_warning_multiple_vattype_must_split' => 'The invoice has multiple invoice types (https://wiki.acumulus.nl/index.php?page=127). The invoice has been saved as concept. Split the invoice in Acumulus and divide the invoice lines over both invoices based on the VAT type the line belongs to.',
67
+ 'message_warning_multiple_vattype_may_split' => 'The invoice can have multiple invoice types (https://wiki.acumulus.nl/index.php?page=127). The invoice has been saved as concept. Check the VAT type of the invoice in Acumulus and correct if necessary, or split the invoice and divide the invoice lines over both invoices based on the VAT type the line belongs to.',
68
+ );
69
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/FormRenderer.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Helpers;
3
+
4
+ use JHtml;
5
+ use Siel\Acumulus\Helpers\FormRenderer as BaseFormRenderer;
6
+
7
+ /**
8
+ * Class FormRenderer renders a form in the Joomla/VirtueMart standards.
9
+ */
10
+ class FormRenderer extends BaseFormRenderer
11
+ {
12
+ /**
13
+ * Constructor.
14
+ */
15
+ public function __construct()
16
+ {
17
+ // Default Joomla template seems to use xhtml.
18
+ $this->html5 = false;
19
+ $this->fieldsetWrapperClass = 'adminform';
20
+ $this->elementWrapperClass = 'control-group';
21
+ $this->requiredMarkup = '<span class="star"> *</span>';
22
+ $this->labelWrapperTag = 'div';
23
+ $this->labelWrapperClass = 'control-label';
24
+ $this->inputWrapperTag = 'div';
25
+ $this->inputWrapperClass = 'controls';
26
+ $this->multiLabelClass = 'control-label';
27
+ $this->descriptionClass = 'controls';
28
+ }
29
+
30
+ /**
31
+ * {@inheritdoc}
32
+ */
33
+ public function input($type, $id, $name, $value = '', array $attributes = array())
34
+ {
35
+ $output = '';
36
+ if ($type === 'date') {
37
+ $output .= $this->getWrapper('input');
38
+ $output .= JHtml::calendar($value, $name, $id, $this->form->getShopDateFormat()/*, $attributes*/);
39
+ $output .= $this->getWrapperEnd('input');
40
+ } else {
41
+ $output .= parent::input($type, $id, $name, $value, $attributes);
42
+ }
43
+ return $output;
44
+ }
45
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/Log.php ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Helpers;
3
+
4
+ use JLog;
5
+ use Siel\Acumulus\Helpers\Log as BaseLog;
6
+ use Siel\Acumulus\Web\ConfigInterface;
7
+
8
+ /**
9
+ * Extends the base log class to log any library logging to the Joomla log.
10
+ */
11
+ class Log extends BaseLog
12
+ {
13
+ public function __construct(ConfigInterface $config)
14
+ {
15
+ parent::__construct($config);
16
+ JLog::addLogger(array('text_file' => 'acumulus.log.php'),
17
+ JLog::ALL,
18
+ array('com_acumulus')
19
+ );
20
+ }
21
+
22
+ /**
23
+ * {@inheritdoc}
24
+ *
25
+ * This override uses JLog.
26
+ */
27
+ protected function write($message, $severity)
28
+ {
29
+ jimport('joomla.log.log');
30
+ JLog::add($message, $this->getJoomlaSeverity($severity), 'com_acumulus');
31
+ }
32
+
33
+ /**
34
+ * Returns the joomla equivalent of the severity.
35
+ *
36
+ * @param int $severity
37
+ * One of the constants of the base Log class.
38
+ *
39
+ * @return int
40
+ * the Joomla equivalent of the severity.
41
+ */
42
+ protected function getJoomlaSeverity($severity)
43
+ {
44
+ switch ($severity) {
45
+ case Log::Error:
46
+ return JLog::ERROR;
47
+ case Log::Warning:
48
+ return JLog::WARNING;
49
+ case Log::Notice:
50
+ return JLog::NOTICE;
51
+ case Log::Debug:
52
+ default:
53
+ return JLog::DEBUG;
54
+ }
55
+ }
56
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Helpers/Mailer.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Helpers;
3
+
4
+ use JFactory;
5
+ use Siel\Acumulus\Helpers\Mailer as BaseMailer;
6
+
7
+ /**
8
+ * Extends the base mailer class to send a mail using the Joomla mail features.
9
+ */
10
+ class Mailer extends BaseMailer
11
+ {
12
+ /**
13
+ * {@inheritdoc}
14
+ */
15
+ public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
16
+ {
17
+ $app = JFactory::getApplication();
18
+ $mailer = JFactory::getMailer();
19
+ $mailer->isHtml(true);
20
+
21
+ $mailer->setSender(array($app->get('mailfrom'), $this->getFromName()));
22
+ $mailer->addRecipient($this->getToAddress());
23
+ $mailer->setSubject(html_entity_decode($this->getSubject($result)));
24
+ $body = $this->getBody($result, $messages, $invoiceSourceType, $invoiceSourceReference);
25
+ $mailer->setBody($body['html']);
26
+
27
+ return $mailer->Send();
28
+ }
29
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Invoice/Creator.php ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\HikaShop\Invoice;
3
+
4
+ use hikashopConfigClass;
5
+ use Siel\Acumulus\Helpers\Number;
6
+ use Siel\Acumulus\Invoice\ConfigInterface;
7
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
8
+ use stdClass;
9
+
10
+ /**
11
+ * Allows to create arrays in the Acumulus invoice structure from a HikaShop
12
+ * order
13
+ *
14
+ * Notes:
15
+ * - HikaShop knows discounts in the form of coupons or unrestricted discounts.
16
+ * Coupons can be without vat (to be seen as partial payment, which was
17
+ * probably not meant, thus incorrect) or with a fixed vat rate, independent
18
+ * from the products in the cart, thus also incorrect.
19
+ * - When a cart with a coupon contains products with another vat rate, the
20
+ * shown vat amount breakdown is incorrect. The Acumulus invoice will be
21
+ * correct, but may differ from the shop invoice, though the overall amount
22
+ * tends to be equal. It is the meta data in the invoice (as sent to Acumulus)
23
+ * that shows the differences.
24
+ * that shows the differences.
25
+ */
26
+ class Creator extends BaseCreator
27
+ {
28
+ /**
29
+ * @var object
30
+ */
31
+ protected $order;
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ *
36
+ * This override also initializes VM specific properties related to the
37
+ * source.
38
+ */
39
+ protected function setInvoiceSource($source)
40
+ {
41
+ parent::setInvoiceSource($source);
42
+ $this->order = $this->invoiceSource->getSource();
43
+ }
44
+
45
+ /**
46
+ * {@inheritdoc}
47
+ */
48
+ protected function getCustomer()
49
+ {
50
+ $result = array();
51
+
52
+ $this->addIfNotEmpty($result, 'contactyourid', $this->order->order_user_id);
53
+
54
+ $billingAddress = $this->order->billing_address;
55
+ if (!empty($billingAddress)) {
56
+ // @todo: hoe kan een klant dit (en vat#) invullen?
57
+ $this->addIfNotEmpty($result, 'companyname1', $billingAddress->address_company);
58
+ if (!empty($result['companyname1'])) {
59
+ $this->addIfNotEmpty($result, 'vatnumber', $billingAddress->address_vat);
60
+ }
61
+ $result['fullname'] = $billingAddress->address_lastname;
62
+ if (!empty($billingAddress->address_middle_name)) {
63
+ $result['fullname'] = $billingAddress->address_middle_name . ' ' . $result['fullname'];
64
+ }
65
+ if (!empty($billingAddress->address_firstname)) {
66
+ $result['fullname'] = $billingAddress->address_firstname . ' ' . $result['fullname'];
67
+ }
68
+ if (empty($result['fullname'])) {
69
+ $this->addIfNotEmpty($result, 'fullname', $this->order->customer->name);
70
+ }
71
+ $this->addIfNotEmpty($result, 'address1', $billingAddress->address_street);
72
+ $this->addIfNotEmpty($result, 'address2', $billingAddress->address_street2);
73
+ $this->addIfNotEmpty($result, 'postalcode', $billingAddress->address_post_code);
74
+ $this->addIfNotEmpty($result, 'city', $billingAddress->address_city);
75
+ $this->addIfNotEmpty($result, 'countrycode', $billingAddress->address_country_code_2);
76
+ // Preference for 1st phone number.
77
+ $this->addIfNotEmpty($result, 'telephone', $billingAddress->address_telephone2);
78
+ $this->addIfNotEmpty($result, 'telephone', $billingAddress->address_telephone);
79
+ $this->addIfNotEmpty($result, 'fax', $billingAddress->address_fax);
80
+ } else {
81
+ $this->addIfNotEmpty($result, 'fullname', $this->order->customer->name);
82
+ }
83
+
84
+ // Preference for the user email, not the registration email.
85
+ $this->addIfNotEmpty($result, 'email', $this->order->customer->email);
86
+ $this->addIfNotEmpty($result, 'email', $this->order->customer->user_email);
87
+
88
+ return $result;
89
+ }
90
+
91
+ /**
92
+ * {@inheritdoc}
93
+ */
94
+ protected function searchProperty($property)
95
+ {
96
+ $value = @$this->getProperty($property, $this->order);
97
+ if (empty($value)) {
98
+ $value = @$this->getProperty($property, $this->order->billing_address);
99
+ if (empty($value)) {
100
+ $value = @$this->getProperty($property, $this->order->customer);
101
+ }
102
+ }
103
+ return $value;
104
+ }
105
+
106
+ /**
107
+ * {@inheritdoc}
108
+ */
109
+ protected function getInvoiceNumber($invoiceNumberSource)
110
+ {
111
+ $result = $this->invoiceSource->getReference();
112
+ if ($invoiceNumberSource == ConfigInterface::InvoiceNrSource_ShopInvoice && !empty($this->order->order_invoice_number)) {
113
+ $result = $this->order->order_invoice_number;
114
+ }
115
+ return $result;
116
+ }
117
+
118
+ /**
119
+ * {@inheritdoc}
120
+ */
121
+ protected function getInvoiceDate($dateToUse)
122
+ {
123
+ $result = date('Y-m-d', $this->order->order_created);
124
+ if ($dateToUse == ConfigInterface::InvoiceDate_InvoiceCreate && !empty($this->order->order_invoice_created)) {
125
+ $result = date('Y-m-d', $this->order->order_invoice_created);
126
+ }
127
+ return $result;
128
+ }
129
+
130
+ /**
131
+ * {@inheritdoc}
132
+ */
133
+ protected function getPaymentState()
134
+ {
135
+ /** @var hikashopConfigClass $config */
136
+ $config = hikashop_config();
137
+ $unpaidStatuses = explode(',', $config->get('order_unpaid_statuses', 'created'));
138
+ return in_array($this->order->order_status, $unpaidStatuses)
139
+ ? ConfigInterface::PaymentStatus_Due
140
+ : ConfigInterface::PaymentStatus_Paid;
141
+ }
142
+
143
+ /**
144
+ * {@inheritdoc}
145
+ */
146
+ protected function getPaymentDate()
147
+ {
148
+ // Scan through the history and look for a non empty history_payment_id.
149
+ // The order of this array is by history_created DESC, we take the one that
150
+ // is furthest away in time.
151
+ $date = null;
152
+ foreach ($this->order->history as $history) {
153
+ if (!empty($history->history_payment_id)) {
154
+ $date = $history->history_created;
155
+ }
156
+ }
157
+ if (!$date) {
158
+ // Scan through the history and look for a non unpaid order status.
159
+ // We take the one that is furthest away in time.
160
+ /** @var hikashopConfigClass $config */
161
+ $config = hikashop_config();
162
+ $unpaidStatuses = explode(',', $config->get('order_unpaid_statuses', 'created'));
163
+ foreach ($this->order->history as $history) {
164
+ if (!empty($history->history_new_status) && !in_array($history->history_new_status, $unpaidStatuses)) {
165
+ $date = $history->history_created;
166
+ }
167
+ }
168
+ }
169
+ return $date ? date('Y-m-d', $date) : $date;
170
+ }
171
+
172
+ /**
173
+ * {@inheritdoc}
174
+ *
175
+ * This override provides the values meta-invoice-amountinc and
176
+ * meta-invoice-vatamount.
177
+ */
178
+ protected function getInvoiceTotals()
179
+ {
180
+ $vatAmount = 0.0;
181
+ // No order_taxinfo => no tax (?) => vatamount = 0.
182
+ if (!empty($this->order->order_tax_info)) {
183
+ foreach ($this->order->order_tax_info as $taxInfo) {
184
+ $vatAmount += $taxInfo->tax_amount;
185
+ }
186
+ }
187
+ return array(
188
+ 'meta-invoice-amountinc' => $this->order->order_full_price,
189
+ 'meta-invoice-vatamount' => $vatAmount,
190
+ );
191
+ }
192
+
193
+ /**
194
+ * {@inheritdoc}
195
+ */
196
+ protected function getItemLines()
197
+ {
198
+ $result = array_map(array($this, 'getItemLine'), $this->order->products);
199
+ return $result;
200
+ }
201
+
202
+ /**
203
+ * Returns 1 item line for 1 product line.
204
+ *
205
+ * @param stdClass $item
206
+ *
207
+ * @return array
208
+ */
209
+ protected function getItemLine(stdClass $item)
210
+ {
211
+ $productPriceEx = (float) $item->order_product_price;
212
+ $productVat = (float) $item->order_product_tax;
213
+
214
+ // Note that this info remains correct when rates are changed as upon order
215
+ // creation this info is stored in the order_product table.
216
+ if (is_array($item->order_product_tax_info) && count($item->order_product_tax_info) === 1) {
217
+ $productVatInfo = reset($item->order_product_tax_info);
218
+ if (!empty($productVatInfo->tax_rate)) {
219
+ $vatRate = $productVatInfo->tax_rate;
220
+ }
221
+ }
222
+ if (isset($vatRate)) {
223
+ $vatInfo = array(
224
+ 'vatrate' => 100.0 * $vatRate,
225
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
226
+ );
227
+ } else {
228
+ $vatInfo = $this->getVatRangeTags($productVat, $productPriceEx, 0.0001, 0.0001);
229
+ }
230
+ // @todo: options/variations: $item->order_product_options?
231
+ $result = array(
232
+ 'itemnumber' => $item->order_product_code,
233
+ 'product' => $item->order_product_name,
234
+ 'unitprice' => $productPriceEx,
235
+ 'meta-line-price' => $item->order_product_total_price_no_vat,
236
+ 'meta-line-priceinc' => $item->order_product_total_price,
237
+ 'quantity' => $item->order_product_quantity,
238
+ 'vatamount' => $productVat,
239
+ ) + $vatInfo;
240
+
241
+ return $result;
242
+ }
243
+
244
+ /**
245
+ * {@inheritdoc}
246
+ */
247
+ protected function getShippingLine()
248
+ {
249
+ $result = array();
250
+ // Check if there is a shipping id attached to the order.
251
+ if (!empty($this->order->order_shipping_id)) {
252
+ // Check for free shipping on a credit note.
253
+ if (!Number::isZero($this->order->order_shipping_price) || $this->invoiceSource->getType() !== Source::CreditNote) {
254
+ $shippingInc = (float) $this->order->order_shipping_price;
255
+ $shippingVat = (float) $this->order->order_shipping_tax;
256
+ $shippingEx = $shippingInc - $shippingVat;
257
+ $vatInfo = $this->getVatRangeTags($shippingVat, $shippingEx, 0.0001, 0.0002);
258
+ $description = $this->t('shipping_costs');
259
+
260
+ $result = array(
261
+ 'product' => $description,
262
+ 'unitpriceinc' => $shippingInc,
263
+ 'quantity' => 1,
264
+ 'vatamount' => $shippingVat,
265
+ ) + $vatInfo;
266
+ }
267
+ }
268
+ return $result;
269
+ }
270
+
271
+ /**
272
+ * {@inheritdoc}
273
+ */
274
+ protected function getDiscountLines()
275
+ {
276
+ $result = array();
277
+
278
+ if (!Number::isZero($this->order->order_discount_price)) {
279
+ $discountInc = (float) $this->order->order_discount_price;
280
+ $discountVat = (float) $this->order->order_discount_tax;
281
+ $discountEx = $discountInc - $discountVat;
282
+ $vatInfo = $this->getVatRangeTags($discountVat, $discountEx, 0.0001, 0.0002);
283
+ $description = empty($this->order->order_discount_code)
284
+ ? $this->t('discount')
285
+ : $this->t('discount_code') . ' ' . $this->order->order_discount_code;
286
+
287
+ $result[] = array(
288
+ 'product' => $description,
289
+ 'unitpriceinc' => -$discountInc,
290
+ 'quantity' => 1,
291
+ 'vatamount' => -$discountVat,
292
+ ) + $vatInfo;
293
+ }
294
+
295
+ return $result;
296
+ }
297
+
298
+ /**
299
+ * {@inheritdoc}
300
+ */
301
+ protected function getPaymentFeeLine()
302
+ {
303
+ // @todo check (return on refund?)
304
+ $result = array();
305
+ if (!Number::isZero($this->order->order_payment_price)) {
306
+ $paymentInc = (float) $this->order->order_payment_price;
307
+ $paymentVat = (float) $this->order->order_payment_tax;
308
+ $paymentEx = $paymentInc - $paymentVat;
309
+ $vatInfo = $this->getVatRangeTags($paymentVat, $paymentEx, 0.0001, 0.0002);
310
+ $description = $this->t('payment_costs');
311
+
312
+ $result = array(
313
+ 'product' => $description,
314
+ 'unitpriceinc' => $paymentInc,
315
+ 'quantity' => 1,
316
+ 'vatamount' => $paymentVat,
317
+ ) + $vatInfo;
318
+ }
319
+ return $result;
320
+ }
321
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Invoice/Source.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\HikaShop\Invoice;
3
+
4
+ use hikashopOrderClass;
5
+ use Siel\Acumulus\Invoice\Source as BaseSource;
6
+
7
+ /**
8
+ * Wraps a HikaShop order in an invoice source object.
9
+ */
10
+ class Source extends BaseSource
11
+ {
12
+ // More specifically typed properties.
13
+ /** @var object */
14
+ protected $source;
15
+
16
+ /**
17
+ * Loads an Order source for the set id.
18
+ */
19
+ protected function setSourceOrder()
20
+ {
21
+ /** @var hikashopOrderClass $class */
22
+ $class = hikashop_get('class.order');
23
+ $this->source = $class->loadFullOrder($this->id, true, false);
24
+ }
25
+
26
+ /**
27
+ * Sets the id based on the loaded Order.
28
+ */
29
+ protected function setIdOrder()
30
+ {
31
+ $this->id = $this->source->order_id;
32
+ }
33
+
34
+ /**
35
+ * {@inheritdoc}
36
+ */
37
+ public function getReference()
38
+ {
39
+ return $this->source->order_number;
40
+ }
41
+
42
+ /**
43
+ * {@inheritdoc}
44
+ *
45
+ * @return string
46
+ */
47
+ public function getStatus()
48
+ {
49
+ return $this->source->order_status;
50
+ }
51
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Shop/ConfigForm.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\HikaShop\Shop;
3
+
4
+ use Siel\Acumulus\Joomla\Shop\ConfigForm as BaseConfigForm;
5
+ use Siel\Acumulus\Shop\ConfigInterface;
6
+
7
+ /**
8
+ * Class ConfigForm processes and builds the settings form page for the
9
+ * HikaShop Acumulus module.
10
+ */
11
+ class ConfigForm extends BaseConfigForm
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ protected function getShopOrderStatuses()
17
+ {
18
+ /** @var \hikashopCategoryClass $class */
19
+ $class = hikashop_get('class.category');
20
+ $statuses = $class->loadAllWithTrans('status');
21
+
22
+ $orderStatuses = array();
23
+ foreach ($statuses as $state) {
24
+ $orderStatuses[$state->category_name] = $state->translation;
25
+ }
26
+ return $orderStatuses;
27
+ }
28
+
29
+ /**
30
+ * {@inheritdoc}
31
+ */
32
+ protected function getTriggerInvoiceSendEventOptions()
33
+ {
34
+ $result = parent::getTriggerInvoiceSendEventOptions();
35
+ // HikaShop does not have separate invoice entities, let alone a create
36
+ // event for that.
37
+ unset($result[ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate]);
38
+ return $result;
39
+ }
40
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/HikaShop/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\HikaShop\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\Invoice\Source as Source;
6
+ use Siel\Acumulus\Joomla\Shop\InvoiceManager as BaseInvoiceManager;
7
+
8
+ /**
9
+ * {@inheritdoc}
10
+ *
11
+ * This override provides HikaShop specific queries.
12
+ */
13
+ class InvoiceManager extends BaseInvoiceManager
14
+ {
15
+ /**
16
+ * {@inheritdoc}
17
+ *
18
+ * @todo: this override only returns order as supported invoice source type.
19
+ */
20
+ public function getSupportedInvoiceSourceTypes()
21
+ {
22
+ return array(
23
+ Source::Order,
24
+ //Source::CreditNote,
25
+ );
26
+ }
27
+
28
+ /**
29
+ * {@inheritdoc}
30
+ */
31
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
32
+ {
33
+ if ($invoiceSourceType === Source::Order) {
34
+ $query = sprintf("select order_id
35
+ from #__hikashop_order
36
+ where order_id between %d and %d",
37
+ $InvoiceSourceIdFrom, $InvoiceSourceIdTo);
38
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
39
+ }
40
+ return array();
41
+ }
42
+
43
+ /**
44
+ * {@inheritdoc}
45
+ *
46
+ * By default, HikaShop order numbers are non sequential random strings.
47
+ * So getting a range is not logical. However, extensions may exists that do
48
+ * introduce sequential order numbers in which case this query should be
49
+ * adapted.
50
+ */
51
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
52
+ {
53
+ if ($invoiceSourceType === Source::Order) {
54
+ $query = sprintf("select order_id
55
+ from #__hikashop_order
56
+ where order_number between '%s' and '%s'",
57
+ $this->getDb()->escape($InvoiceSourceReferenceFrom),
58
+ $this->getDb()->escape($InvoiceSourceReferenceTo)
59
+ );
60
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
61
+ }
62
+ return array();
63
+ }
64
+
65
+ /**
66
+ * {@inheritdoc}
67
+ */
68
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
69
+ {
70
+ if ($invoiceSourceType === Source::Order) {
71
+ $query = sprintf("select order_id
72
+ from #__hikashop_order
73
+ where order_modified between %u and %u",
74
+ $dateFrom->getTimestamp(), $dateTo->getTimestamp());
75
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
76
+ }
77
+ return array();
78
+ }
79
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Shop;
3
+
4
+ use DateTimeZone;
5
+ use JDate;
6
+ use JFactory;
7
+ use JTable;
8
+ use Siel\Acumulus\Shop\AcumulusEntryModel as BaseAcumulusEntryModel;
9
+ use Siel\Acumulus\Joomla\Helpers\Log;
10
+
11
+ /**
12
+ * Implements the VirtueMart specific acumulus entry model class.
13
+ */
14
+ class AcumulusEntryModel extends BaseAcumulusEntryModel
15
+ {
16
+ /**
17
+ * @return \AcumulusTableAcumulusEntry
18
+ */
19
+ protected function newTable()
20
+ {
21
+ $table = JTable::getInstance('AcumulusEntry', 'AcumulusTable');
22
+ if ($table === false) {
23
+ Log::getInstance()->error('AcumulusEntryModel::newTable(): table not created');
24
+ }
25
+ return $table;
26
+ }
27
+
28
+ /**
29
+ * {@inheritdoc}
30
+ */
31
+ public function getByEntryId($entryId)
32
+ {
33
+ $table = $this->newTable();
34
+ $result = $table->load(array('entry_id' => $entryId), true);
35
+ return $result ? $table : null;
36
+ }
37
+
38
+ /**
39
+ * {@inheritdoc}
40
+ */
41
+ public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId)
42
+ {
43
+ $table = $this->newTable();
44
+ $result = $table->load(array('source_type' => $invoiceSourceType, 'source_id' => $invoiceSourceId), true);
45
+ return $result ? $table : null;
46
+ }
47
+
48
+ /**
49
+ * {@inheritdoc}
50
+ */
51
+ protected function insert($invoiceSource, $entryId, $token, $created)
52
+ {
53
+ // Start with new table class to not overwrite any loaded record.
54
+ $table = $this->newTable();
55
+ $table->entry_id = $entryId;
56
+ $table->token = $token;
57
+ $table->source_type = $invoiceSource->getType();
58
+ $table->source_id = $invoiceSource->getId();
59
+ $table->created = $created;
60
+ $table->updated = $created;
61
+ return $table->store();
62
+ }
63
+
64
+ /**
65
+ * {@inheritdoc}
66
+ */
67
+ protected function update($record, $entryId, $token, $updated)
68
+ {
69
+ // Continue with existing table object with already loaded record.
70
+ $table = $record;
71
+ $table->entry_id = $entryId;
72
+ $table->token = $token;
73
+ $table->updated = $updated;
74
+ return $table->store(false);
75
+ }
76
+
77
+ /**
78
+ * {@inheritdoc}
79
+ */
80
+ protected function sqlNow()
81
+ {
82
+ $tz = new DateTimeZone(JFactory::getApplication()->get('offset'));
83
+ $date = new JDate();
84
+ $date->setTimezone($tz);
85
+ return $date->toSql(true);
86
+ }
87
+
88
+ /**
89
+ * {@inheritdoc}
90
+ *
91
+ * Joomla has separate install scripts, so nothing has to be done here.
92
+ */
93
+ public function install()
94
+ {
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * {@inheritdoc}
100
+ *
101
+ * Joomla has separate install scripts, so nothing has to be done here.
102
+ */
103
+ public function uninstall()
104
+ {
105
+ return false;
106
+ }
107
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/BatchForm.php ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Shop;
3
+
4
+ use JSession;
5
+ use JText;
6
+ use Siel\Acumulus\Shop\BatchForm as BaseBatchForm;
7
+
8
+ /**
9
+ * Provides the Batch send form handling for the VirtueMart Acumulus module.
10
+ */
11
+ class BatchForm extends BaseBatchForm
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ *
16
+ * This override checks the Joomla form token:
17
+ * https://docs.joomla.org/How_to_add_CSRF_anti-spoofing_to_forms
18
+ */
19
+ protected function systemValidate()
20
+ {
21
+ return JSession::checkToken();
22
+ }
23
+
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function getDateFormat()
28
+ {
29
+ return JText::_('DATE_FORMAT_LC4');
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ */
35
+ public function getShopDateFormat()
36
+ {
37
+ return str_replace(array('Y', 'm', 'd'), array('%Y', '%m', '%d'), $this->getDateFormat());
38
+ }
39
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/ConfigForm.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Shop;
3
+
4
+ use JSession;
5
+ use Siel\Acumulus\Shop\ConfigForm as BaseConfigForm;
6
+
7
+ /**
8
+ * Class ConfigForm adds features for the settings form page for the Joomla
9
+ * based shop modules.
10
+ */
11
+ abstract class ConfigForm extends BaseConfigForm
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ *
16
+ * This override checks the Joomla form token:
17
+ * https://docs.joomla.org/How_to_add_CSRF_anti-spoofing_to_forms
18
+ */
19
+ protected function systemValidate()
20
+ {
21
+ return JSession::checkToken();
22
+ }
23
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/ConfigStore.php ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Shop;
3
+
4
+ use JComponentHelper;
5
+ use JLoader;
6
+ use JModelLegacy;
7
+ use JTable;
8
+ use JTableExtension;
9
+ use Siel\Acumulus\Shop\ConfigStore as BaseConfigStore;
10
+
11
+ /**
12
+ * Implements the connection to the Joomla config component.
13
+ */
14
+ class ConfigStore extends BaSeConfigStore
15
+ {
16
+ /** @var array */
17
+ protected $savedValues = array();
18
+
19
+ /**
20
+ * {@inheritdoc}
21
+ */
22
+ public function getShopEnvironment()
23
+ {
24
+ /** @var JTableExtension $extension */
25
+ $extension = JTable::getInstance('extension');
26
+
27
+ $id = $extension->find(array('element' => 'com_acumulus', 'type' => 'component'));
28
+ $extension->load($id);
29
+ /** @noinspection PhpUndefinedFieldInspection */
30
+ $componentInfo = json_decode($extension->manifest_cache, true);
31
+ $moduleVersion = $componentInfo['version'];
32
+
33
+ $id = $extension->find(array('element' => 'com_' . strtolower($this->shopName), 'type' => 'component'));
34
+ $extension->load($id);
35
+ /** @noinspection PhpUndefinedFieldInspection */
36
+ $componentInfo = json_decode($extension->manifest_cache, true);
37
+ $shopVersion = $componentInfo['version'];
38
+
39
+ $joomlaVersion = JVERSION;
40
+
41
+ $environment = array(
42
+ 'moduleVersion' => $moduleVersion,
43
+ 'shopName' => $this->shopName,
44
+ 'shopVersion' => "$shopVersion (CMS: Joomla $joomlaVersion)",
45
+ );
46
+
47
+ return $environment;
48
+ }
49
+
50
+ /**
51
+ * {@inheritdoc}
52
+ */
53
+ public function load(array $keys)
54
+ {
55
+ $result = array();
56
+ $params = JComponentHelper::getParams('com_acumulus');
57
+ foreach ($keys as $key) {
58
+ $value = $params->get($key, null);;
59
+ if (isset($value)) {
60
+ $result[$key] = $value;
61
+ }
62
+ // Overwrite with values saved during this request.
63
+ if (isset($this->savedValues[$key])) {
64
+ $result[$key] = $this->savedValues[$key];
65
+ }
66
+ }
67
+ return $result;
68
+ }
69
+
70
+ /**
71
+ * {@inheritdoc}
72
+ */
73
+ public function save(array $values)
74
+ {
75
+ $values = $this->saveCommon($values);
76
+
77
+ // When the values are loaded in the same request, the new values are not
78
+ // retrieved: store a copy of them here to merge them when loading.
79
+ $this->savedValues = $values;
80
+
81
+ // Place the values in the component record.
82
+ /** @var \stdClass|array $component */
83
+ $component = JComponentHelper::getComponent('com_acumulus');
84
+ $component = (array) $component;
85
+ $component['params'] = empty($component['params']['data']) ? $values : array_merge($component['params']['data'], $values);
86
+
87
+ // Use save method of com_config component model.
88
+ JLoader::registerPrefix('Config', JPATH_ROOT . '/components/com_config');
89
+ JModelLegacy::addIncludePath(JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . 'com_config' . DIRECTORY_SEPARATOR . 'model');
90
+ /** @var \ConfigModelComponent $model */
91
+ $model = JModelLegacy::getInstance('Component', 'ConfigModel', array('ignore_request' => true));
92
+ return $model->save($component);
93
+ }
94
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\Shop;
3
+
4
+ use DateTimeZone;
5
+ use JDate;
6
+ use JEventDispatcher;
7
+ use JFactory;
8
+ use JPluginHelper;
9
+ use Siel\Acumulus\Invoice\Source as Source;
10
+ use Siel\Acumulus\Shop\InvoiceManager as BaseInvoiceManager;
11
+
12
+ /**
13
+ * {@inheritdoc}
14
+ *
15
+ * This override provides Joomla specific db helper methods and defines
16
+ * and dispatches Joomla events for the events defined by our library.
17
+ */
18
+ abstract class InvoiceManager extends BaseInvoiceManager
19
+ {
20
+ /**
21
+ * Helper method that executes a query to retrieve a list of invoice source
22
+ * ids and returns a list of invoice sources for these ids.
23
+ *
24
+ * @param string $invoiceSourceType
25
+ * @param string $query
26
+ *
27
+ * @return \Siel\Acumulus\Invoice\Source[]
28
+ * A non keyed array with invoice Sources.
29
+ */
30
+ protected function getSourcesByQuery($invoiceSourceType, $query)
31
+ {
32
+ $sourceIds = $this->loadColumn($query);
33
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $sourceIds);
34
+ }
35
+
36
+ /**
37
+ * Helper method to execute a query and return the 1st column from the
38
+ * results.
39
+ *
40
+ * @param string $query
41
+ *
42
+ * @return int[]
43
+ * A non keyed array with the values of the 1st results of the query result.
44
+ */
45
+ protected function loadColumn($query)
46
+ {
47
+ return $this->getDb()->setQuery($query)->loadColumn();
48
+ }
49
+
50
+ /**
51
+ * Helper method to get the db object.
52
+ *
53
+ * @return \JDatabaseDriver
54
+ */
55
+ protected function getDb()
56
+ {
57
+ return JFactory::getDbo();
58
+ }
59
+
60
+ /**
61
+ * Helper method that returns a date in the correct and escaped sql format.
62
+ *
63
+ * @param string $date
64
+ * Date in yyyy-mm-dd format.
65
+ *
66
+ * @return string
67
+ */
68
+ protected function toSql($date)
69
+ {
70
+ $tz = new DateTimeZone(JFactory::getApplication()->get('offset'));
71
+ $date = new JDate($date);
72
+ $date->setTimezone($tz);
73
+ return $date->toSql(true);
74
+ }
75
+
76
+ /**
77
+ * {@inheritdoc}
78
+ *
79
+ * This Joomla override dispatches the 'onAcumulusInvoiceCreated' event.
80
+ */
81
+ protected function triggerInvoiceCreated(array &$invoice, Source $invoiceSource)
82
+ {
83
+ JPluginHelper::importPlugin('acumulus');
84
+ $results = JEventDispatcher::getInstance()->trigger('onAcumulusInvoiceCreated', array(&$invoice, $invoiceSource));
85
+ if (count(array_filter($results, function ($value) {
86
+ return $value === false;
87
+ })) > 1
88
+ ) {
89
+ $invoice = null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * {@inheritdoc}
95
+ *
96
+ * This Joomla override dispatches the 'onAcumulusInvoiceCompleted' event.
97
+ */
98
+ protected function triggerInvoiceCompleted(array &$invoice, Source $invoiceSource)
99
+ {
100
+ JPluginHelper::importPlugin('acumulus');
101
+ $results = JEventDispatcher::getInstance()->trigger('onAcumulusInvoiceCompleted', array(&$invoice, $invoiceSource));
102
+ if (count(array_filter($results, function ($value) {
103
+ return $value === false;
104
+ })) > 1
105
+ ) {
106
+ $invoice = null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * {@inheritdoc}
112
+ *
113
+ * This Joomla override dispatches the 'onAcumulusInvoiceSent' event.
114
+ */
115
+ protected function triggerInvoiceSent(array $invoice, Source $invoiceSource, array $result)
116
+ {
117
+ JPluginHelper::importPlugin('acumulus');
118
+ JEventDispatcher::getInstance()->trigger('onAcumulusInvoiceSent', array($invoice, $invoiceSource, $result));
119
+ }
120
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Invoice/Creator.php ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\VirtueMart\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\ConfigInterface;
6
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
7
+ use stdClass;
8
+ use VmModel;
9
+
10
+ /**
11
+ * Allows to create arrays in the Acumulus invoice structure from a VirtueMart
12
+ * order
13
+ *
14
+ * Notes:
15
+ * - Calculation rules used, e.g, to give a certain customer group, a discount
16
+ * (fixed amount or percentage) should always have "price modifier before tax"
17
+ * or "price modifier before tax per bill" for the Type of Arithmetic
18
+ * Operation. Otherwise the VAT computations won't comply with Dutch
19
+ * regulations.
20
+ * - "price modifier before tax per bill" will show normal product prices with a
21
+ * separate discount line indicating the name of the discount rule.
22
+ * - "price modifier before tax per bill" will show discounted product prices
23
+ * without a separate discount line and thus also no mention of the applied
24
+ * discount. In general this option will be a bit more accurate, but IMO that
25
+ * does not weigh up against the loss of information on the invoice.
26
+ * - The VMInvoice extension offers credit notes, but for now we do not support
27
+ * this.
28
+ */
29
+ class Creator extends BaseCreator
30
+ {
31
+ /** @var \VirtueMartModelOrders */
32
+ protected $orderModel;
33
+
34
+ /**
35
+ * @var array
36
+ * Array with keys:
37
+ * [details]
38
+ * [BT]: stdClass (BillTo details)
39
+ * [ST]: stdClass (ShipTo details, if available)
40
+ * [history]
41
+ * [0]: stdClass (virtuemart_order_histories table record)
42
+ * ...
43
+ * [items]
44
+ * [0]: stdClass (virtuemart_order_items table record)
45
+ * ...
46
+ * [calc_rules]
47
+ * [0]: stdClass (virtuemart_order_calc_rules table record)
48
+ * ...
49
+ */
50
+ protected $order;
51
+
52
+ /** @var array */
53
+ protected $shopInvoice = array();
54
+
55
+ /** @var stdClass
56
+ * Object with properties:
57
+ * [...]: virtuemart_vmusers table record columns
58
+ * [shopper_groups]: array of stdClass virtuemart_vmuser_shoppergroups table
59
+ * records
60
+ * [JUser]: JUser object
61
+ * [userInfo]: array of stdClass virtuemart_userinfos table records
62
+ */
63
+ protected $user;
64
+
65
+ /** @var int */
66
+ protected $userBtUid;
67
+
68
+ /**
69
+ * {@inheritdoc}
70
+ *
71
+ * This override also initializes VM specific properties related to the
72
+ * source.
73
+ */
74
+ protected function setInvoiceSource($source)
75
+ {
76
+ parent::setInvoiceSource($source);
77
+ $this->order = $this->invoiceSource->getSource();
78
+ $this->orderModel = VmModel::getModel('orders');
79
+ /** @var \TableInvoices $invoiceTable */
80
+ if ($invoiceTable = $this->orderModel->getTable('invoices')->load($this->order['details']['BT']->virtuemart_order_id, 'virtuemart_order_id')) {
81
+ $this->shopInvoice = $invoiceTable->getProperties();
82
+ }
83
+ if (!empty($this->order['details']['BT']->virtuemart_user_id)) {
84
+ /** @var \VirtueMartModelUser $userModel */
85
+ $userModel = VmModel::getModel('user');
86
+ $userModel->setId($this->order['details']['BT']->virtuemart_user_id);
87
+ $this->user = $userModel->getUser();
88
+ $this->userBtUid = null;
89
+
90
+ foreach ($this->user->userInfo as $uid => $userInfo) {
91
+ if ($userInfo->address_type === 'BT') {
92
+ $this->userBtUid = $uid;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * {@inheritdoc}
101
+ */
102
+ protected function getCustomer()
103
+ {
104
+ $result = array();
105
+
106
+ $this->addIfSetAndNotEmpty($result, 'contactyourid', $this->order['details']['BT'], 'virtuemart_user_id');
107
+ $this->addIfSetAndNotEmpty($result, 'companyname1', $this->order['details']['BT'], 'company');
108
+ if (!empty($result['companyname1']) && !empty($this->userBtUid)) {
109
+ // @todo: there's also a (paid) EU VAT checker extension that probably does not use the field 'tax_exemption_number'.
110
+ $this->addIfSetAndNotEmpty($result, 'vatnumber', $this->user->userInfo[$this->userBtUid], 'tax_exemption_number');
111
+ }
112
+ $result['fullname'] = $this->order['details']['BT']->last_name;
113
+ if (!empty($this->order['details']['BT']->middle_name)) {
114
+ $result['fullname'] = $this->order['details']['BT']->middle_name . ' ' . $result['fullname'];
115
+ }
116
+ if (!empty($this->order['details']['BT']->first_name)) {
117
+ $result['fullname'] = $this->order['details']['BT']->first_name . ' ' . $result['fullname'];
118
+ }
119
+ $this->addIfSetAndNotEmpty($result, 'address1', $this->order['details']['BT'], 'address_1');
120
+ $this->addIfSetAndNotEmpty($result, 'address2', $this->order['details']['BT'], 'address_2');
121
+ $this->addIfSetAndNotEmpty($result, 'postalcode', $this->order['details']['BT'], 'zip');
122
+ $this->addIfSetAndNotEmpty($result, 'city', $this->order['details']['BT'], 'city');
123
+ if (!empty($this->order['details']['BT']->virtuemart_country_id)) {
124
+ /** @var \VirtueMartModelCountry $countryModel */
125
+ $countryModel = VmModel::getModel('country');
126
+ $country = $countryModel->getData($this->order['details']['BT']->virtuemart_country_id);
127
+ $this->addIfSetAndNotEmpty($result, 'countrycode', $country, 'country_2_code');
128
+ }
129
+ $this->addIfSetAndNotEmpty($result, 'telephone', $this->order['details']['BT'], 'phone_2');
130
+ $this->addIfSetAndNotEmpty($result, 'telephone', $this->order['details']['BT'], 'phone_1');
131
+ $this->addIfSetAndNotEmpty($result, 'fax', $this->order['details']['BT'], 'fax');
132
+ $this->addIfSetAndNotEmpty($result, 'email', $this->order['details']['BT'], 'email');
133
+
134
+ return $result;
135
+ }
136
+
137
+ /**
138
+ * {@inheritdoc}
139
+ */
140
+ protected function searchProperty($property)
141
+ {
142
+ $value = @$this->getProperty($property, $this->order['details']['BT']);
143
+ if (empty($value)) {
144
+ $value = @$this->getProperty($property, $this->user);
145
+ }
146
+ if (empty($value)) {
147
+ $value = @$this->getProperty($property, $this->user->userInfo[$this->userBtUid]);
148
+ }
149
+ return $value;
150
+ }
151
+
152
+ /**
153
+ * {@inheritdoc}
154
+ */
155
+ protected function getInvoiceNumber($invoiceNumberSource)
156
+ {
157
+ $result = $this->invoiceSource->getReference();
158
+ if ($invoiceNumberSource == ConfigInterface::InvoiceNrSource_ShopInvoice && !empty($this->shopInvoice['invoice_number'])) {
159
+ $result = $this->shopInvoice['invoice_number'];
160
+ }
161
+ return $result;
162
+ }
163
+
164
+ /**
165
+ * {@inheritdoc}
166
+ */
167
+ protected function getInvoiceDate($dateToUse)
168
+ {
169
+ $result = date('Y-m-d', strtotime($this->order['details']['BT']->created_on));
170
+ if ($dateToUse == ConfigInterface::InvoiceDate_InvoiceCreate && !empty($this->shopInvoice['created_on'])) {
171
+ $result = date('Y-m-d', strtotime($this->shopInvoice['created_on']));
172
+ }
173
+ return $result;
174
+ }
175
+
176
+ /**
177
+ * {@inheritdoc}
178
+ */
179
+ protected function getPaymentState()
180
+ {
181
+ $order = $this->invoiceSource->getSource();
182
+ return in_array($order['details']['BT']->order_status, $this->getPaidStates())
183
+ ? ConfigInterface::PaymentStatus_Paid
184
+ : ConfigInterface::PaymentStatus_Due;
185
+ }
186
+
187
+ /**
188
+ * {@inheritdoc}
189
+ */
190
+ protected function getPaymentDate()
191
+ {
192
+ $date = null;
193
+ $previousStatus = '';
194
+ foreach ($this->order['history'] as $orderHistory) {
195
+ if (in_array($orderHistory->order_status_code, $this->getPaidStates()) && !in_array($previousStatus, $this->getPaidStates())) {
196
+ $date = $orderHistory->created_on;
197
+ }
198
+ $previousStatus = $orderHistory->order_status_code;
199
+ }
200
+ return $date ? date('Y-m-d', strtotime($date)) : $date;
201
+ }
202
+
203
+ /**
204
+ * Returns a list of order states that indicate that the order has been paid.
205
+ *
206
+ * @return array
207
+ */
208
+ protected function getPaidStates()
209
+ {
210
+ return array('C', 'S', 'R');
211
+ }
212
+
213
+ /**
214
+ * {@inheritdoc}
215
+ *
216
+ * This override provides the values meta-invoice-amountinc and
217
+ * meta-invoice-vatamount as they may be needed by the Completor.
218
+ */
219
+ protected function getInvoiceTotals()
220
+ {
221
+ return array(
222
+ 'meta-invoice-amountinc' => $this->order['details']['BT']->order_total,
223
+ 'meta-invoice-vatamount' => $this->order['details']['BT']->order_billTaxAmount,
224
+ );
225
+ }
226
+
227
+ /**
228
+ * {@inheritdoc}
229
+ */
230
+ protected function getItemLines()
231
+ {
232
+ $result = array_map(array($this, 'getItemLine'), $this->order['items']);
233
+ return $result;
234
+ }
235
+
236
+ /**
237
+ * Returns 1 item line for 1 product line.
238
+ *
239
+ * @param stdClass $item
240
+ *
241
+ * @return array
242
+ */
243
+ protected function getItemLine(stdClass $item)
244
+ {
245
+ $productPriceEx = (float) $item->product_discountedPriceWithoutTax;
246
+ $productPriceInc = (float) $item->product_final_price;
247
+ $productVat = (float) $item->product_tax;
248
+
249
+ $calcRule = $this->getCalcRule('VatTax', $item->virtuemart_order_item_id);
250
+ if (!empty($calcRule->calc_value)) {
251
+ $vatInfo = array(
252
+ 'vatrate' => (float) $calcRule->calc_value,
253
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
254
+ );
255
+ } else {
256
+ $vatInfo = $this->getVatRangeTags($productVat, $productPriceEx, 0.0001, 0.0001);
257
+ }
258
+
259
+ $result = array(
260
+ 'itemnumber' => $item->order_item_sku,
261
+ 'product' => $item->order_item_name,
262
+ 'unitprice' => $productPriceEx,
263
+ 'unitpriceinc' => $productPriceInc,
264
+ 'quantity' => $item->product_quantity,
265
+ 'vatamount' => $productVat,
266
+ ) + $vatInfo;
267
+
268
+ return $result;
269
+ }
270
+
271
+ /**
272
+ * {@inheritdoc}
273
+ */
274
+ protected function getShippingLine()
275
+ {
276
+ $result = array();
277
+ // We are checking on empty, assuming that a null value will be used to
278
+ // indicate no shipping at all (downloadable product) and that free shipping
279
+ // will be represented as the string '0.00' which is not considered empty.
280
+ if (!empty($this->order['details']['BT']->order_shipment)) {
281
+ $shippingEx = (float) $this->order['details']['BT']->order_shipment;
282
+ $shippingVat = (float) $this->order['details']['BT']->order_shipment_tax;
283
+
284
+ $calcRule = $this->getCalcRule('shipment');
285
+ if (!empty($calcRule->calc_value)) {
286
+ $vatInfo = array(
287
+ 'vatrate' => (float) $calcRule->calc_value,
288
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
289
+ );
290
+ } else {
291
+ $vatInfo = $this->getVatRangeTags($shippingVat, $shippingEx, 0.0001, 0.01);
292
+ }
293
+
294
+ $result = array(
295
+ 'product' => $this->t('shipping_costs'),
296
+ 'unitprice' => $shippingEx,
297
+ 'quantity' => 1,
298
+ 'vatamount' => $shippingVat,
299
+ ) + $vatInfo;
300
+ }
301
+ return $result;
302
+ }
303
+
304
+ /**
305
+ * {@inheritdoc}
306
+ */
307
+ protected function getDiscountLines()
308
+ {
309
+ $result = array();
310
+
311
+ // We do have several discount related fields in the order details:
312
+ // - order_billDiscountAmount
313
+ // - order_discountAmount
314
+ // - coupon_discount
315
+ // - order_discount
316
+ // However, these fields seem to be totals based on applied non-tax
317
+ // calculation rules. So it is better to add a line per calc rule with a
318
+ // negative amount: this gives us descriptions of the discounts as well.
319
+ $result = array_merge($result, array_map(array($this, 'getCalcRuleDiscountLine'),
320
+ array_filter($this->order['calc_rules'], array($this, 'isDiscountCalcRule'))));
321
+
322
+ // Coupon codes are not stored in a calc rules, so handle them separately.
323
+ if (!Number::isZero($this->order['details']['BT']->coupon_discount)) {
324
+ $result[] = $this->getCouponCodeDiscountLine();
325
+ }
326
+
327
+ return $result;
328
+ }
329
+
330
+ /**
331
+ * Returns whether the calculation rule is a discount rule.
332
+ *
333
+ * @param \stdClass $calcRule
334
+ *
335
+ * @return bool
336
+ * True if the calculation rule is a discount rule.
337
+ */
338
+ protected function isDiscountCalcRule(stdClass $calcRule)
339
+ {
340
+ return $calcRule->calc_amount < 0.0
341
+ && !in_array($calcRule->calc_kind, array('VatTax', 'shipment', 'payment'));
342
+ }
343
+
344
+ /*
345
+ * Returns a discount item line for the discount calculation rule.
346
+ *
347
+ * The returned line will only contain a discount amount including tax.
348
+ * The completor will have to divide this amount over vat rates that are used
349
+ * in this invoice.
350
+ *
351
+ * @param \stdClass $calcRule
352
+ *
353
+ * @return array
354
+ * An item line for the invoice.
355
+ */
356
+ protected function getCalcRuleDiscountLine(stdClass $calcRule)
357
+ {
358
+ $result = array(
359
+ 'product' => $calcRule->calc_rule_name,
360
+ 'unitprice' => null,
361
+ 'unitpriceinc' => (float) $calcRule->calc_amount,
362
+ 'vatrate' => null,
363
+ 'quantity' => 1,
364
+ 'meta-vatrate-source' => static::VatRateSource_Strategy,
365
+ 'meta-strategy-split' => true,
366
+ );
367
+
368
+ return $result;
369
+ }
370
+
371
+ /**
372
+ * Returns an item line for the coupon code discount on this order.
373
+ *
374
+ * @return array
375
+ * An item line array.
376
+ */
377
+ protected function getCouponCodeDiscountLine()
378
+ {
379
+ // @todo: standardize coupon lines: use itemnumber?, use shop specific names?, etc.
380
+ $result = array(
381
+ 'itemnumber' => $this->order['details']['BT']->coupon_code,
382
+ 'product' => $this->t('discount'),
383
+ 'quantity' => 1,
384
+ 'unitpriceinc' => (float) $this->order['details']['BT']->coupon_discount,
385
+ 'vatrate' => null,
386
+ 'meta-vatrate-source' => static::VatRateSource_Strategy,
387
+ 'meta-strategy-split' => true,
388
+ );
389
+
390
+ return $result;
391
+ }
392
+
393
+ /**
394
+ * {@inheritdoc}
395
+ */
396
+ protected function getPaymentFeeLine()
397
+ {
398
+ $result = array();
399
+ if (!empty($this->order['details']['BT']->order_payment)) {
400
+ $paymentEx = (float) $this->order['details']['BT']->order_payment;
401
+ if (!Number::isZero($paymentEx)) {
402
+ $paymentVat = (float) $this->order['details']['BT']->order_payment_tax;
403
+
404
+ $calcRule = $this->getCalcRule('payment');
405
+ if (!empty($calcRule->calc_value)) {
406
+ $vatInfo = array(
407
+ 'vatrate' => (float) $calcRule->calc_value,
408
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
409
+ );
410
+ } else {
411
+ $vatInfo = $this->getVatRangeTags($paymentVat, $paymentEx, 0.0001, 0.01);
412
+ }
413
+
414
+ $result = array(
415
+ 'product' => $this->t('payment_costs'),
416
+ 'unitprice' => $paymentEx,
417
+ 'quantity' => 1,
418
+ 'vatamount' => $paymentVat,
419
+ ) + $vatInfo;
420
+ }
421
+ }
422
+ return $result;
423
+ }
424
+
425
+ /**
426
+ * Returns a calculation rule identified by the given reference
427
+ *
428
+ * @param string $calcKind
429
+ * The value for the kind of calc rule.
430
+ * @param int $orderItemId
431
+ * The value for the order item id, or 0 for special lines.
432
+ *
433
+ * @return null|object
434
+ * The (1st) calculation rule for the given reference, or null if none
435
+ * found.
436
+ */
437
+ protected function getCalcRule($calcKind, $orderItemId = 0)
438
+ {
439
+ foreach ($this->order['calc_rules'] as $calcRule) {
440
+ if ($calcRule->calc_kind == $calcKind) {
441
+ if (empty($orderItemId) || $calcRule->virtuemart_order_item_id == $orderItemId) {
442
+ return $calcRule;
443
+ }
444
+ }
445
+ }
446
+ return null;
447
+ }
448
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Invoice/Source.php ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\VirtueMart\Invoice;
3
+
4
+ use Siel\Acumulus\Invoice\Source as BaseSource;
5
+ use VmModel;
6
+
7
+ /**
8
+ * Wraps a VirtueMart order in an invoice source object.
9
+ */
10
+ class Source extends BaseSource
11
+ {
12
+ // More specifically typed properties.
13
+ /** @var array */
14
+ protected $source;
15
+
16
+ /**
17
+ * Loads an Order source for the set id.
18
+ */
19
+ protected function setSourceOrder()
20
+ {
21
+ /** @var \VirtueMartModelOrders $orders */
22
+ $orders = VmModel::getModel('orders');
23
+ $this->source = $orders->getOrder($this->id);
24
+ }
25
+
26
+ /**
27
+ * Sets the id based on the loaded Order.
28
+ */
29
+ protected function setIdOrder()
30
+ {
31
+ $this->id = $this->source['details']['BT']->virtuemart_order_id;
32
+ }
33
+
34
+ /**
35
+ * {@inheritdoc}
36
+ */
37
+ public function getReference()
38
+ {
39
+ return $this->source['details']['BT']->order_number;
40
+ }
41
+
42
+ /**
43
+ * {@inheritDoc}
44
+ *
45
+ * @return string
46
+ * A single character indicating the order status.
47
+ */
48
+ public function getStatus()
49
+ {
50
+ return $this->source['details']['BT']->order_status;
51
+ }
52
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Shop/ConfigForm.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\VirtueMart\Shop;
3
+
4
+ use JText;
5
+ use Siel\Acumulus\Joomla\Shop\ConfigForm as BaseConfigForm;
6
+ use Siel\Acumulus\Shop\ConfigInterface;
7
+ use VirtueMartModelOrderstatus;
8
+ use VmModel;
9
+
10
+ /**
11
+ * Class ConfigForm processes and builds the settings form page for the
12
+ * VirtueMart Acumulus module.
13
+ */
14
+ class ConfigForm extends BaseConfigForm
15
+ {
16
+ /**
17
+ * {@inheritdoc}
18
+ */
19
+ protected function getShopOrderStatuses()
20
+ {
21
+ /** @var VirtueMartModelOrderstatus $orderStatusModel */
22
+ $orderStatusModel = VmModel::getModel('orderstatus');
23
+ /** @var array[] $orderStates Method getOrderStatusNames() has an incorrect @return type ... */
24
+ $orderStates = $orderStatusModel->getOrderStatusNames();
25
+ foreach ($orderStates as $code => &$value) {
26
+ $value = \JText::_($value['order_status_name']);
27
+ }
28
+ return $orderStates;
29
+ }
30
+
31
+ /**
32
+ * {@inheritdoc}
33
+ */
34
+ protected function getTriggerInvoiceSendEventOptions()
35
+ {
36
+ $result = parent::getTriggerInvoiceSendEventOptions();
37
+ // @todo: find out if there's something like an invoice create event.
38
+ unset($result[ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate]);
39
+ return $result;
40
+ }
41
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Joomla/VirtueMart/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Joomla\VirtueMart\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\Invoice\Source as Source;
6
+ use Siel\Acumulus\Joomla\Shop\InvoiceManager as BaseInvoiceManager;
7
+
8
+ /**
9
+ * {@inheritdoc}
10
+ *
11
+ * This override provides the VirtueMart specific queries.
12
+ */
13
+ class InvoiceManager extends BaseInvoiceManager
14
+ {
15
+ /**
16
+ * {@inheritdoc}
17
+ *
18
+ * @todo: this override only returns order as supported invoice source type.
19
+ *
20
+ * Note: the VMInvoice extension seems to offer credit notes, but for now we
21
+ * do not support them.
22
+ */
23
+ public function getSupportedInvoiceSourceTypes()
24
+ {
25
+ return array(
26
+ Source::Order,
27
+ //Source::CreditNote,
28
+ );
29
+ }
30
+
31
+ /**
32
+ * {@inheritdoc}
33
+ */
34
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
35
+ {
36
+ if ($invoiceSourceType === Source::Order) {
37
+ $query = sprintf("select virtuemart_order_id
38
+ from #__virtuemart_orders
39
+ where virtuemart_order_id between %d and %d",
40
+ $InvoiceSourceIdFrom, $InvoiceSourceIdTo);
41
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
42
+ }
43
+ return array();
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ *
49
+ * By default, VirtueMart order numbers are non sequential random strings.
50
+ * So getting a range is not logical. However, extensions exists that do
51
+ * introduce sequential order numbers, E.g:
52
+ * http://extensions.joomla.org/profile/extension/extension-specific/virtuemart-extensions/human-readable-order-numbers
53
+ */
54
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
55
+ {
56
+ if ($invoiceSourceType === Source::Order) {
57
+ $query = sprintf("select virtuemart_order_id
58
+ from #__virtuemart_orders
59
+ where order_number between '%s' and '%s'",
60
+ $this->getDb()->escape($InvoiceSourceReferenceFrom),
61
+ $this->getDb()->escape($InvoiceSourceReferenceTo)
62
+ );
63
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
64
+ }
65
+ return array();
66
+ }
67
+
68
+ /**
69
+ * {@inheritdoc}
70
+ */
71
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
72
+ {
73
+ if ($invoiceSourceType === Source::Order) {
74
+ $dateFrom = $this->getSqlDate($dateFrom);
75
+ $dateTo = $this->getSqlDate($dateTo);
76
+ $query = sprintf("select virtuemart_order_id
77
+ from #__virtuemart_orders
78
+ where modified_on between '%s' and '%s'",
79
+ $this->toSql($dateFrom), $this->toSql($dateTo));
80
+ return $this->getSourcesByQuery($invoiceSourceType, $query);
81
+ }
82
+ return array();
83
+ }
84
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/FormMapper.php ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Helpers;
3
+
4
+ use Mage;
5
+ use Varien_Data_Form_Abstract;
6
+
7
+ /**
8
+ * Class FormMapper maps an Acumulus form definition to a Magento form
9
+ * definition.
10
+ */
11
+ class FormMapper
12
+ {
13
+ /** @var bool */
14
+ protected $hasRadios = false;
15
+
16
+ /**
17
+ * Maps a set of field definitions.
18
+ *
19
+ * @param \Varien_Data_Form_Abstract $form
20
+ * @param array[] $fields
21
+ */
22
+ public function map(Varien_Data_Form_Abstract $form, array $fields)
23
+ {
24
+ $this->fields($form, $fields);
25
+ if ($this->hasRadios) {
26
+ $form->addField('radio-styling',
27
+ 'note',
28
+ array('text' => '<style> input[type=radio] { float: left; clear: both; margin-top: 0.2em;} .value label.inline {float: left !important; max-width: 95%; padding-left: 1em;} .note {clear: both;}</style>'),
29
+ '^');
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Maps a set of field definitions.
35
+ *
36
+ * @param \Varien_Data_Form_Abstract $parent
37
+ * @param array[] $fields
38
+ */
39
+ public function fields($parent, array $fields)
40
+ {
41
+ foreach ($fields as $id => $field) {
42
+ if (!isset($field['id'])) {
43
+ $field['id'] = $id;
44
+ }
45
+ if (!isset($field['name'])) {
46
+ $field['name'] = $id;
47
+ }
48
+ $this->field($parent, $field);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Maps a single field definition.
54
+ *
55
+ * @param \Varien_Data_Form_Abstract $parent
56
+ * @param array $field
57
+ */
58
+ public function field($parent, array $field)
59
+ {
60
+ if (!isset($field['attributes'])) {
61
+ $field['attributes'] = array();
62
+ }
63
+ $element = $parent->addField($field['id'], $this->getMagentoType($field), $this->getMagentoElementSettings($field));
64
+ if ($field['type'] === 'fieldset') {
65
+ $this->fields($element, $field['fields']);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Returns the Magento form element type for the given Acumulus type string.
71
+ *
72
+ * @param array $field
73
+ *
74
+ * @return string
75
+ */
76
+ protected function getMagentoType(array $field)
77
+ {
78
+ switch ($field['type']) {
79
+ case 'email':
80
+ $type = 'text';
81
+ break;
82
+ case 'markup':
83
+ $type = 'note';
84
+ break;
85
+ case 'radio':
86
+ $type = 'radios';
87
+ $this->hasRadios = true;
88
+ break;
89
+ case 'checkbox':
90
+ $type = 'checkboxes';
91
+ break;
92
+ case 'select':
93
+ $type = empty($field['attributes']['multiple']) ? 'select' : 'multiselect';
94
+ break;
95
+ default:
96
+ $type = $field['type'];
97
+ break;
98
+ }
99
+ return $type;
100
+ }
101
+
102
+ /**
103
+ * Returns the Magento form element settings.
104
+ *
105
+ * @param array $field
106
+ * The Acumulus field settings.
107
+ *
108
+ * @return array
109
+ * The Magento form element settings.
110
+ */
111
+ protected function getMagentoElementSettings(array $field)
112
+ {
113
+ $config = array();
114
+
115
+ foreach ($field as $key => $value) {
116
+ $config += $this->getMagentoProperty($key, $value, $field['type']);
117
+ }
118
+
119
+ return $config;
120
+ }
121
+
122
+ /**
123
+ * Converts an Acumulus settings to a Magento setting.
124
+ *
125
+ * @param string $key
126
+ * The name of the setting to convert.
127
+ * @param mixed $value
128
+ * The value for the setting to convert.
129
+ * @param string $type
130
+ * The Acumulus field type.
131
+ *
132
+ * @return array
133
+ * The Magento setting. This will typically contain 1 element, but in some
134
+ * cases, 1 Acumulus field setting may result in multiple Magento settings.
135
+ */
136
+ protected function getMagentoProperty($key, $value, $type)
137
+ {
138
+ switch ($key) {
139
+ // Fields to ignore:
140
+ case 'type':
141
+ $result = array();
142
+ if ($value === 'date') {
143
+ $result['image'] = Mage::getDesign()->getSkinUrl('images/grid-cal.gif');
144
+ }
145
+ break;
146
+ case 'id':
147
+ case 'fields':
148
+ $result = array();
149
+ break;
150
+ // Fields to return unchanged:
151
+ case 'legend':
152
+ case 'label':
153
+ case 'format':
154
+ $result = array($key => $value);
155
+ break;
156
+ case 'name':
157
+ if ($type === 'checkbox') {
158
+ // Make it an array for PHP POST processing, in case there are
159
+ // multiple checkboxes.
160
+ $value .= '[]';
161
+ }
162
+ $result = array($key => $value);
163
+ break;
164
+ case 'description':
165
+ $result = array('after_element_html' => '<p class="note">' . $value . '</p>');
166
+ break;
167
+ case 'value':
168
+ if ($type === 'markup') {
169
+ $result = array('text' => $value);
170
+ } else { // $type === 'hidden'
171
+ $result = array('value' => $value);
172
+ }
173
+ break;
174
+ case 'attributes':
175
+ // In magento you add pure html attributes at the same level as the
176
+ // "field attributes" that are for Magento.
177
+ $result = $value;
178
+ if (!empty($value['required'])) {
179
+ if (isset($result['class'])) {
180
+ $result['class'] .= ' ';
181
+ } else {
182
+ $result['class'] = '';
183
+ }
184
+ if ($type === 'radio') {
185
+ unset($result['required']);
186
+ $result['class'] .= 'validate-one-required-by-name';
187
+ } else {
188
+ $result['class'] .= 'required-entry';
189
+ }
190
+ }
191
+ break;
192
+ case 'options':
193
+ $result = array('values' => $this->getMagentoOptions($value));
194
+ break;
195
+ default:
196
+ Log::getInstance()->warning(__METHOD__ . "Unknown key '$key'");
197
+ $result = array($key => $value);
198
+ break;
199
+ }
200
+ return $result;
201
+ }
202
+
203
+ /**
204
+ * Converts a list of Acumulus field options to a list of Magento options.
205
+ *
206
+ * @param array $options
207
+ *
208
+ * @return array
209
+ * A list of Magento form element options.
210
+ */
211
+ protected function getMagentoOptions(array $options)
212
+ {
213
+ $result = array();
214
+ foreach ($options as $value => $label) {
215
+ $result[] = array(
216
+ 'value' => $value,
217
+ 'label' => $label,
218
+ );
219
+ }
220
+ return $result;
221
+ }
222
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/Log.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Helpers;
3
+
4
+ use Mage;
5
+ use Siel\Acumulus\Helpers\Log as BaseLog;
6
+ use Zend_Log;
7
+
8
+ /**
9
+ * Extends the base log class to log any library logging to the Magento log.
10
+ */
11
+ class Log extends BaseLog
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ *
16
+ * This override uses Mage::log().
17
+ */
18
+ protected function write($message, $severity)
19
+ {
20
+ Mage::log($message, $this->getMagentoSeverity($severity));
21
+ }
22
+
23
+ /**
24
+ * Returns the Magento equivalent of the severity.
25
+ *
26
+ * @param int $severity
27
+ * One of the constants of the base Log class.
28
+ *
29
+ * @return int
30
+ * The Magento equivalent of the severity.
31
+ */
32
+ protected function getMagentoSeverity($severity)
33
+ {
34
+ switch ($severity) {
35
+ case Log::Error:
36
+ return Zend_Log::ERR;
37
+ case Log::Warning:
38
+ return Zend_Log::WARN;
39
+ case Log::Notice:
40
+ return Zend_Log::NOTICE;
41
+ case Log::Debug:
42
+ default:
43
+ return Zend_Log::DEBUG;
44
+ }
45
+ }
46
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Helpers/Mailer.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Helpers;
3
+
4
+ use Mage;
5
+ use Mage_Core_Model_Email_Template;
6
+ use Siel\Acumulus\Helpers\Mailer as BaseMailer;
7
+
8
+ /**
9
+ * Extends the base mailer class to send a mail using the Magento mail features.
10
+ */
11
+ class Mailer extends BaseMailer
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
17
+ {
18
+ /** @var Mage_Core_Model_Email_Template $emailTemplate */
19
+ $emailTemplate = Mage::getModel('core/email_template');
20
+ $emailTemplate->setSenderEmail(Mage::getStoreConfig('trans_email/ident_general/email'));
21
+ $emailTemplate->setSenderName($this->getFromName());
22
+ $emailTemplate->setTemplateSubject($this->getSubject($result));
23
+ $body = $this->getBody($result, $messages, $invoiceSourceType, $invoiceSourceReference);
24
+ $emailTemplate->setTemplateText($body['html']);
25
+ return $emailTemplate->send($this->getToAddress(), Mage::getStoreConfig('trans_email/ident_general/name'));
26
+ }
27
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Completor.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\Completor as BaseCompletor;
6
+
7
+ /**
8
+ * Class Completor
9
+ */
10
+ class Completor extends BaseCompletor
11
+ {
12
+ /**
13
+ * {@inheritdoc}
14
+ *
15
+ * !Magento bug!
16
+ * In credit memos, the discount amount from discount lines may differ from
17
+ * the summed discount amounts per line. This occurs when because refunded
18
+ * shipping costs do not advertise any discount amount.
19
+ *
20
+ * So, if the comparison fails, we correct the discount amount on the shipping
21
+ * line so that SplitKnownDiscountLine::checkPreconditions() will pass.
22
+ */
23
+ protected function completeLineTotals()
24
+ {
25
+ parent::completeLineTotals();
26
+
27
+ if ($this->source->getType() === Source::CreditNote) {
28
+ $discountAmountInc = 0.0;
29
+ $discountLineAmountInc = 0.0;
30
+
31
+ $invoiceLines = $this->invoice['customer']['invoice']['line'];
32
+ foreach ($invoiceLines as $line) {
33
+ if (isset($line['meta-line-discount-amountinc'])) {
34
+ $discountAmountInc += $line['meta-line-discount-amountinc'];
35
+ }
36
+
37
+ if ($line['meta-line-type'] === Creator::LineType_Discount) {
38
+ if (isset($line['meta-line-priceinc'])) {
39
+ $discountLineAmountInc += $line['meta-line-priceinc'];
40
+ } else if (isset($line['unitpriceinc'])) {
41
+ $discountLineAmountInc += $line['quantity'] * $line['unitpriceinc'];
42
+ }
43
+ }
44
+ }
45
+
46
+ if (!Number::floatsAreEqual($discountAmountInc, $discountLineAmountInc)) {
47
+ foreach ($invoiceLines as $line) {
48
+ if ($line['meta-line-type'] === Creator::LineType_Shipping && isset($line['meta-line-discount-amountinc'])) {
49
+ $line['meta-line-discount-amountinc'] += $discountLineAmountInc - $discountAmountInc;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Creator.php ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Invoice;
3
+
4
+ use Mage;
5
+ use Mage_Core_Model_Resource_Db_Collection_Abstract;
6
+ use Mage_Customer_Model_Customer;
7
+ use Mage_Sales_Model_Order;
8
+ use Mage_Sales_Model_Order_Creditmemo;
9
+ use Mage_Sales_Model_Order_Creditmemo_Item;
10
+ use Mage_Sales_Model_Order_Invoice;
11
+ use Mage_Sales_Model_Order_Item;
12
+ use Mage_Tax_Model_Config;
13
+ use Siel\Acumulus\Helpers\Number;
14
+ use Siel\Acumulus\Invoice\ConfigInterface;
15
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
16
+
17
+ /**
18
+ * Allows to create arrays in the Acumulus invoice structure from a Magento
19
+ * order or credit memo.
20
+ *
21
+ * @todo: multi currency: use base values (default store currency) or values
22
+ * without base in their names (selected store currency). Other fields
23
+ * involved:
24
+ * - base_currency_code
25
+ * - store_to_base_rate
26
+ * - store_to_order_rate
27
+ * - order_currency_code
28
+ */
29
+ class Creator extends BaseCreator
30
+ {
31
+ /** @var Mage_Sales_Model_Order */
32
+ protected $order;
33
+
34
+ /** @var Mage_Sales_Model_Order_Creditmemo */
35
+ protected $creditNote;
36
+
37
+ /** @var Mage_Core_Model_Resource_Db_Collection_Abstract */
38
+ protected $shopInvoices;
39
+
40
+ /** @var Mage_Sales_Model_Order_Invoice */
41
+ protected $shopInvoice;
42
+
43
+ /**
44
+ * {@inheritdoc}
45
+ *
46
+ * This override also initializes Magento specific properties related to the
47
+ * source.
48
+ */
49
+ protected function setInvoiceSource($source)
50
+ {
51
+ parent::setInvoiceSource($source);
52
+ switch ($this->invoiceSource->getType()) {
53
+ case Source::Order:
54
+ $this->order = $this->invoiceSource->getSource();
55
+ $this->creditNote = null;
56
+ break;
57
+ case Source::CreditNote:
58
+ $this->creditNote = $this->invoiceSource->getSource();
59
+ $this->order = $this->creditNote->getOrder();
60
+ break;
61
+ }
62
+ $this->shopInvoices = $this->order->getInvoiceCollection();
63
+ $this->shopInvoice = count($this->shopInvoices) > 0 ? $this->shopInvoices->getFirstItem() : null;
64
+ }
65
+
66
+ /**
67
+ * {@inheritdoc}
68
+ */
69
+ protected function getCustomer()
70
+ {
71
+ $result = array();
72
+
73
+ /** @var Mage_Sales_Model_Order|Mage_Sales_Model_Order_Creditmemo $order */
74
+ $order = $this->creditNote !== null ? $this->creditNote : $this->order;
75
+
76
+ if ($order->getCustomerId()) {
77
+ /** @var Mage_Customer_Model_Customer $customer */
78
+ $customer = Mage::getModel('customer/customer')->load($order->getCustomerId());
79
+ $result['contactyourid'] = $customer->getId();
80
+ /** @noinspection PhpUndefinedMethodInspection */
81
+ $this->addIfNotEmpty($result, 'contactyourid', $customer->getIncrementId());
82
+ }
83
+
84
+ $invoiceAddress = $order->getBillingAddress();
85
+ $this->addEmpty($result, 'companyname1', $invoiceAddress->getCompany());
86
+ $result['fullname'] = $invoiceAddress->getFirstname() . ' ' . $invoiceAddress->getLastname();
87
+ $this->addEmpty($result, 'address1', $invoiceAddress->getStreet(1));
88
+ $this->addEmpty($result, 'address2', $invoiceAddress->getStreet(2));
89
+ $this->addEmpty($result, 'postalcode', $invoiceAddress->getPostcode());
90
+ $this->addEmpty($result, 'city', $invoiceAddress->getCity());
91
+ if ($invoiceAddress->getCountryId()) {
92
+ $result['countrycode'] = $invoiceAddress->getCountry();
93
+ }
94
+ // Magento has 2 VAT numbers:
95
+ // http://magento.stackexchange.com/questions/42164/there-are-2-vat-fields-in-onepage-checkout-which-one-should-i-be-using
96
+ $this->addIfNotEmpty($result, 'vatnumber', $order->getCustomerTaxvat());
97
+ /** @noinspection PhpUndefinedMethodInspection */
98
+ $this->addIfNotEmpty($result, 'vatnumber', $invoiceAddress->getVatId());
99
+ $this->addIfNotEmpty($result, 'telephone', $invoiceAddress->getTelephone());
100
+ $this->addIfNotEmpty($result, 'fax', $invoiceAddress->getFax());
101
+ $result['email'] = $invoiceAddress->getEmail();
102
+
103
+ return $result;
104
+ }
105
+
106
+ /**
107
+ * {@inheritdoc}
108
+ */
109
+ protected function searchProperty($property)
110
+ {
111
+ $invoiceAddress = $this->invoiceSource->getSource()->getBillingAddress();
112
+ $value = $this->getProperty($property, $invoiceAddress);
113
+ if (empty($value)) {
114
+ $customer = Mage::getModel('customer/customer')->load($this->invoiceSource->getSource()->getCustomerId());
115
+ $value = $this->getProperty($property, $customer);
116
+ }
117
+ if (empty($value)) {
118
+ $value = parent::searchProperty($property);
119
+ }
120
+ return $value;
121
+ }
122
+
123
+ /**
124
+ * {@inheritdoc}
125
+ */
126
+ protected function getInvoiceNumber($invoiceNumberSource)
127
+ {
128
+ $result = $this->invoiceSource->getReference();
129
+ if ($invoiceNumberSource == ConfigInterface::InvoiceNrSource_ShopInvoice && $this->invoiceSource->getType() === Source::Order && $this->shopInvoice !== null) {
130
+ $result = $this->shopInvoice->getIncrementId();
131
+ }
132
+ return $result;
133
+ }
134
+
135
+ /**
136
+ * {@inheritdoc}
137
+ */
138
+ protected function getInvoiceDate($dateToUse)
139
+ {
140
+ // createdAt returns yyyy-mm-dd hh:mm:ss, take date part.
141
+ $result = substr($this->invoiceSource->getSource()->getCreatedAt(), 0, strlen('yyyy-mm-dd'));
142
+ // A credit note is to be considered an invoice on its own.
143
+ if ($dateToUse == ConfigInterface::InvoiceDate_InvoiceCreate && $this->invoiceSource->getType() === Source::Order && $this->shopInvoice !== null) {
144
+ $result = substr($this->shopInvoice->getCreatedAt(), 0, strlen('yyyy-mm-dd'));
145
+ }
146
+ return $result;
147
+ }
148
+
149
+ protected function getPaymentStateOrder()
150
+ {
151
+ return Number::isZero($this->order->getBaseTotalDue())
152
+ ? ConfigInterface::PaymentStatus_Paid
153
+ : ConfigInterface::PaymentStatus_Due;
154
+ }
155
+
156
+ protected function getPaymentStateCreditNote()
157
+ {
158
+ return $this->creditNote->getState() == Mage_Sales_Model_Order_Creditmemo::STATE_REFUNDED
159
+ ? ConfigInterface::PaymentStatus_Paid
160
+ : ConfigInterface::PaymentStatus_Due;
161
+ }
162
+
163
+ protected function getPaymentDateOrder()
164
+ {
165
+ // Take date of last payment as payment date.
166
+ $paymentDate = null;
167
+ foreach ($this->order->getStatusHistoryCollection() as $statusChange) {
168
+ /** @var \Mage_Sales_Model_Order_Status_History $statusChange */
169
+ if (!$paymentDate || $this->isPaidStatus($statusChange->getStatus())) {
170
+ $createdAt = substr($statusChange->getCreatedAt(), 0, strlen('yyyy-mm-dd'));
171
+ if (!$paymentDate || $createdAt < $paymentDate) {
172
+ $paymentDate = $createdAt;
173
+ }
174
+ }
175
+ }
176
+ return $paymentDate;
177
+ }
178
+
179
+ protected function isPaidStatus($status)
180
+ {
181
+ return in_array($status, array('processing', 'closed', 'complete'));
182
+ }
183
+
184
+ protected function getPaymentDateCreditNote()
185
+ {
186
+ return substr($this->creditNote->getCreatedAt(), 0, strlen('yyyy-mm-dd'));
187
+ }
188
+
189
+ /**
190
+ * {@inheritdoc}
191
+ *
192
+ * This override provides the values meta-invoice-amountinc and
193
+ * meta-invoice-vatamount.
194
+ */
195
+ protected function getInvoiceTotals()
196
+ {
197
+ $sign = $this->invoiceSource->getType() === Source::CreditNote ? -1.0 : 1.0;
198
+ return array(
199
+ 'meta-invoice-amountinc' => $sign * $this->invoiceSource->getSource()->getBaseGrandTotal(),
200
+ 'meta-invoice-vatamount' => $sign * $this->invoiceSource->getSource()->getBaseTaxAmount(),
201
+ );
202
+ }
203
+
204
+ /**
205
+ * {@inheritdoc}
206
+ */
207
+ protected function getItemLinesOrder()
208
+ {
209
+ $result = array();
210
+ // Items may be composed, so start with all "visible" items.
211
+ foreach ($this->order->getAllVisibleItems() as $item) {
212
+ $result = array_merge($result, $this->getItemLineOrder($item));
213
+ }
214
+ return $result;
215
+ }
216
+
217
+ /**
218
+ * {@inheritdoc}
219
+ */
220
+ protected function getItemLinesCreditNote()
221
+ {
222
+ $result = array();
223
+ // Items may be composed, so start with all "visible" items.
224
+ foreach ($this->creditNote->getAllItems() as $item) {
225
+ // Only items for which row total is set, are refunded
226
+ /** @var Mage_Sales_Model_Order_Creditmemo_Item $item */
227
+ if ((float) $item->getRowTotal() > 0.0) {
228
+ $result[] = $this->getItemLineCreditNote($item);
229
+ }
230
+ }
231
+ return $result;
232
+ }
233
+
234
+ /**
235
+ * Returns 1 or more item lines for 1 main product line.
236
+ *
237
+ * @param Mage_Sales_Model_Order_Item $item
238
+ *
239
+ * @return array
240
+ */
241
+ protected function getItemLineOrder(Mage_Sales_Model_Order_Item $item)
242
+ {
243
+ $result = array();
244
+ $childLines = array();
245
+
246
+ $vatRate = (float) $item->getTaxPercent();
247
+ $productPriceInc = (float) $item->getPriceInclTax();
248
+ // For higher precision of the unit price, we use the prices as entered by
249
+ // the admin.
250
+ $productPriceEx = $this->productPricesIncludeTax() ? (float) $productPriceInc / (100.0 + $vatRate) * 100.0 : (float) $item->getPrice();
251
+ // Tax amount = VAT over discounted product price.
252
+ // Hidden tax amount = VAT over discount.
253
+ // But as discounts get their own lines and the product lines are showing
254
+ // the normal (not discounted) price we add these 2.
255
+ $lineVat = (float) $item->getTaxAmount() + (float) $item->getHiddenTaxAmount();
256
+
257
+ // Simple products (products without children): add as 1 line.
258
+ $this->addIfNotEmpty($result, 'itemnumber', $item->getSku());
259
+ $result += array(
260
+ 'product' => $item->getName(),
261
+ 'unitprice' => $productPriceEx,
262
+ 'unitpriceinc' => $productPriceInc,
263
+ 'vatrate' => $vatRate,
264
+ 'meta-line-vatamount' => $lineVat,
265
+ 'quantity' => $item->getQtyOrdered(),
266
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
267
+ );
268
+ if (!Number::isZero($item->getDiscountAmount())) {
269
+ // Store discount on this item to be able to get correct discount lines.
270
+ $result['meta-line-discount-amountinc'] = -$item->getDiscountAmount();
271
+ }
272
+
273
+ // Also add child lines for composed products, a.o. to be able to print a
274
+ // packing slip in Acumulus.
275
+ foreach ($item->getChildrenItems() as $child) {
276
+ $childLine = $this->getItemLineOrder($child);
277
+ $childLines = array_merge($childLines, $childLine);
278
+ }
279
+
280
+ // If:
281
+ // - there is exactly 1 child line
282
+ // - for the same item number, and quantity
283
+ // - with no price info on the child
284
+ // We seem to be processing a configurable product that for some reason
285
+ // appears twice: do not add the child, but copy the product description to
286
+ // the result as it contains more option descriptions.
287
+ // @todo: refine this: when to add just 1 line, when multiple lines (see OC)
288
+ if (count($childLines) === 1
289
+ && $result['itemnumber'] === $childLines[0]['itemnumber']
290
+ && $childLines[0]['unitprice'] == 0
291
+ && $result['quantity'] === $childLines[0]['quantity']
292
+ ) {
293
+ $result['product'] = $childLines[0]['product'];
294
+ $childLines = array();
295
+ }
296
+ // keep price info on bundle level or child level?
297
+ if (count($childLines) > 0) {
298
+ if ($item->getPriceInclTax() > 0.0 && ($item->getTaxPercent() > 0 || $item->getTaxAmount() == 0.0)) {
299
+ // If the bundle line contains valid price and tax info, we remove that
300
+ // info from all child lines (to prevent accounting amounts twice).
301
+ foreach ($childLines as &$childLine) {
302
+ $childLine['unitprice'] = 0;
303
+ $childLine['vatrate'] = $result['vatrate'];
304
+ }
305
+ } else {
306
+ // Do all children have the same vat?
307
+ $vatRate = null;
308
+ foreach ($childLines as $childLine) {
309
+ // Check if this is not an empty price/vat line.
310
+ if ($childLine['unitprice'] != 0 && $childLine['vatrate'] !== -1) {
311
+ // Same vat?
312
+ if ($vatRate === null || $childLine['vatrate'] === $vatRate) {
313
+ $vatRate = $childLine['vatrate'];
314
+ } else {
315
+ $vatRate = null;
316
+ break;
317
+ }
318
+ }
319
+ }
320
+
321
+ if ($vatRate !== null && $vatRate == $result['vatrate'] && $productPriceEx != 0.0) {
322
+ // Bundle has price info and same vat as ALL children: use price and
323
+ // vat info from bundle line and remove it from child lines to prevent
324
+ // accounting amounts twice.
325
+ foreach ($childLines as &$childLine) {
326
+ $childLine['unitprice'] = 0;
327
+ $childLine['vatrate'] = $result['vatrate'];
328
+ }
329
+ } else {
330
+ // All price and vat info is/remains on the child lines.
331
+ // Make sure no price and vat info is left on the bundle line.
332
+ $result['unitprice'] = 0;
333
+ $result['vatrate'] = -1;
334
+ }
335
+ }
336
+ }
337
+
338
+ $result = array_merge(array($result), $childLines);
339
+ return $result;
340
+ }
341
+
342
+ /**
343
+ * Returns 1 item line for 1 credit line.
344
+ *
345
+ * @param Mage_Sales_Model_Order_Creditmemo_Item $item
346
+ *
347
+ * @return array
348
+ */
349
+ protected function getItemLineCreditNote(Mage_Sales_Model_Order_Creditmemo_Item $item)
350
+ {
351
+ $result = array();
352
+
353
+ $lineVat = -((float) $item->getTaxAmount() + (float) $item->getHiddenTaxAmount());
354
+ $productPriceEx = -((float) $item->getPrice());
355
+
356
+ // On a credit note we only have single lines, no compound lines.
357
+ $this->addIfNotEmpty($result, 'itemnumber', $item->getSku());
358
+ $result += array(
359
+ 'product' => $item->getName(),
360
+ 'unitprice' => $productPriceEx,
361
+ 'quantity' => $item->getQty(),
362
+ 'meta-line-vatamount' => $lineVat,
363
+ );
364
+
365
+ if ($this->productPricesIncludeTax()) {
366
+ $productPriceInc = -((float) $item->getPriceInclTax());
367
+ $result['unitpriceinc'] = $productPriceInc;
368
+ }
369
+
370
+ $orderItemId = $item->getOrderItemId();
371
+ if (!empty($orderItemId)) {
372
+ $orderItem = $item->getOrderItem();
373
+ $result += array(
374
+ 'vatrate' => $orderItem->getTaxPercent(),
375
+ 'meta-vatrate-source' => static::VatRateSource_Exact,
376
+ );
377
+ } else {
378
+ $result += $this->getVatRangeTags($lineVat / $item->getQty(), $productPriceEx, 0.02, 0.02);
379
+ $result['meta-calculated-fields'] = 'vatamount';
380
+ }
381
+
382
+ if (!Number::isZero($item->getDiscountAmount())) {
383
+ // Credit note: discounts are cancelled, thus amount is positive.
384
+ $result['meta-line-discount-amountinc'] = $item->getDiscountAmount();
385
+ }
386
+
387
+ return $result;
388
+ }
389
+
390
+ /**
391
+ * {@inheritdoc}
392
+ */
393
+ protected function getShippingLine()
394
+ {
395
+ $shippingDescription = $this->order->getShippingDescription();
396
+ $result = array(
397
+ 'itemnumber' => '',
398
+ 'product' => !empty($shippingDescription) ? $shippingDescription : $this->t('shipping_costs'),
399
+ 'quantity' => 1,
400
+ );
401
+
402
+ // What do the following methods return:
403
+ // - getShippingAmount(): shipping costs excl VAT excl any discount
404
+ // - getShippingInclTax(): shipping costs incl VAT excl any discount
405
+ // - getShippingTaxAmount(): VAT on shipping costs incl discount
406
+ // - getShippingDiscountAmount(): discount on shipping incl VAT
407
+ /** @var Mage_Sales_Model_Order|Mage_Sales_Model_Order_Creditmemo $magentoSource */
408
+ $magentoSource = $this->invoiceSource->getSource();
409
+ if (!Number::isZero($magentoSource->getShippingAmount())) {
410
+ // We have 2 ways of calculating the vat rate: first one is based on tax
411
+ // amount and normal shipping costs corrected with any discount (as the
412
+ // tax amount is including any discount):
413
+ // $vatRate1 = $magentoSource->getShippingTaxAmount() / ($magentoSource->getShippingInclTax() - $magentoSource->getShippingDiscountAmount() - $magentoSource->getShippingTaxAmount());
414
+ // However, we will use the 2nd way as that seems to be more precise and,
415
+ // thus generally leads to a smaller range:
416
+ // Get range based on normal shipping costs incl and excl VAT.
417
+ $sign = $this->getSign();
418
+ $shippingInc = $sign * $magentoSource->getShippingInclTax();
419
+ $shippingEx = $sign * $magentoSource->getShippingAmount();
420
+ $shippingVat = $shippingInc - $shippingEx;
421
+ $result += array(
422
+ 'unitprice' => $shippingEx,
423
+ 'unitpriceinc' => $shippingInc,
424
+ ) + $this->getVatRangeTags($shippingVat, $shippingEx, 0.02, 0.01);
425
+ $result['meta-calculated-fields'] = 'vatamount';
426
+
427
+ // getShippingDiscountAmount() only exists on Orders.
428
+ if ($this->invoiceSource->getType() === Source::Order && !Number::isZero($magentoSource->getShippingDiscountAmount())) {
429
+ $result['meta-line-discount-amountinc'] = -$sign * $magentoSource->getShippingDiscountAmount();
430
+ } else if ($this->invoiceSource->getType() === Source::CreditNote && !Number::floatsAreEqual($shippingVat, $magentoSource->getShippingTaxAmount(), 0.02)) {
431
+ // On credit notes, the shipping discount amount is not stored but can
432
+ // be deduced via the shipping discount tax amount and the shipping vat
433
+ // rate. To get a more precise 'meta-line-discount-amountinc', we
434
+ // compute that in the completor when we have corrected the vatrate.
435
+ $result['meta-line-discount-vatamount'] = $sign * ($shippingVat - $sign * $magentoSource->getShippingTaxAmount());
436
+ }
437
+ }
438
+
439
+ // Only add a free shipping line on an order, not on a credit note: free
440
+ // shipping is never refunded...
441
+ else if ($this->invoiceSource->getType() === Source::Order) {
442
+ // Free shipping should get a "normal" tax rate. We leave that to the
443
+ // completor to determine.
444
+ $result += array(
445
+ 'unitprice' => 0,
446
+ 'vatrate' => null,
447
+ 'meta-vatrate-source' => static::VatRateSource_Completor,
448
+ );
449
+ }
450
+ return $result;
451
+ }
452
+
453
+ /**
454
+ * {@inheritdoc}
455
+ */
456
+ protected function getDiscountLines()
457
+ {
458
+ $result = array();
459
+ if (!Number::isZero($this->invoiceSource->getSource()->getDiscountAmount())) {
460
+ $line = array(
461
+ 'itemnumber' => '',
462
+ 'product' => $this->getDiscountDescription(),
463
+ 'vatrate' => null,
464
+ 'meta-vatrate-source' => static::VatRateSource_Strategy,
465
+ 'meta-strategy-split' => true,
466
+ 'quantity' => 1,
467
+ );
468
+ // Product prices incl. VAT => discount amount is also incl. VAT
469
+ if ($this->productPricesIncludeTax()) {
470
+ $line['unitpriceinc'] = $this->getSign() * $this->invoiceSource->getSource()->getDiscountAmount();
471
+ } else {
472
+ $line['unitprice'] = $this->getSign() * $this->invoiceSource->getSource()->getDiscountAmount();
473
+ }
474
+ $result[] = $line;
475
+ }
476
+ return $result;
477
+ }
478
+
479
+ /**
480
+ * {@inheritdoc}
481
+ *
482
+ * This implementation may return a manual line for a credit memo.
483
+ */
484
+ protected function getManualLines()
485
+ {
486
+ $result = array();
487
+
488
+ if (isset($this->creditNote) && !Number::isZero($this->creditNote->getAdjustment())) {
489
+ $line = array(
490
+ 'product' => $this->t('refund_adjustment'),
491
+ 'unitprice' => -$this->creditNote->getAdjustment(),
492
+ 'quantity' => 1,
493
+ 'vatrate' => 0,
494
+ );
495
+ $result[] = $line;
496
+ }
497
+ return $result;
498
+ }
499
+
500
+ /**
501
+ * @return string
502
+ */
503
+ protected function getDiscountDescription()
504
+ {
505
+ if ($this->order->getDiscountDescription()) {
506
+ $description = $this->t('discount_code') . ' ' . $this->order->getDiscountDescription();
507
+ } else if ($this->order->getCouponCode()) {
508
+ $description = $this->t('discount_code') . ' ' . $this->order->getCouponCode();
509
+ } else {
510
+ $description = $this->t('discount');
511
+ }
512
+ return $description;
513
+ }
514
+
515
+ /**
516
+ * Returns if the prices for the products are entered with or without tax.
517
+ *
518
+ * @return bool
519
+ * Whether the prices for the products are entered with or without tax.
520
+ */
521
+ protected function productPricesIncludeTax()
522
+ {
523
+ /** @var Mage_Tax_Model_Config $taxConfig */
524
+ $taxConfig = Mage::getModel('tax/config');
525
+ return $taxConfig->priceIncludesTax();
526
+ }
527
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Invoice/Source.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Invoice;
3
+
4
+ use Mage;
5
+ use Mage_Sales_Model_Order;
6
+ use Mage_Sales_Model_Order_Creditmemo;
7
+ use Siel\Acumulus\Invoice\Source as BaseSource;
8
+
9
+ /**
10
+ * Wraps a Magento order or credit memo in an invoice source object.
11
+ */
12
+ class Source extends BaseSource
13
+ {
14
+ // More specifically typed properties.
15
+ /** @var Mage_Sales_Model_Order|Mage_Sales_Model_Order_Creditmemo */
16
+ protected $source;
17
+
18
+ /**
19
+ * Loads an Order source for the set id.
20
+ */
21
+ protected function setSourceOrder()
22
+ {
23
+ $this->source = Mage::getModel('sales/order');
24
+ $this->source->load($this->id);
25
+ }
26
+
27
+ /**
28
+ * Loads a Credit memo source for the set id.
29
+ */
30
+ protected function setSourceCreditNote()
31
+ {
32
+ $this->source = Mage::getModel('sales/order_creditmemo');
33
+ $this->source->load($this->id);
34
+ }
35
+
36
+ /**
37
+ * {@inheritdoc}
38
+ */
39
+ protected function setId()
40
+ {
41
+ $this->id = $this->source->getId();
42
+ }
43
+
44
+ /**
45
+ * {@inheritdoc}
46
+ */
47
+ public function getReference()
48
+ {
49
+ return $this->callTypeSpecificMethod(__FUNCTION__);
50
+ }
51
+
52
+ /**
53
+ * Returns the order reference.
54
+ *
55
+ * @return string
56
+ */
57
+ protected function getReferenceOrder()
58
+ {
59
+ return $this->source->getIncrementId();
60
+ }
61
+
62
+ /**
63
+ * Returns the credit note reference.
64
+ *
65
+ * @return string
66
+ */
67
+ protected function getReferenceCreditNote()
68
+ {
69
+ return 'CM' . $this->source->getIncrementId();
70
+ }
71
+
72
+ /**
73
+ * Returns the status of this order.
74
+ *
75
+ * @return string
76
+ */
77
+ protected function getStatusOrder()
78
+ {
79
+ return $this->source->getStatus();
80
+ }
81
+
82
+ /**
83
+ * Returns the status of this order.
84
+ *
85
+ * @return int
86
+ * 1 of
87
+ * Mage_Sales_Model_Order_Creditmemo::STATE_OPEN = 1;
88
+ * Mage_Sales_Model_Order_Creditmemo::STATE_REFUNDED = 2;
89
+ * Mage_Sales_Model_Order_Creditmemo::STATE_CANCELED = 3;
90
+ */
91
+ protected function getStatusCreditNote()
92
+ {
93
+ return $this->source->getState();
94
+ }
95
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Shop;
3
+
4
+ use Mage;
5
+ use Siel\Acumulus\Shop\AcumulusEntryModel as BaseAcumulusEntryModel;
6
+ use Siel_Acumulus_Model_Entry;
7
+
8
+ /**
9
+ * Implements the Magento specific acumulus entry model class.
10
+ */
11
+ class AcumulusEntryModel extends BaseAcumulusEntryModel
12
+ {
13
+ /** @var Siel_Acumulus_Model_Entry */
14
+ protected $model;
15
+
16
+ /**
17
+ * AcumulusEntryModel constructor.
18
+ */
19
+ public function __construct()
20
+ {
21
+ $this->model = Mage::getModel('acumulus/entry');
22
+ }
23
+
24
+ /**
25
+ * @return Siel_Acumulus_Model_Entry
26
+ */
27
+ protected function getModel()
28
+ {
29
+ return $this->model;
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ */
35
+ public function getByEntryId($entryId)
36
+ {
37
+ return $this->getModel()->load($entryId, 'entry_id');
38
+ }
39
+
40
+ /**
41
+ * {@inheritdoc}
42
+ */
43
+ public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId)
44
+ {
45
+ /** @var Siel_Acumulus_Model_Entry $result */
46
+ $result = $this->getModel()->getResourceCollection()
47
+ ->addFieldToFilter('source_type', $invoiceSourceType)
48
+ ->addFieldToFilter('source_id', $invoiceSourceId)
49
+ ->getFirstItem();
50
+ return $result->getSourceId() == $invoiceSourceId ? $result : null;
51
+ }
52
+
53
+ /**
54
+ * {@inheritdoc}
55
+ */
56
+ protected function insert($invoiceSource, $entryId, $token, $created)
57
+ {
58
+ return $this->getModel()
59
+ ->setEntryId($entryId)
60
+ ->setToken($token)
61
+ ->setSourceType($invoiceSource->getType())
62
+ ->setSourceId($invoiceSource->getId())
63
+ ->save();
64
+ }
65
+
66
+ /**
67
+ * {@inheritdoc}
68
+ */
69
+ protected function update($record, $entryId, $token, $updated)
70
+ {
71
+ return $record
72
+ ->setEntryId($entryId)
73
+ ->setToken($token)
74
+ ->save();
75
+ }
76
+
77
+ /**
78
+ * {@inheritdoc}
79
+ */
80
+ protected function sqlNow()
81
+ {
82
+ return time();
83
+ }
84
+
85
+ /**
86
+ * {@inheritdoc}
87
+ *
88
+ * Magento has separate install scripts, so nothing has to be done here.
89
+ */
90
+ public function install()
91
+ {
92
+ return true;
93
+ }
94
+
95
+ /**
96
+ * {@inheritdoc}
97
+ *
98
+ * Magento has separate install scripts, so nothing has to be done here.
99
+ */
100
+ public function uninstall()
101
+ {
102
+ return true;
103
+ }
104
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/BatchForm.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Shop;
3
+
4
+ use Mage;
5
+ use Mage_Core_Model_Locale;
6
+ use Siel\Acumulus\Shop\BatchForm as BaseBatchForm;
7
+
8
+ /**
9
+ * Provides the Batch send form handling for the Magento Acumulus module.
10
+ */
11
+ class BatchForm extends BaseBatchForm
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ protected function setFormValues()
17
+ {
18
+ parent::setFormValues();
19
+
20
+ // Group (checked) checkboxes into their collections.
21
+ foreach ($this->getCheckboxKeys() as $checkboxName => $collectionName) {
22
+ if (!empty($this->formValues[$checkboxName])) {
23
+ // Handle the case where $collectionName and $checkboxName are the same.
24
+ if (array_key_exists($collectionName, $this->formValues) && is_array($this->formValues[$collectionName])) {
25
+ $this->formValues[$collectionName][] = $checkboxName;
26
+ } else {
27
+ $this->formValues[$collectionName] = array($checkboxName);
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ *
36
+ * This override returns the default date format as set in the Magento config.
37
+ */
38
+ public function getDateFormat()
39
+ {
40
+ $result = $this->getShopDateFormat();
41
+ $result = str_replace(array('yyyy', 'MM', 'dd'), array('Y', 'm', 'd'), $result);
42
+ return $result;
43
+ }
44
+
45
+ public function getShopDateFormat()
46
+ {
47
+ return Mage::app()->getLocale()->getDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT);
48
+ }
49
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/ConfigForm.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Shop;
3
+
4
+ use Mage;
5
+ use Siel\Acumulus\Shop\ConfigForm as BaseConfigForm;
6
+
7
+ /**
8
+ * Class ConfigForm processes and builds the settings form page for the
9
+ * Magento Acumulus module.
10
+ */
11
+ class ConfigForm extends BaseConfigForm
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ protected function setFormValues()
17
+ {
18
+ parent::setFormValues();
19
+
20
+ // Group (checked) checkboxes into their collections.
21
+ foreach ($this->getCheckboxKeys() as $checkboxName => $collectionName) {
22
+ if (!empty($this->formValues[$checkboxName])) {
23
+ // Handle the case where $collectionName and $checkboxName are the same.
24
+ if (array_key_exists($collectionName, $this->formValues) && is_array($this->formValues[$collectionName])) {
25
+ $this->formValues[$collectionName][] = $checkboxName;
26
+ } else {
27
+ $this->formValues[$collectionName] = array($checkboxName);
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ */
36
+ protected function getShopOrderStatuses()
37
+ {
38
+ $items = Mage::getModel('sales/order_status')->getResourceCollection()->getData();
39
+ $result = array();
40
+ foreach ($items as $item) {
41
+ $result[reset($item)] = next($item);
42
+ }
43
+ return $result;
44
+ }
45
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/ConfigStore.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Shop;
3
+
4
+ use Mage;
5
+ use Siel\Acumulus\Shop\ConfigStore as BaseConfigStore;
6
+
7
+ /**
8
+ * Implements the connection to the Magento config component.
9
+ */
10
+ class ConfigStore extends BaSeConfigStore
11
+ {
12
+ protected $configKey = 'siel_acumulus/';
13
+
14
+ /**
15
+ * {@inheritdoc}
16
+ */
17
+ public function getShopEnvironment()
18
+ {
19
+ /** @noinspection PhpUndefinedFieldInspection */
20
+ $environment = array(
21
+ 'moduleVersion' => Mage::getConfig()->getModuleConfig("Siel_Acumulus")->version,
22
+ 'shopName' => $this->shopName,
23
+ 'shopVersion' => Mage::getVersion(),
24
+ );
25
+
26
+ return $environment;
27
+ }
28
+
29
+ /**
30
+ * {@inheritdoc}
31
+ */
32
+ public function load(array $keys)
33
+ {
34
+ $result = array();
35
+ // Load the values from the web shop specific configuration.
36
+ foreach ($keys as $key) {
37
+ $value = Mage::getStoreConfig($this->configKey . $key);
38
+ // Do not overwrite defaults if no value is set.
39
+ if (isset($value)) {
40
+ if (is_string($value) && strpos($value, '{') !== false) {
41
+ $unserialized = @unserialize($value);
42
+ if ($unserialized !== false) {
43
+ $value = $unserialized;
44
+ }
45
+ }
46
+ $result[$key] = $value;
47
+ }
48
+ }
49
+ return $result;
50
+ }
51
+
52
+ /**
53
+ * {@inheritdoc}
54
+ */
55
+ public function save(array $values)
56
+ {
57
+ $values = $this->saveCommon($values);
58
+
59
+ foreach ($values as $key => $value) {
60
+ if ($value !== null) {
61
+ if (is_bool($value)) {
62
+ $value = $value ? 1 : 0;
63
+ } elseif (is_array($value)) {
64
+ $value = serialize($value);
65
+ }
66
+ /** @var \Mage_Core_Model_Config $configModel */
67
+ $configModel = Mage::getModel('core/config');
68
+ $configModel->saveConfig($this->configKey . $key, $value);
69
+ }
70
+ }
71
+ Mage::getConfig()->reinit();
72
+ return true;
73
+ }
74
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Magento/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Magento\Shop;
3
+
4
+ use DateTime;
5
+ use Mage;
6
+ use Siel\Acumulus\Magento\Invoice\Source;
7
+ use Varien_Object;
8
+ use Siel\Acumulus\Invoice\Source as BaseSource;
9
+ use Siel\Acumulus\Shop\InvoiceManager as BaseInvoiceManager;
10
+
11
+ class InvoiceManager extends BaseInvoiceManager
12
+ {
13
+ /**
14
+ * Returns a Magento model for the given source type.
15
+ *
16
+ * @param $invoiceSourceType
17
+ *
18
+ * @return \Mage_Sales_Model_Abstract
19
+ */
20
+ protected function getInvoiceSourceTypeModel($invoiceSourceType)
21
+ {
22
+ return $invoiceSourceType == Source::Order ? Mage::getModel('sales/order') : Mage::getModel('sales/order_creditmemo');
23
+ }
24
+
25
+ /**
26
+ * {@inheritdoc}
27
+ */
28
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
29
+ {
30
+ $field = 'entity_id';
31
+ $condition = array('from' => $InvoiceSourceIdFrom, 'to' => $InvoiceSourceIdTo);
32
+ return $this->getByCondition($invoiceSourceType, $field, $condition);
33
+ }
34
+
35
+ /**
36
+ * {@inheritdoc}
37
+ */
38
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
39
+ {
40
+ $field = 'increment_id';
41
+ $condition = array('from' => $InvoiceSourceReferenceFrom, 'to' => $InvoiceSourceReferenceTo);
42
+ return $this->getByCondition($invoiceSourceType, $field, $condition);
43
+ }
44
+
45
+ /**
46
+ * {@inheritdoc}
47
+ */
48
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
49
+ {
50
+ $dateFrom = $this->getSqlDate($dateFrom);
51
+ $dateTo = $this->getSqlDate($dateTo);
52
+ $field = 'updated_at';
53
+ $condition = array('from' => $dateFrom, 'to' => $dateTo);
54
+ return $this->getByCondition($invoiceSourceType, $field, $condition);
55
+ }
56
+
57
+ /**
58
+ * Helper method that executes a query to retrieve a list of invoice source
59
+ * ids and returns a list of invoice sources for these ids.
60
+ *
61
+ * @param string $invoiceSourceType
62
+ * @param string|string[] $field
63
+ * @param int|string|array $condition
64
+ *
65
+ * @return \Siel\Acumulus\Magento\Invoice\Source[]
66
+ * A non keyed array with invoice Sources.
67
+ */
68
+ protected function getByCondition($invoiceSourceType, $field, $condition)
69
+ {
70
+ /** @var \Mage_Core_Model_Resource_Db_Collection_Abstract $collection */
71
+ $collection = $this->getInvoiceSourceTypeModel($invoiceSourceType)->getResourceCollection();
72
+ $items = $collection
73
+ ->addFieldToFilter($field, $condition)
74
+ ->getItems();
75
+
76
+ // @todo: replace with getSourcesByIds
77
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $items);
78
+ }
79
+
80
+ /**
81
+ * {@inheritdoc}
82
+ *
83
+ * This Magento override dispatches the 'acumulus_invoice_created' event.
84
+ */
85
+ protected function triggerInvoiceCreated(array &$invoice, BaseSource $invoiceSource)
86
+ {
87
+ $transportObject = new Varien_Object(array('invoice' => $invoice));
88
+ Mage::dispatchEvent('acumulus_invoice_created', array('transport_object' => $transportObject, 'source' => $invoiceSource));
89
+ }
90
+
91
+ /**
92
+ * {@inheritdoc}
93
+ *
94
+ * This Magento override dispatches the 'acumulus_invoice_completed' event.
95
+ */
96
+ protected function triggerInvoiceCompleted(array &$invoice, BaseSource $invoiceSource)
97
+ {
98
+ $transportObject = new Varien_Object(array('invoice' => $invoice));
99
+ Mage::dispatchEvent('acumulus_invoice_completed', array('transport_object' => $transportObject, 'source' => $invoiceSource));
100
+ }
101
+
102
+ /**
103
+ * {@inheritdoc}
104
+ *
105
+ * This Magento override dispatches the 'acumulus_invoice_sent' event.
106
+ */
107
+ protected function triggerInvoiceSent(array $invoice, BaseSource $invoiceSource, array $result)
108
+ {
109
+ Mage::dispatchEvent('acumulus_invoice_sent', array('invoice' => $invoice, 'source' => $invoiceSource, 'result' => $result));
110
+ }
111
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Log.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Log as BaseLog;
5
+
6
+ /**
7
+ * Extends the base log class to log any library logging to the PrestaShop log.
8
+ */
9
+ class Log extends BaseLog
10
+ {
11
+ /**
12
+ * {@inheritdoc}
13
+ *
14
+ * This override uses the OpenCart Log class.
15
+ */
16
+ protected function write($message, $severity)
17
+ {
18
+ $log = new \Log('acumulus.log');
19
+ $message = sprintf('%s - %s', $this->getSeverityString($severity), $message);
20
+ $log->write($message);
21
+ }
22
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Mailer.php ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Helpers;
3
+
4
+ use Mail;
5
+ use Siel\Acumulus\Helpers\Mailer as BaseMailer;
6
+
7
+ /**
8
+ * Extends the base mailer class to send a mail using the PrestaShop mailer.
9
+ */
10
+ class Mailer extends BaseMailer
11
+ {
12
+ /** @var string */
13
+ protected $templateDir;
14
+
15
+ /** @var string */
16
+ protected $templateName;
17
+
18
+ /**
19
+ * {@inheritdoc}
20
+ */
21
+ public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
22
+ {
23
+ $config = Registry::getInstance()->config;
24
+ $mail = new Mail();
25
+
26
+ $mail->protocol = $config->get('config_mail_protocol') ? $config->get('config_mail_protocol') : 'mail';
27
+ $mail->parameter = $config->get('config_mail_parameter');
28
+ $mail->hostname = $config->get('config_smtp_host');
29
+ $mail->username = $config->get('config_smtp_username');
30
+ $mail->password = $config->get('config_smtp_password');
31
+ $mail->port = $config->get('config_smtp_port');
32
+ $mail->timeout = $config->get('config_smtp_timeout');
33
+ $mail->setTo($this->getToAddress());
34
+ $mail->setFrom($config->get('config_email'));
35
+ $mail->setSender($this->getFromName());
36
+ $mail->setSubject($this->getSubject($result));
37
+ $content = $this->getBody($result, $messages, $invoiceSourceType, $invoiceSourceReference);
38
+ $mail->setText($content['text']);
39
+ $mail->setHtml($content['html']);
40
+
41
+ $mail->send();
42
+ }
43
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/OcHelper.php ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Requirements;
5
+ use Siel\Acumulus\Invoice\Source;
6
+ use Siel\Acumulus\Shop\Config as ShopConfig;
7
+ use Siel\Acumulus\Shop\ModuleTranslations;
8
+
9
+ /**
10
+ * OcHelper contains functionality shared between the OC1 and OC2 controllers
11
+ * and models, for both admin and catalog.
12
+ */
13
+ class OcHelper
14
+ {
15
+ /** @var \Siel\Acumulus\Shop\Config */
16
+ protected $acumulusConfig = null;
17
+
18
+ /** @var array */
19
+ public $data;
20
+
21
+ /** @var \Siel\Acumulus\Helpers\Form */
22
+ protected $form;
23
+
24
+ /** @var \Registry */
25
+ protected $registry;
26
+
27
+ /** @var string */
28
+ protected $shopNamespace;
29
+
30
+ /**
31
+ * OcHelper constructor.
32
+ *
33
+ * @param \Registry $registry
34
+ * @param string $shopNamespace
35
+ */
36
+ public function __construct(\Registry $registry, $shopNamespace)
37
+ {
38
+ Registry::setRegistry($registry);
39
+ $this->registry = Registry::getInstance();
40
+ $this->shopNamespace = $shopNamespace;
41
+ }
42
+
43
+ protected function addError($message)
44
+ {
45
+ if (is_array($message)) {
46
+ $this->data['error_messages'] = array_merge($this->data['error_messages'], $message);
47
+ } else {
48
+ $this->data['error_messages'][] = $message;
49
+ }
50
+ }
51
+
52
+ protected function addSuccess($message)
53
+ {
54
+ $this->data['success_messages'][] = $message;
55
+ }
56
+
57
+ /**
58
+ * Helper method that initializes some object properties:
59
+ * - language
60
+ * - model_Setting_Setting
61
+ * - webAPI
62
+ * - acumulusConfig
63
+ */
64
+ protected function init()
65
+ {
66
+ if ($this->acumulusConfig === null) {
67
+ $languageCode = $this->registry->language->get('code');
68
+ if (empty($languageCode)) {
69
+ $languageCode = 'nl';
70
+ }
71
+ $this->acumulusConfig = new ShopConfig($this->shopNamespace, $languageCode);
72
+ $this->acumulusConfig->getTranslator()->add(new ModuleTranslations());
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Helper method to translate strings.
78
+ *
79
+ * @param string $key
80
+ * The key to get a translation for.
81
+ *
82
+ * @return string
83
+ * The translation for the given key or the key itself if no translation
84
+ * could be found.
85
+ */
86
+ protected function t($key)
87
+ {
88
+ return $this->acumulusConfig->getTranslator()->get($key);
89
+ }
90
+
91
+ /**
92
+ * Install controller action, called when the module is installed.
93
+ *
94
+ * @return bool
95
+ */
96
+ public function install()
97
+ {
98
+ // Call the actual install method.
99
+ $this->doInstall();
100
+
101
+ return empty($this->data['error_messages']);
102
+ }
103
+
104
+ /**
105
+ * Uninstall function, called when the module is uninstalled by an admin.
106
+ */
107
+ public function uninstall()
108
+ {
109
+ $this->registry->response->redirect($this->registry->url->link('module/acumulus/confirmUninstall', 'token=' . $this->registry->session->data['token'], 'SSL'));
110
+ }
111
+
112
+ /**
113
+ * Controller action: show/process the settings form for this module.
114
+ */
115
+ public function config()
116
+ {
117
+ $this->displayFormCommon('config');
118
+
119
+ // Are we posting? If not so, handle this as a trigger to update.
120
+ if ($this->registry->request->server['REQUEST_METHOD'] !== 'POST') {
121
+ $this->doUpgrade();
122
+ }
123
+
124
+ // Add an intermediate level to the breadcrumb.
125
+ $this->data['breadcrumbs'][] = array(
126
+ 'text' => $this->t('modules'),
127
+ 'href' => Registry::getInstance()->url->link('extension/module', 'token=' . $this->registry->session->data['token'], 'SSL'),
128
+ 'separator' => ' :: '
129
+ );
130
+
131
+ $this->renderFormCommon('config', 'button_save');
132
+ }
133
+
134
+ /**
135
+ * Controller action: show/process the settings form for this module.
136
+ */
137
+ public function batch()
138
+ {
139
+ $this->displayFormCommon('batch');
140
+ $this->renderFormCommon('batch', 'button_send');
141
+ }
142
+
143
+ /**
144
+ * Explicit confirmation step to allow to retain the settings.
145
+ *
146
+ * The normal uninstall action will unconditionally delete all settings.
147
+ */
148
+ public function confirmUninstall()
149
+ {
150
+ $this->displayFormCommon('uninstall');
151
+
152
+ // Are we confirming, or should we show the confirm message?
153
+ if ($this->registry->request->server['REQUEST_METHOD'] === 'POST') {
154
+ $this->doUninstall();
155
+ $this->registry->response->redirect($this->registry->url->link('extension/module', 'token=' . $this->registry->session->data['token'],
156
+ 'SSL'));
157
+ }
158
+
159
+ // Add an intermediate level to the breadcrumb.
160
+ $this->data['breadcrumbs'][] = array(
161
+ 'text' => $this->t('modules'),
162
+ 'href' => $this->registry->url->link('extension/module', 'token=' . $this->registry->session->data['token'], 'SSL'),
163
+ 'separator' => ' :: '
164
+ );
165
+
166
+ $this->renderFormCommon('confirmUninstall', 'button_confirm_uninstall');
167
+ }
168
+
169
+ /**
170
+ * Event handler that executes on the creation or update of an order.
171
+ *
172
+ * @param int $order_id
173
+ */
174
+ public function eventOrderUpdate($order_id) {
175
+ $this->init();
176
+ $source = $this->acumulusConfig->getSource(Source::Order, $order_id);
177
+ $this->acumulusConfig->getManager()->sourceStatusChange($source);
178
+ }
179
+
180
+ /**
181
+ * Performs the common tasks when displaying a form.
182
+ *
183
+ * @param string $task
184
+ */
185
+ protected function displayFormCommon($task)
186
+ {
187
+ $this->init();
188
+
189
+ $this->form = $this->acumulusConfig->getForm($task);
190
+
191
+ $this->registry->document->addStyle('view/stylesheet/acumulus.css');
192
+
193
+ $this->data['success_messages'] = array();
194
+ $this->data['error_messages'] = array();
195
+
196
+ // Set the page title.
197
+ $this->registry->document->setTitle($this->t("{$task}_form_title"));
198
+ $this->data["page_title"] = $this->t("{$task}_form_title");
199
+ $this->data["heading_title"] = $this->t("{$task}_form_header");
200
+ $this->data["text_edit"] = $this->t("{$task}_form_header");
201
+
202
+ // Set up breadcrumb.
203
+ $this->data['breadcrumbs'] = array();
204
+ $this->data['breadcrumbs'][] = array(
205
+ 'text' => $this->t('text_home'),
206
+ 'href' => $this->registry->url->link('common/dashboard', 'token=' . $this->registry->session->data['token'], 'SSL'),
207
+ 'separator' => false
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Performs the common tasks when processing and rendering a form.
213
+ *
214
+ * @param string $task
215
+ * @param string $button
216
+ */
217
+ protected function renderFormCommon($task, $button)
218
+ {
219
+ // Process the form if it was submitted and render it again.
220
+ $this->form->process();
221
+
222
+ // Show messages.
223
+ foreach ($this->form->getSuccessMessages() as $message) {
224
+ $this->addSuccess($message);
225
+ }
226
+ foreach ($this->form->getErrorMessages() as $message) {
227
+ $this->addError($this->t($message));
228
+ }
229
+
230
+ $this->data['form'] = $this->form;
231
+ $this->data['formRenderer'] = $this->acumulusConfig->getFormRenderer();
232
+
233
+ // Complete the breadcrumb with the current path.
234
+ $link = 'module/acumulus';
235
+ if ($task !== 'config') {
236
+ $link .= "/$task";
237
+ }
238
+ $this->data['breadcrumbs'][] = array(
239
+ 'text' => $this->t("{$task}_form_header"),
240
+ 'href' => $this->registry->url->link($link, 'token=' . $this->registry->session->data['token'], 'SSL'),
241
+ 'separator' => ' :: '
242
+ );
243
+
244
+ // Set the action buttons (action + text).
245
+ $this->data['action'] = $this->registry->url->link($link, 'token=' . $this->registry->session->data['token'], 'SSL');
246
+ $this->data['button_icon'] = $task === 'batch' ? 'fa-envelope-o' : ($task === 'uninstall' ? 'fa-delete' : 'fa-save');
247
+ $this->data['button_save'] = $this->t($button);
248
+ $this->data['cancel'] = $this->registry->url->link('common/dashboard', 'token=' . $this->registry->session->data['token'], 'SSL');
249
+ $this->data['button_cancel'] = $task === 'uninstall' ? $this->t('button_cancel_uninstall') : $this->t('button_cancel');
250
+ }
251
+
252
+ /**
253
+ * Checks requirements and installs tables for this module.
254
+ *
255
+ * @return bool
256
+ * Success.
257
+ */
258
+ protected function doInstall()
259
+ {
260
+ $this->init();
261
+
262
+ $result = true;
263
+ $this->registry->load->model('setting/setting');
264
+ $setting = $this->registry->model_setting_setting->getSetting('acumulus_siel');
265
+ $currentDataModelVersion = isset($setting['acumulus_siel_datamodel_version']) ? $setting['acumulus_siel_datamodel_version'] : '';
266
+
267
+ if ($currentDataModelVersion === '' || version_compare($currentDataModelVersion, '4.0', '<')) {
268
+ // Check requirements (we assume this has been done successfully before
269
+ // if the data model is at the latest version.
270
+ $requirements = new Requirements();
271
+ $messages = $requirements->check();
272
+ foreach ($messages as $message) {
273
+ $this->addError($message['message']);
274
+ }
275
+ if (!empty($messages)) {
276
+ return false;
277
+ }
278
+
279
+ // Install tables.
280
+ $result = $this->acumulusConfig->getAcumulusEntryModel()->install();
281
+ $setting['acumulus_siel_datamodel_version'] = '4.0';
282
+ $this->registry->model_setting_setting->editSetting('acumulus_siel', $setting);
283
+ }
284
+
285
+ return $result;
286
+ }
287
+
288
+ /**
289
+ * Uninstalls data and settings from this module.
290
+ *
291
+ * @return bool
292
+ * Whether the uninstall was successful.
293
+ */
294
+ protected function doUninstall()
295
+ {
296
+ $this->init();
297
+ $this->acumulusConfig->getAcumulusEntryModel()->uninstall();
298
+
299
+ // Delete all config values.
300
+ $this->registry->load->model('setting/setting');
301
+ $this->registry->model_setting_setting->deleteSetting('acumulus_siel');
302
+
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Upgrades the data and settings for this module if needed.
308
+ *
309
+ * The install now checks for the data model and can do an upgrade instead of
310
+ * a clean install.
311
+ *
312
+ * @return bool
313
+ * Whether the upgrade was successful.
314
+ */
315
+ protected function doUpgrade()
316
+ {
317
+ return $this->doInstall();
318
+ }
319
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Helpers/Registry.php ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Helpers;
3
+
4
+ /** @noinspection PhpUndefinedNamespaceInspection */
5
+ /** @noinspection PhpUndefinedClassInspection */
6
+ /**
7
+ * Registry is a wrapper around the registry instance which is not directly
8
+ * accessible as the single instance is passed to each constructor in the
9
+ * OpenCart classes.
10
+ *
11
+ * @property \Config config
12
+ * @property \DBMySQLi|\DB\MySQLi db
13
+ * @property \Document document
14
+ * @property \Event event
15
+ * @property \Language language
16
+ * @property \Loader load
17
+ * @property \Request request
18
+ * @property \Response response
19
+ * @property \Session session
20
+ * @property \Url url
21
+ * @property \ModelAccountOrder model_account_order
22
+ * @property \ModelCatalogProduct model_catalog_product
23
+ * @property \ModelCheckoutOrder model_checkout_order
24
+ * @property \ModelExtensionEvent model_extension_event
25
+ * @property \ModelSaleOrder model_sale_order
26
+ * @property \ModelLocalisationOrderStatus model_localisation_order_status
27
+ * @property \ModelSettingSetting model_setting_setting
28
+ */
29
+ class Registry
30
+ {
31
+ /** @var \Siel\Acumulus\OpenCart\Helpers\Registry */
32
+ protected static $instance;
33
+
34
+ /** @var \Registry */
35
+ protected $registry;
36
+
37
+ /** @noinspection PhpUndefinedClassInspection */
38
+ /** @var \ModelAccountOrder|\ModelSaleOrder */
39
+ protected $orderModel;
40
+
41
+ /**
42
+ * Sets the OC Registry.
43
+ *
44
+ * @param \Registry $registry
45
+ */
46
+ public static function setRegistry(\Registry $registry) {
47
+ static::$instance = new Registry($registry);
48
+ }
49
+
50
+ /**
51
+ * Returns the Registry instance.
52
+ *
53
+ * @return \Siel\Acumulus\OpenCart\Helpers\Registry
54
+ */
55
+ public static function getInstance()
56
+ {
57
+ return static::$instance;
58
+ }
59
+
60
+ /**
61
+ * Registry constructor.
62
+ *
63
+ * @param \Registry $registry
64
+ */
65
+ public function __construct(\Registry $registry)
66
+ {
67
+ $this->registry = $registry;
68
+ $this->orderModel = null;
69
+ }
70
+
71
+ public function __get($key)
72
+ {
73
+ return $this->registry->get($key);
74
+ }
75
+
76
+ public function __set($key, $value)
77
+ {
78
+ $this->registry->set($key, $value);
79
+ }
80
+
81
+ public function getOrderModel()
82
+ {
83
+ if ($this->orderModel === null) {
84
+ if (strrpos(DIR_APPLICATION, '/catalog/') === strlen(DIR_APPLICATION) - strlen('/catalog/')) {
85
+ // We are in the catalog section, use the account/order model.
86
+ $this->load->model('account/order');
87
+ $this->orderModel = $this->model_account_order;
88
+ } else {
89
+ // We are in the admin section, use the sale/order model.
90
+ Registry::getInstance()->load->model('sale/order');
91
+ $this->orderModel = Registry::getInstance()->model_sale_order;
92
+ }
93
+ }
94
+ return $this->orderModel;
95
+ }
96
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Invoice/Creator.php ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Invoice;
3
+
4
+ use Siel\Acumulus\Invoice\ConfigInterface;
5
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
6
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
7
+
8
+ /**
9
+ * Allows to create arrays in the Acumulus invoice structure from an OpenCart
10
+ * order.
11
+ */
12
+ class Creator extends BaseCreator
13
+ {
14
+ // More specifically typed property.
15
+ /** @var array */
16
+ protected $order;
17
+
18
+ /** @var array[] List of OpenCart order total records. */
19
+ protected $orderTotalLines;
20
+
21
+ /**
22
+ * {@inheritdoc}
23
+ *
24
+ * This override also initializes WooCommerce specific properties related to
25
+ * the source.
26
+ */
27
+ protected function setInvoiceSource($invoiceSource)
28
+ {
29
+ parent::setInvoiceSource($invoiceSource);
30
+
31
+ // Load some models and properties we are going to use.
32
+ Registry::getInstance()->load->model('catalog/product');
33
+ $this->orderTotalLines = null;
34
+
35
+ switch ($this->invoiceSource->getType()) {
36
+ case Source::Order:
37
+ $this->order = $this->invoiceSource->getSource();
38
+ break;
39
+ case Source::CreditNote:
40
+ break;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * {@inheritdoc}
46
+ */
47
+ protected function getCustomer()
48
+ {
49
+ $order = $this->order;
50
+ $result = array();
51
+
52
+ $this->addIfSetAndNotEmpty($result, 'contactyourid', $order, 'customer_id');
53
+ $this->addEmpty($result, 'companyname1', $order['payment_company']);
54
+ if (!empty($result['companyname1'])) {
55
+ // @todo: Are there VAT number extensions?
56
+ $this->addIfSetAndNotEmpty($result, 'vatnumber', $order, 'payment_tax_id');
57
+ }
58
+ $result['fullname'] = $order['firstname'] . ' ' . $order['lastname'];
59
+ $this->addEmpty($result, 'address1', $order['payment_address_1']);
60
+ $this->addEmpty($result, 'address2', $order['payment_address_2']);
61
+ $this->addEmpty($result, 'postalcode', $order['payment_postcode']);
62
+ $this->addEmpty($result, 'city', $order['payment_city']);
63
+ if (!empty($order['payment_iso_code_2'])) {
64
+ $result['countrycode'] = $order['payment_iso_code_2'];
65
+ }
66
+ $this->addIfSetAndNotEmpty($result, 'country', $order, 'payment_country');
67
+ $this->addIfSetAndNotEmpty($result, 'telephone', $order, 'telephone');
68
+ $this->addIfSetAndNotEmpty($result, 'fax', $order, 'fax');
69
+ $result['email'] = $order['email'];
70
+
71
+ return $result;
72
+ }
73
+
74
+ /**
75
+ * {@inheritdoc}
76
+ */
77
+ protected function getInvoiceNumber($invoiceNumberSource)
78
+ {
79
+ $result = $this->invoiceSource->getReference();
80
+ if ($invoiceNumberSource == ConfigInterface::InvoiceNrSource_ShopInvoice && !empty($this->order['invoice_no'])) {
81
+ $result = $this->order['invoice_prefix'] . $this->order['invoice_no'];
82
+ }
83
+ return $result;
84
+ }
85
+
86
+ /**
87
+ * {@inheritdoc}
88
+ */
89
+ protected function getInvoiceDate($dateToUse)
90
+ {
91
+ $result = substr($this->order['date_added'], 0, strlen('2000-01-01'));
92
+ // There doesn't seem to be an invoice date: stick with order create date,
93
+ // also when $dateToUse === ConfigInterface::InvoiceDate_InvoiceCreate.
94
+ return $result;
95
+ }
96
+
97
+ /**
98
+ * {@inheritdoc}
99
+ */
100
+ protected function getPaymentState()
101
+ {
102
+ // @todo: Can we determine this based on payment_method and payment_code?
103
+ $result = ConfigInterface::PaymentStatus_Paid;
104
+ return $result;
105
+ }
106
+
107
+ /**
108
+ * {@inheritdoc}
109
+ */
110
+ protected function getPaymentDate()
111
+ {
112
+ // @todo: Can we determine this based on history (and optionally payment_method and payment_code)?
113
+ // Will default to the issue date.
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * {@inheritdoc}
119
+ *
120
+ * This override provides the values meta-invoice-amountinc,
121
+ * meta-invoice-vatamount and a vat breakdown in meta-invoice-vat.
122
+ */
123
+ protected function getInvoiceTotals()
124
+ {
125
+ $result = array(
126
+ 'meta-invoice-amountinc' => $this->order['total'],
127
+ 'meta-invoice-vatamount' => 0.0,
128
+ 'meta-invoice-vat' => array(),
129
+ );
130
+
131
+ $orderTotals = $this->getOrderTotalLines();
132
+ foreach ($orderTotals as $totalLine) {
133
+ if ($totalLine['code'] === 'tax') {
134
+ $result['meta-invoice-vat'][] = $totalLine['title'] . ': ' . $totalLine['value'];
135
+ $result['meta-invoice-vatamount'] += $totalLine['value'];
136
+ }
137
+ }
138
+ return $result;
139
+ }
140
+
141
+ /**
142
+ * {@inheritdoc}
143
+ */
144
+ protected function getInvoiceLines()
145
+ {
146
+ $itemLines = $this->getItemLines();
147
+ $itemLines = $this->addLineType($itemLines, static::LineType_Order);
148
+
149
+ $totalLines = $this->getTotalLines();
150
+
151
+ $result = array_merge($itemLines, $totalLines);
152
+ return $result;
153
+ }
154
+
155
+ /**
156
+ * {@inheritdoc}
157
+ */
158
+ protected function getItemLines()
159
+ {
160
+ $result = array();
161
+
162
+ $orderProducts = $this->getOrderModel()->getOrderProducts($this->invoiceSource->getId());
163
+ foreach ($orderProducts as $line) {
164
+ $result = array_merge($result, $this->getItemLine($line));
165
+ }
166
+
167
+ return $result;
168
+ }
169
+
170
+ /**
171
+ * Returns the item line(s) for 1 product line.
172
+ *
173
+ * This method may return multiple lines if there are many options.
174
+ * These additional lines will be informative, their price will be 0.
175
+ *
176
+ * @param array $item
177
+ *
178
+ * @return array[]
179
+ */
180
+ protected function getItemLine(array $item)
181
+ {
182
+ $result = array();
183
+
184
+ $product = Registry::getInstance()->model_catalog_product->getProduct($item['product_id']);
185
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'mpn');
186
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'isbn');
187
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'jan');
188
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'ean');
189
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'upc');
190
+ $this->addIfSetAndNotEmpty($result, 'itemnumber', $product, 'sku');
191
+
192
+ // Product name, model, and option(s).
193
+ $result['product'] = $item['name'];
194
+ if (!empty($item['model'])) {
195
+ $result['product'] .= ' (' . $item['model'] . ')';
196
+ }
197
+
198
+ $productPriceEx = $item['price'];
199
+ $productVat = $item['tax'];
200
+ $vatInfo = $this->getVatRangeTags($productVat, $productPriceEx);
201
+
202
+ // Options (variants).
203
+ $optionsLines = array();
204
+ $optionsTexts = array();
205
+ $options = $this->getOrderModel()->getOrderOptions($item['order_id'], $item['order_product_id']);
206
+ foreach ($options as $option) {
207
+ $optionsTexts[] = "{$option['name']}: {$option['value']}";
208
+ }
209
+ if (count($options) > 0) {
210
+ $optionsText = ' (' . implode(', ', $optionsTexts) . ')';
211
+
212
+ $invoiceSettings = $this->config->getInvoiceSettings();
213
+ if (count($options) <= $invoiceSettings['optionsAllOn1Line']
214
+ || (count($options) < $invoiceSettings['optionsAllOnOwnLine'] && strlen($optionsText) <= $invoiceSettings['optionsMaxLength'])
215
+ ) {
216
+ // All options on 1 item.
217
+ $result['product'] .= ' (' . implode(', ', $optionsTexts) . ')';
218
+ } else {
219
+ // All options on their own item.
220
+ foreach ($optionsTexts as $optionsText) {
221
+ $optionsLines[] = array(
222
+ 'product' => " - $optionsText",
223
+ 'unitprice' => 0,
224
+ // Table order_option does not have a quantity
225
+ // field, so composite products are apparently not
226
+ // covered.
227
+ 'quantity' => 1,
228
+ ) + $vatInfo;
229
+ }
230
+ }
231
+ }
232
+
233
+ $result['unitprice'] = $productPriceEx;
234
+ $result['quantity'] = $item['quantity'];
235
+ $result += $vatInfo;
236
+ $result['vatamount'] = $productVat;
237
+
238
+ return array_merge(array($result), $optionsLines);
239
+ }
240
+
241
+ /**
242
+ *
243
+ *
244
+ *
245
+ * @return array[]
246
+ *
247
+ */
248
+ protected function getTotalLines()
249
+ {
250
+ $result = array();
251
+
252
+ $totalLines = $this->getOrderTotalLines();
253
+ foreach ($totalLines as $totalLine) {
254
+ switch ($totalLine['code']) {
255
+ case 'sub_total':
256
+ // Sub total of all product lines: ignore.
257
+ $line = null;
258
+ break;
259
+ case 'shipping':
260
+ $line = $this->getTotalLine($totalLine);
261
+ $line['meta-line-type'] = static::LineType_Shipping;
262
+ break;
263
+ case 'coupon':
264
+ $line = $this->getTotalLine($totalLine);
265
+ $line['meta-line-type'] = static::LineType_Discount;
266
+ break;
267
+ case 'tax':
268
+ // Tax line: added to invoice level
269
+ $line = null;
270
+ break;
271
+ case 'voucher':
272
+ $line = $this->getTotalLine($totalLine);
273
+ $line['meta-line-type'] = static::LineType_Voucher;
274
+ break;
275
+ case 'total':
276
+ // Overall total: ignore.
277
+ $line = null;
278
+ break;
279
+ default:
280
+ $line = $this->getTotalLine($totalLine);
281
+ $line['meta-line-type'] = static::LineType_Other;
282
+ break;
283
+ }
284
+ if ($line) {
285
+ $result[] = $line;
286
+ }
287
+ }
288
+
289
+ return $result;
290
+ }
291
+
292
+ /**
293
+ * Returns a line based on a "order total line".
294
+ *
295
+ * @param array $line
296
+ *
297
+ * @return array
298
+ */
299
+ protected function getTotalLine(array $line)
300
+ {
301
+ $result = array(
302
+ 'product' => $line['title'],
303
+ // Let's hope that this is the value ex vat...
304
+ 'unitprice' => $line['value'],
305
+ 'quantity' => 1,
306
+ );
307
+
308
+ if ($line['code'] === 'voucher') {
309
+ // A voucher is to be seen as a partial payment, thus no tax.
310
+ $result += array(
311
+ 'vatrate' => -1,
312
+ 'meta-vatrate-source' => Creator::VatRateSource_Exact0,
313
+ );
314
+ } else {
315
+ // Other lines do not have a discoverable vatrate, let a strategy try to
316
+ // compute it.
317
+ $result += array(
318
+ 'vatrate' => null,
319
+ 'meta-vatrate-source' => Creator::VatRateSource_Strategy,
320
+ // Coupons may have to be split over various taxes, but shipping and
321
+ // other fees not.
322
+ 'meta-strategy-split' => $line['code'] === 'coupon',
323
+ );
324
+ }
325
+
326
+ return $result;
327
+ }
328
+
329
+ /**
330
+ * Returns a list of OpenCart order total records. These are shipment,
331
+ * other fee, tax, and discount lines.
332
+ *
333
+ * @return array[]
334
+ */
335
+ protected function getOrderTotalLines()
336
+ {
337
+ if (!$this->orderTotalLines) {
338
+ $orderModel = $this->getOrderModel();
339
+ $this->orderTotalLines = $orderModel->getOrderTotals($this->order['order_id']);
340
+ }
341
+ return $this->orderTotalLines;
342
+ }
343
+
344
+ /** @noinspection PhpUndefinedClassInspection */
345
+ /**
346
+ * @return \ModelAccountOrder|\ModelSaleOrder
347
+ */
348
+ protected function getOrderModel()
349
+ {
350
+ return Registry::getInstance()->getOrderModel();
351
+ }
352
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Invoice/Source.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Invoice;
3
+
4
+ use Siel\Acumulus\Invoice\Source as BaseSource;
5
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
6
+
7
+ /**
8
+ * Wraps an OpenCart order in an invoice source object.
9
+ */
10
+ class Source extends BaseSource
11
+ {
12
+ // More specifically typed properties.
13
+ /** @var array */
14
+ protected $source;
15
+
16
+ /**
17
+ * {@inheritdoc}
18
+ */
19
+ protected function setSource()
20
+ {
21
+ $this->source = Registry::getInstance()->getOrderModel()->getOrder($this->id);
22
+ }
23
+
24
+ /**
25
+ * Sets the id based on the loaded Order.
26
+ */
27
+ protected function setId()
28
+ {
29
+ $this->id = $this->source['order_id'];
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ *
35
+ * @return int
36
+ */
37
+ public function getStatus()
38
+ {
39
+ return $this->source['order_status_id'];
40
+ }
41
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Helpers/FormRenderer.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart1\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\FormRenderer as BaseFormRenderer;
5
+
6
+ /**
7
+ * FormMapper maps an Acumulus form definition to a OpenCart 1 form definition.
8
+ */
9
+ class FormRenderer extends BaseFormRenderer
10
+ {
11
+ /**
12
+ * Constructor.
13
+ */
14
+ public function __construct()
15
+ {
16
+ // Default OpenCart template seems to use html 5.
17
+ $this->fieldsetWrapperClass = 'adminform';
18
+ $this->legendWrapperClass = 'form-group';
19
+ $this->requiredMarkup = '';
20
+ $this->labelWrapperClass = '';
21
+ $this->inputWrapperClass = 'form-control';
22
+ $this->radioWrapperClass = 'form-element-radios';
23
+ $this->checkboxWrapperClass = 'form-element-checkboxes';
24
+ $this->multiLabelClass = 'label';
25
+ $this->descriptionClass = 'desc';
26
+ }
27
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Helpers/OcHelper.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart1\Helpers;
3
+
4
+ use Siel\Acumulus\OpenCart\Helpers\OcHelper as BaseOcHelper;
5
+
6
+ /**
7
+ * OcHelper contains functionality shared between the OC1 and OC2 controllers
8
+ * and models, for both admin and catalog.
9
+ */
10
+ class OcHelper extends BaseOcHelper
11
+ {
12
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart1/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart1\Shop;
3
+
4
+ use Siel\Acumulus\Invoice\Source as BaseSource;
5
+ use Siel\Acumulus\OpenCart\Shop\InvoiceManager as BaseInvoiceManager;
6
+
7
+ /**
8
+ * This OpenCart 1 override allows you to insert your event handler code using
9
+ * VQMOD.
10
+ */
11
+ class InvoiceManager extends BaseInvoiceManager
12
+ {
13
+ /**
14
+ * {@inheritdoc}
15
+ */
16
+ protected function triggerInvoiceCreated(array &$invoice, BaseSource $invoiceSource)
17
+ {
18
+ // VQMOD: insert your 'acumulus.invoice.created' event code here.
19
+ // END VQMOD: insert your 'acumulus.invoice.created' event code here.
20
+ }
21
+
22
+ /**
23
+ * {@inheritdoc}
24
+ */
25
+ protected function triggerInvoiceCompleted(array &$invoice, BaseSource $invoiceSource)
26
+ {
27
+ // VQMOD: insert your 'acumulus.invoice.completed' event code here.
28
+ // END VQMOD: insert your 'acumulus.invoice.completed' event code here.
29
+ }
30
+
31
+ /**
32
+ * {@inheritdoc}
33
+ */
34
+ protected function triggerInvoiceSent(array $invoice, BaseSource $invoiceSource, array $result)
35
+ {
36
+ // VQMOD: insert your 'acumulus.invoice.sent' event code here.
37
+ // END VQMOD: insert your 'acumulus.invoice.sent' event code here.
38
+ }
39
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Helpers/FormRenderer.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart2\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\FormRenderer as BaseFormRenderer;
5
+
6
+ /**
7
+ * FormMapper maps an Acumulus form definition to a OpenCart 2 form definition.
8
+ */
9
+ class FormRenderer extends BaseFormRenderer
10
+ {
11
+ /**
12
+ * Constructor.
13
+ */
14
+ public function __construct()
15
+ {
16
+ // Default OpenCart template seems to use html 5.
17
+ $this->fieldsetWrapperClass = 'adminform';
18
+ $this->legendWrapperClass = 'form-group';
19
+ $this->elementWrapperClass = 'form-group';
20
+ $this->requiredMarkup = '';
21
+ $this->labelWrapperClass = 'form-group';
22
+ $this->inputWrapperClass = 'form-control';
23
+ $this->multiLabelClass = 'control-label';
24
+ $this->descriptionClass = 'description';
25
+ }
26
+
27
+ protected function renderField(array $field)
28
+ {
29
+ $oldElementWrapperClass = $this->elementWrapperClass;
30
+ if (!empty($field['attributes']['required'])) {
31
+ if (empty($this->elementWrapperClass)) {
32
+ $this->elementWrapperClass = '';
33
+ } else {
34
+ $this->elementWrapperClass .= ' ';
35
+ }
36
+ $this->elementWrapperClass .= 'required';
37
+ }
38
+ $result = parent::renderField($field);
39
+ $this->elementWrapperClass = $oldElementWrapperClass;
40
+ return $result;
41
+ }
42
+
43
+
44
+ protected function addLabelAttributes(array $attributes, $id)
45
+ {
46
+ $attributes = parent::addLabelAttributes($attributes, $id);
47
+ $attributes = $this->addAttribute($attributes, 'class', 'col-sm-2');
48
+ $attributes = $this->addAttribute($attributes, 'class', 'control-label');
49
+ return $attributes;
50
+ }
51
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Helpers/OcHelper.php ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart2\Helpers;
3
+
4
+ use Siel\Acumulus\OpenCart\Helpers\OcHelper as BaseOcHelper;
5
+
6
+ /**
7
+ * OcHelper contains functionality shared between the OC1 and OC2 controllers
8
+ * and models, for both admin and catalog.
9
+ */
10
+ class OcHelper extends BaseOcHelper
11
+ {
12
+ /**
13
+ * Uninstall function, called when the module is uninstalled by an admin.
14
+ */
15
+ public function uninstall()
16
+ {
17
+ // "Disable" (delete) events, regardless the confirmation answer.
18
+ $this->uninstallEvents();
19
+ parent::uninstall();
20
+ }
21
+
22
+ /**
23
+ * Performs the common tasks when displaying a form.
24
+ *
25
+ * @param string $task
26
+ */
27
+ protected function displayFormCommon($task)
28
+ {
29
+ parent::displayFormCommon($task);
30
+
31
+ // Render the common parts.
32
+ $this->data['header'] = $this->registry->load->controller('common/header');
33
+ $this->data['column_left'] = $this->registry->load->controller('common/column_left');
34
+ $this->data['footer'] = $this->registry->load->controller('common/footer');
35
+ }
36
+
37
+ /**
38
+ * Performs the common tasks when processing and rendering a form.
39
+ *
40
+ * @param string $task
41
+ * @param string $button
42
+ */
43
+ protected function renderFormCommon($task, $button)
44
+ {
45
+ parent::renderFormCommon($task, $button);
46
+
47
+ // Send the output.
48
+ $this->registry->response->setOutput($this->registry->load->view('module/acumulus-form.tpl', $this->data));
49
+ }
50
+
51
+ /**
52
+ * Checks requirements and installs tables for this module.
53
+ *
54
+ * @return bool
55
+ * Success.
56
+ */
57
+ protected function doInstall()
58
+ {
59
+ $result = parent::doInstall();
60
+
61
+ // Install events
62
+ if (empty($this->data['error_warnings'])) {
63
+ $this->installEvents();
64
+ }
65
+
66
+ return $result;
67
+ }
68
+
69
+ /**
70
+ * Installs our events.
71
+ *
72
+ * This will add them to the table 'event' from where they are registered on
73
+ * the start of each request. The controller actions can be found in the
74
+ * catalog controller.
75
+ *
76
+ * To support updating, this will also be called by the index function.
77
+ * Therefore we will first remove any existing events from our module.
78
+ */
79
+ protected function installEvents()
80
+ {
81
+ $this->uninstallEvents();
82
+ $this->registry->model_extension_event->addEvent('acumulus', 'post.order.add', 'module/acumulus/eventOrderUpdate');
83
+ $this->registry->model_extension_event->addEvent('acumulus', 'post.order.history.add',
84
+ 'module/acumulus/eventOrderUpdate');
85
+ }
86
+
87
+ /**
88
+ * Removes the Acumulus event handlers from the event table.
89
+ */
90
+ protected function uninstallEvents()
91
+ {
92
+ $this->registry->load->model('extension/event');
93
+ $this->registry->model_extension_event->deleteEvent('acumulus');
94
+ }
95
+
96
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/OpenCart2/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\OpenCart2\Shop;
3
+
4
+ use Siel\Acumulus\Invoice\Source as BaseSource;
5
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
6
+ use Siel\Acumulus\OpenCart\Shop\InvoiceManager as BaseInvoiceManager;
7
+
8
+ class InvoiceManager extends BaseInvoiceManager
9
+ {
10
+ /**
11
+ * {@inheritdoc}
12
+ *
13
+ * This OpenCart 2 override triggers the 'acumulus.invoice.created' event.
14
+ */
15
+ protected function triggerInvoiceCreated(array &$invoice, BaseSource $invoiceSource)
16
+ {
17
+ $args = array('invoice' => &$invoice, 'source' => $invoiceSource);
18
+ $this->getEvent()->trigger('acumulus.invoice.created', $args);
19
+ }
20
+
21
+ /**
22
+ * {@inheritdoc}
23
+ *
24
+ * This OpenCart 2 override triggers the 'acumulus.invoice.completed' event.
25
+ */
26
+ protected function triggerInvoiceCompleted(array &$invoice, BaseSource $invoiceSource)
27
+ {
28
+ $args = array('invoice' => &$invoice, 'source' => $invoiceSource);
29
+ $this->getEvent()->trigger('acumulus.invoice.completed', $args);
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ *
35
+ * This OpenCart 2 override triggers the 'acumulus.invoice.sent' event.
36
+ */
37
+ protected function triggerInvoiceSent(array $invoice, BaseSource $invoiceSource, array $result)
38
+ {
39
+ $args = array('invoice' => $invoice, 'source' => $invoiceSource, 'result' => $result);
40
+ $this->getEvent()->trigger('acumulus.invoice.sent', $args);
41
+ }
42
+
43
+ /**
44
+ * @return \Event
45
+ */
46
+ private function getEvent()
47
+ {
48
+ return Registry::getInstance()->event;
49
+ }
50
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Shop;
3
+
4
+ use Siel\Acumulus\Invoice\Source;
5
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
6
+ use Siel\Acumulus\Shop\AcumulusEntryModel as BaseAcumulusEntryModel;
7
+
8
+ /**
9
+ * Implements the OpenCart specific acumulus entry model class.
10
+ *
11
+ * In WooCommerce this data is stored as metadata. As such, the "records"
12
+ * returned here are an array of all metadata values, thus not filtered by
13
+ * Acumulus keys.
14
+ */
15
+ class AcumulusEntryModel extends BaseAcumulusEntryModel
16
+ {
17
+ /** @var string */
18
+ protected $tableName;
19
+
20
+ /**
21
+ * AcumulusEntryModel constructor.
22
+ */
23
+ public function __construct()
24
+ {
25
+ $this->tableName = DB_PREFIX . 'acumulus_entry';
26
+ }
27
+
28
+ /**
29
+ * {@inheritdoc}
30
+ */
31
+ public function getByEntryId($entryId)
32
+ {
33
+ $result = $this->getDb()->query(sprintf('SELECT * FROM %s WHERE entry_id = %u', $this->tableName, $entryId));
34
+ return empty($result->row) ? $result->row : null;
35
+ }
36
+
37
+ /**
38
+ * {@inheritdoc}
39
+ */
40
+ public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId)
41
+ {
42
+ $result = $this->getDb()->query(sprintf("SELECT * FROM `%s` WHERE source_type = '%s' AND source_id = %u", $this->tableName, $invoiceSourceType, $invoiceSourceId));
43
+ return !empty($result->row) ? $result->row : null;
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ */
49
+ protected function insert($invoiceSource, $entryId, $token, $created)
50
+ {
51
+ if ($invoiceSource->getType() === Source::Order) {
52
+ $storeId = $invoiceSource->getSource();
53
+ $storeId = $storeId['store_id'];
54
+ } else {
55
+ $storeId = 0;
56
+ }
57
+ return (bool) $this->getDb()->query(sprintf("INSERT INTO `%s` (store_id, entry_id, token, source_type, source_id, updated) VALUES (%d, %d, '%s', '%s', %d, '%s')",
58
+ $this->tableName, $storeId, $entryId, $token, $invoiceSource->getType(), $invoiceSource->getId(), $created));
59
+ }
60
+
61
+ /**
62
+ * {@inheritdoc}
63
+ */
64
+ protected function update($record, $entryId, $token, $updated)
65
+ {
66
+ return (bool) $this->getDb()->query(sprintf("UPDATE `%s` SET entry_id = %u, token = '%s', updated = '%s' WHERE id = %u",
67
+ $this->tableName, $entryId, $token, $updated, $record['id']));
68
+ }
69
+
70
+ /** @noinspection PhpUndefinedNamespaceInspection */
71
+ /** @noinspection PhpUndefinedClassInspection */
72
+ /**
73
+ * Helper method to get the db object.
74
+ *
75
+ * @return \DBMySQLi|\DB\MySQLi
76
+ */
77
+ protected function getDb()
78
+ {
79
+ return Registry::getInstance()->db;
80
+ }
81
+
82
+ /**
83
+ * {@inheritdoc}
84
+ */
85
+ protected function sqlNow()
86
+ {
87
+ return date('Y-m-d H:i:s');
88
+ }
89
+
90
+ /**
91
+ * {@inheritdoc}
92
+ */
93
+ public function install()
94
+ {
95
+ $queryResult = $this->getDb()->query("show tables like '{$this->tableName}'");
96
+ $tableExists = !empty($queryResult->num_rows);
97
+ if (!$tableExists) {
98
+ // Table does not exist: create it.
99
+ return $this->createTable();
100
+ } else {
101
+ // Table does exist: but in old or current data model?
102
+ $columnExists = $this->getDb()->query("show columns from `{$this->tableName}` like 'source_type'");
103
+ $columnExists = !empty($columnExists->num_rows);
104
+ if (!$columnExists) {
105
+ // Table exists but in old data model: alter table
106
+ // Rename currently existing table.
107
+ $oldTableName = $this->tableName . '_old';
108
+ $result = $this->getDb()->query("ALTER TABLE `{$this->tableName}` RENAME `$oldTableName`;");
109
+
110
+ // Create table in new data model.
111
+ $result = $this->createTable() && $result;
112
+
113
+ // Copy data from old to new table.
114
+ // Orders only, credit slips were not supported in that version.
115
+ // Nor did we support multi store shops (though a join could add that).
116
+ $result = $result && $this->getDb()->query("insert into `{$this->tableName}`
117
+ (entry_id, token, source_type, source_id, created, updated)
118
+ select entry_id, token, 'Order' as source_type, order_id as source_id, created, updated
119
+ from `$oldTableName``;");
120
+
121
+ // Delete old table.
122
+ $result = $result && $this->getDb()->query("DROP TABLE `$oldTableName`");
123
+
124
+ return $result;
125
+ } else {
126
+ // Table exists in current data model.
127
+ return true;
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * {@inheritdoc}
134
+ */
135
+ public function uninstall()
136
+ {
137
+ return (bool) $this->getDb()->query("DROP TABLE `{$this->tableName}`");
138
+ }
139
+
140
+ /**
141
+ *
142
+ *
143
+ *
144
+ * @return bool
145
+ *
146
+ */
147
+ protected function createTable()
148
+ {
149
+ return (bool) $this->getDb()->query("CREATE TABLE IF NOT EXISTS `{$this->tableName}` (
150
+ `id` int(11) NOT NULL AUTO_INCREMENT,
151
+ `store_id` int(11) NOT NULL DEFAULT '0',
152
+ `entry_id` int(11) NOT NULL,
153
+ `token` char(32) NOT NULL,
154
+ `source_type` varchar(32) NOT NULL,
155
+ `source_id` int(11) NOT NULL,
156
+ `created` timestamp DEFAULT CURRENT_TIMESTAMP,
157
+ `updated` timestamp NOT NULL,
158
+ PRIMARY KEY (`id`),
159
+ UNIQUE INDEX `acumulus_idx_entry_id` (`entry_id`),
160
+ UNIQUE INDEX `acumulus_idx_source` (`source_id`, `source_type`)
161
+ )");
162
+ }
163
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/BatchForm.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Shop;
3
+
4
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
5
+ use Siel\Acumulus\Shop\BatchForm as BaseBatchForm;
6
+
7
+ /**
8
+ * Provides the Batch send form handling for the VirtueMart Acumulus module.
9
+ */
10
+ class BatchForm extends BaseBatchForm
11
+ {
12
+ /**
13
+ * {@inheritdoc}
14
+ */
15
+ public function isSubmitted()
16
+ {
17
+ return $this->getRequest()->server['REQUEST_METHOD'] == 'POST';
18
+ }
19
+
20
+ /**
21
+ * return \Request
22
+ */
23
+ private function getRequest()
24
+ {
25
+ return Registry::getInstance()->request;
26
+ }
27
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/ConfigForm.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Shop;
3
+
4
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
5
+ use Siel\Acumulus\Shop\ConfigForm as BaseConfigForm;
6
+ use Siel\Acumulus\Shop\ConfigInterface;
7
+
8
+ /**
9
+ * Class ConfigForm processes and builds the settings form page for the
10
+ * PrestaShop Acumulus module.
11
+ */
12
+ class ConfigForm extends BaseConfigForm
13
+ {
14
+ /**
15
+ * {@inheritdoc}
16
+ */
17
+ public function isSubmitted()
18
+ {
19
+ return $this->getRequest()->server['REQUEST_METHOD'] == 'POST';
20
+ }
21
+
22
+ /**
23
+ * return \Request
24
+ */
25
+ private function getRequest()
26
+ {
27
+ return Registry::getInstance()->request;
28
+ }
29
+
30
+ /**
31
+ * {@inheritdoc}
32
+ */
33
+ protected function getShopOrderStatuses()
34
+ {
35
+ Registry::getInstance()->load->model('localisation/order_status');
36
+ $states = Registry::getInstance()->model_localisation_order_status->getOrderStatuses();
37
+ $result = array();
38
+ foreach ($states as $state) {
39
+ list($optionValue, $optionText) = array_values($state);
40
+ $result[$optionValue] = $optionText;
41
+ }
42
+ return $result;
43
+ }
44
+
45
+ /**
46
+ * {@inheritdoc}
47
+ *
48
+ * This override removes the 'Use invoice sent' option as OpenCart does not
49
+ * have an event on creating/sending the invoice.
50
+ *
51
+ * @todo: What does OC have?
52
+ */
53
+ protected function getTriggerInvoiceSendEventOptions()
54
+ {
55
+ $result = parent::getTriggerInvoiceSendEventOptions();
56
+ unset($result[ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate]);
57
+ return $result;
58
+ }
59
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/ConfigStore.php ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Shop;
3
+
4
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
5
+ use Siel\Acumulus\Shop\ConfigStore as BaseConfigStore;
6
+ use Siel\Acumulus\Web\ConfigInterface as ServiceConfigInterface;
7
+
8
+ /**
9
+ * Implements the connection to the PrestaShop config component.
10
+ */
11
+ class ConfigStore extends BaSeConfigStore
12
+ {
13
+ const CONFIG_KEY = 'ACUMULUS_';
14
+
15
+ /**
16
+ * {@inheritdoc}
17
+ */
18
+ public function getShopEnvironment()
19
+ {
20
+ $environment = array(
21
+ // Module has same version as library.
22
+ 'moduleVersion' => ServiceConfigInterface::libraryVersion,
23
+ 'shopName' => $this->shopName,
24
+ 'shopVersion' => VERSION,
25
+ );
26
+ return $environment;
27
+ }
28
+
29
+ /** @noinspection PhpUndefinedClassInspection */
30
+ /**
31
+ * @return \ModelSettingSetting
32
+ */
33
+ protected function getSettings()
34
+ {
35
+ Registry::getInstance()->load->model('setting/setting');
36
+ return Registry::getInstance()->model_setting_setting;
37
+ }
38
+
39
+ /**
40
+ * {@inheritdoc}
41
+ */
42
+ public function load(array $keys)
43
+ {
44
+ $result = array();
45
+ // Load the values from the web shop specific configuration.
46
+ $configurationValues = $this->getSettings()->getSetting('acumulus_siel');
47
+ $configurationValues = isset($configurationValues['acumulus_siel_module']) ? $configurationValues['acumulus_siel_module'] : array();
48
+ foreach ($keys as $key) {
49
+ if (array_key_exists($key, $configurationValues)) {
50
+ $result[$key] = $configurationValues[$key];
51
+ }
52
+ }
53
+ return $result;
54
+ }
55
+
56
+ /**
57
+ * {@inheritdoc}
58
+ */
59
+ public function save(array $values)
60
+ {
61
+ $values = $this->saveCommon($values);
62
+
63
+ $setting = $this->getSettings()->getSetting('acumulus_siel');
64
+ if (!isset($setting['acumulus_siel_module'])) {
65
+ $setting['acumulus_siel_module'] = array();
66
+ }
67
+ $setting['acumulus_siel_module'] = array_merge($setting['acumulus_siel_module'], $values);
68
+ $this->getSettings()->editSetting('acumulus_siel', $setting);
69
+ return true;
70
+ }
71
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/OpenCart/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\OpenCart\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\OpenCart\Helpers\Registry;
6
+ use Siel\Acumulus\PrestaShop\Invoice\Source;
7
+ use Siel\Acumulus\Shop\Config;
8
+ use Siel\Acumulus\Shop\InvoiceManager as BaseInvoiceManager;
9
+
10
+ class InvoiceManager extends BaseInvoiceManager
11
+ {
12
+ /** @var array */
13
+ protected $tableInfo;
14
+
15
+ /** @var string */
16
+ protected $orderTableName;
17
+
18
+ /** @var string */
19
+ protected $returnTableName;
20
+
21
+ /**
22
+ * {@inheritdoc}
23
+ */
24
+ public function __construct(Config $config)
25
+ {
26
+ parent::__construct($config);
27
+ $this->tableInfo = array(
28
+ Source::Order => array(
29
+ 'table' => DB_PREFIX . 'order',
30
+ 'key' => 'order_id',
31
+ ),
32
+ Source::CreditNote => array(
33
+ 'table' => DB_PREFIX . 'return',
34
+ 'key' => 'return_id',
35
+ ),
36
+ );
37
+ }
38
+
39
+ public function getSupportedInvoiceSourceTypes()
40
+ {
41
+ return array(
42
+ Source::Order,
43
+ //Source::CreditNote,
44
+ );
45
+ }
46
+
47
+
48
+ /** @noinspection PhpUndefinedNamespaceInspection */
49
+ /** @noinspection PhpUndefinedClassInspection */
50
+ /**
51
+ * Helper method to get the db object.
52
+ *
53
+ * @return \DBMySQLi|\DB\MySQLi
54
+ */
55
+ protected function getDb()
56
+ {
57
+ return Registry::getInstance()->db;
58
+ }
59
+
60
+ /**
61
+ * {@inheritdoc}
62
+ */
63
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
64
+ {
65
+ $key = $this->tableInfo[$invoiceSourceType]['key'];
66
+ $table = $this->tableInfo[$invoiceSourceType]['table'];
67
+ $query = sprintf("SELECT `%s` FROM `%s` WHERE `%s` BETWEEN %u AND %u", $key, $table, $key, $InvoiceSourceIdFrom, $InvoiceSourceIdTo);
68
+ $result = $this->getDb()->query($query);
69
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $this->getCol($result->rows, $key));
70
+ }
71
+
72
+ /**
73
+ * {@inheritdoc}
74
+ */
75
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
76
+ {
77
+ $dateFrom = $this->getSqlDate($dateFrom);
78
+ $dateTo = $this->getSqlDate($dateTo);
79
+ $dateField = 'date_modified';
80
+ $key = $this->tableInfo[$invoiceSourceType]['key'];
81
+ $table = $this->tableInfo[$invoiceSourceType]['table'];
82
+ $query = sprintf("SELECT `%s` FROM `%s` WHERE `%s` BETWEEN '%s' AND '%s'", $key, $table, $dateField, $dateFrom, $dateTo);
83
+ $result = $this->getDb()->query($query);
84
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $this->getCol($result->rows, $key));
85
+ }
86
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/FormMapper.php ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Form;
5
+
6
+ /**
7
+ * FormMapper maps an Acumulus form definition to a PrestaShop form definition.
8
+ */
9
+ class FormMapper
10
+ {
11
+ /**
12
+ * Maps a set of field definitions.
13
+ *
14
+ * @param Form $form
15
+ *
16
+ * @return array[]
17
+ */
18
+ public function map(Form $form)
19
+ {
20
+ return $this->fields($form->getFields());
21
+ }
22
+
23
+ /**
24
+ * Maps a set of field definitions.
25
+ *
26
+ * @param array[] $fields
27
+ *
28
+ * @return array[]
29
+ */
30
+ protected function fields(array $fields)
31
+ {
32
+ $result = array();
33
+ foreach ($fields as $id => $field) {
34
+ if (!isset($field['id'])) {
35
+ $field['id'] = $id;
36
+ }
37
+ if (!isset($field['name'])) {
38
+ $field['name'] = $id;
39
+ }
40
+ $result[$id] = $this->field($field);
41
+ }
42
+ return $result;
43
+ }
44
+
45
+ /**
46
+ * Maps a single field definition, possibly a fieldset.
47
+ *
48
+ * @param array $field
49
+ * Field(set) definition.
50
+ *
51
+ * @return array
52
+ */
53
+ protected function field(array $field)
54
+ {
55
+ if ($field['type'] === 'fieldset') {
56
+ $result = $this->fieldset($field);
57
+ } else {
58
+ $result = $this->element($field);
59
+ }
60
+ return $result;
61
+ }
62
+
63
+ /**
64
+ * Returns a mapped fieldset.
65
+ *
66
+ * @param array $field
67
+ *
68
+ * @return array[]
69
+ */
70
+ protected function fieldset(array $field)
71
+ {
72
+ $result = array(
73
+ 'form' => array(
74
+ 'legend' => array(
75
+ 'title' => $field['legend'],
76
+ ),
77
+ 'input' => $this->fields($field['fields']),
78
+ ),
79
+ );
80
+ if (isset($field['icon'])) {
81
+ $result['form']['legend']['icon'] = $field['icon'];
82
+ }
83
+ return $result;
84
+ }
85
+
86
+ /**
87
+ * Returns a mapped simple element.
88
+ *
89
+ * @param array $field
90
+ *
91
+ * @return array
92
+ *
93
+ */
94
+ protected function element(array $field)
95
+ {
96
+ $result = array(
97
+ 'type' => $this->getPrestaShopType($field['type']),
98
+ 'label' => isset($field['label']) ? $field['label'] : '',
99
+ 'name' => $field['name'],
100
+ 'required' => isset($field['attributes']['required']) ? $field['attributes']['required'] : false,
101
+ 'multiple' => isset($field['attributes']['multiple']) ? $field['attributes']['multiple'] : false,
102
+ );
103
+
104
+ if (isset($field['attributes'])) {
105
+ $result['attributes'] = $field['attributes'];
106
+ }
107
+ if (isset($field['description'])) {
108
+ $result['desc'] = $field['description'];
109
+ }
110
+
111
+ if ($field['type'] === 'radio') {
112
+ $result['values'] = $this->getPrestaShopValues($field['name'], $field['options']);
113
+ } else if ($field['type'] === 'checkbox') {
114
+ $result['values'] = $this->getPrestaShopOptions($field['options']);
115
+ } else if ($field['type'] === 'select') {
116
+ $result['options'] = $this->getPrestaShopOptions($field['options']);
117
+ if ($result['multiple']) {
118
+ $result['size'] = $field['attributes']['size'];
119
+ }
120
+ }
121
+
122
+ return $result;
123
+ }
124
+
125
+ /**
126
+ * Returns the PrestaShop form element type for the given Acumulus type string.
127
+ *
128
+ * @param string $type
129
+ *
130
+ * @return string
131
+ */
132
+ protected function getPrestaShopType($type)
133
+ {
134
+ switch ($type) {
135
+ case 'fieldset':
136
+ case 'markup':
137
+ $type = 'free';
138
+ break;
139
+ case 'email':
140
+ $type = 'text';
141
+ break;
142
+ default:
143
+ // Return as is: text, password, textarea, radio, select, checkbox,
144
+ // date. PrestaShop accepts these as are.
145
+ break;
146
+ }
147
+ return $type;
148
+ }
149
+
150
+ /**
151
+ * Converts a list of Acumulus field options to a list of PrestaShop radio
152
+ * button values.
153
+ *
154
+ * @param string $id
155
+ * @param array $options
156
+ *
157
+ * @return array A list of PrestaShop radio button options.
158
+ * A list of PrestaShop radio button options.
159
+ */
160
+ protected function getPrestaShopValues($id, array $options)
161
+ {
162
+ $result = array();
163
+ foreach ($options as $value => $label) {
164
+ $result[] = array(
165
+ 'id' => $id . $value,
166
+ 'value' => $value,
167
+ 'label' => $label,
168
+ );
169
+ }
170
+ return $result;
171
+ }
172
+
173
+ /**
174
+ * Converts a list of Acumulus field options to a list of PrestaShop radio
175
+ * button values.
176
+ *
177
+ * @param array $options
178
+ *
179
+ * @return array A list of PrestaShop radio button options.
180
+ * A list of PrestaShop radio button options.
181
+ */
182
+ protected function getPrestaShopOptions(array $options)
183
+ {
184
+ $result = array(
185
+ 'query' => array(),
186
+ 'id' => 'id',
187
+ 'name' => 'name',
188
+ );
189
+
190
+ foreach ($options as $value => $label) {
191
+ $result['query'][] = array(
192
+ 'id' => $value,
193
+ 'name' => $label,
194
+ );
195
+ }
196
+
197
+ return $result;
198
+ }
199
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/Log.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Helpers;
3
+
4
+ use PrestaShopLogger;
5
+ use Siel\Acumulus\Helpers\Log as BaseLog;
6
+
7
+ /**
8
+ * Extends the base log class to log any library logging to the PrestaShop log.
9
+ */
10
+ class Log extends BaseLog
11
+ {
12
+ /**
13
+ * {@inheritdoc}
14
+ *
15
+ * This override uses the PrestaShopLogger.
16
+ */
17
+ protected function write($message, $severity)
18
+ {
19
+ PrestaShopLogger::addLog($message, $this->getPrestaShopSeverity($severity));
20
+ }
21
+
22
+ /**
23
+ * Returns the PrestaShop equivalent of the severity.
24
+ *
25
+ * @param int $severity
26
+ * One of the constants of the base Log class.
27
+ *
28
+ * @return int
29
+ * The PrestaShop equivalent of the severity.
30
+ */
31
+ protected function getPrestaShopSeverity($severity)
32
+ {
33
+ switch ($severity) {
34
+ case Log::Error:
35
+ return 3;
36
+ case Log::Warning:
37
+ return 2;
38
+ case Log::Notice:
39
+ return 1;
40
+ case Log::Debug:
41
+ default:
42
+ return 1;
43
+ }
44
+ }
45
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/Mailer.php ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Helpers;
3
+
4
+ use Configuration;
5
+ use Language;
6
+ use Mail;
7
+ use Siel\Acumulus\Helpers\Mailer as BaseMailer;
8
+
9
+ /**
10
+ * Extends the base mailer class to send a mail using the PrestaShop mailer.
11
+ */
12
+ class Mailer extends BaseMailer
13
+ {
14
+ /** @var string */
15
+ protected $templateDir;
16
+
17
+ /** @var string */
18
+ protected $templateName;
19
+
20
+ /**
21
+ * {@inheritdoc}
22
+ */
23
+ public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
24
+ {
25
+ $this->templateDir = dirname(__FILE__) . '/mails/';
26
+ $this->templateName = 'message';
27
+ $body = $this->getBody($result, $messages, $invoiceSourceType, $invoiceSourceReference);
28
+ $this->writeTemplateFile($body);
29
+
30
+ $languageId = Language::getIdByIso($this->translator->getLanguage());
31
+ $title = $this->getSubject($result);
32
+ $templateVars = array();
33
+ $toEmail = $this->getToAddress();
34
+ $toName = Configuration::get('PS_SHOP_NAME');
35
+ $from = Configuration::get('PS_SHOP_EMAIL');
36
+ $fromName = Configuration::get('PS_SHOP_NAME');
37
+
38
+ $result = Mail::Send($languageId, $this->templateName, $title, $templateVars, $toEmail, $toName, $from, $fromName, null, null, $this->templateDir);
39
+
40
+ // Clear the template files as they contain privacy sensitive data.
41
+ $this->writeTemplateFiles(array('body' => '', 'text' => ''));
42
+
43
+ return $result;
44
+ }
45
+
46
+ /**
47
+ * Writes the mail bodies (html and text) to template files as used by the
48
+ * PrestaShop mailer.
49
+ *
50
+ * @param array $body
51
+ */
52
+ protected function writeTemplateFile(array $body)
53
+ {
54
+ $languageIso = $this->translator->getLanguage();
55
+ $templateBaseName = $this->templateDir . $languageIso . '/' . $this->templateName;
56
+ file_put_contents($templateBaseName . '.html', !empty($body['html']) ? $body['html'] : '');
57
+ file_put_contents($templateBaseName . '.txt', !empty($body['text']) ? $body['text'] : '');
58
+ }
59
+
60
+ /**
61
+ * Writes the mail bodies (html and text) to template files as used by the
62
+ * PrestaShop mailer.
63
+ *
64
+ * @param array $body
65
+ */
66
+ protected function writeTemplateFiles(array $body)
67
+ {
68
+ $languageIso = $this->translator->getLanguage();
69
+ $templateBaseName = $this->templateDir . $languageIso . '/' . $this->templateName;
70
+ file_put_contents($templateBaseName . '.html', !empty($body['html']) ? $body['html'] : '');
71
+ file_put_contents($templateBaseName . '.txt', !empty($body['text']) ? $body['text'] : '');
72
+ }
73
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/.htaccess ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # Apache/PHP/Drupal settings:
3
+ #
4
+
5
+ # Protect files and directories from prying eyes.
6
+ <FilesMatch "\.(html|txt)$">
7
+ Order allow,deny
8
+ </FilesMatch>
9
+
10
+ # Don't show directory listings for URLs which map to a directory.
11
+ Options -Indexes
12
+
13
+ # Follow symbolic links in this directory.
14
+ Options +FollowSymLinks
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/en/message.html ADDED
File without changes
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/en/message.txt ADDED
File without changes
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/nl/message.html ADDED
File without changes
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Helpers/mails/nl/message.txt ADDED
File without changes
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Invoice/Creator.php ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Invoice;
3
+
4
+ use Address;
5
+ use Carrier;
6
+ use Configuration;
7
+ use Country;
8
+ use Customer;
9
+ use Order;
10
+ use OrderPayment;
11
+ use OrderSlip;
12
+ use Siel\Acumulus\Helpers\Number;
13
+ use Siel\Acumulus\Invoice\ConfigInterface;
14
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
15
+
16
+ /**
17
+ * Allows to create arrays in the Acumulus invoice structure from a PrestaShop
18
+ * order or order slip.
19
+ *
20
+ * Notes:
21
+ * - If needed, PrestaShop allows us to get tax rates by querying the tax table
22
+ * because as soon as an existing tax rate gets updated it will get a new id,
23
+ * so old order details still point to a tax record with the tax rate as was
24
+ * used at the moment the order was placed.
25
+ * - Fixed in 1.6.1.1: bug in partial refund, not executed the hook
26
+ * actionOrderSlipAdd #PSCSX-6287. So before 1.6.1.1, partial refunds will not
27
+ * be automatically sent to Acumulus.
28
+ * - Credit notes can get a correction line. They get one if the total amount
29
+ * does not match the sum of the lines added so far. This can happen if an
30
+ * amount was entered manually, or if discount(s) applied during the sale were
31
+ * subtracted from the credit amount but we could not find which discounts
32
+ * this were. However:
33
+ * - amount is excl vat if not manually entered.
34
+ * - amount is incl vat if manually entered (assuming administrators enter
35
+ * amounts incl tax, and this is what gets listed on the credit PDF.
36
+ * - shipping_cost_amount is excl vat.
37
+ * So this is never going to work in all situations!!!
38
+ *
39
+ * @todo: So, can we get a tax amount/rate over the manually entered refund?
40
+ */
41
+ class Creator extends BaseCreator
42
+ {
43
+ // More specifically typed property.
44
+ /** @var Source */
45
+ protected $invoiceSource;
46
+
47
+ /** @var Order */
48
+ protected $order;
49
+
50
+ /** @var OrderSlip */
51
+ protected $creditSlip;
52
+
53
+ /**
54
+ * {@inheritdoc}
55
+ *
56
+ * This override also initializes WooCommerce specific properties related to
57
+ * the source.
58
+ */
59
+ protected function setInvoiceSource($invoiceSource)
60
+ {
61
+ parent::setInvoiceSource($invoiceSource);
62
+ switch ($this->invoiceSource->getType()) {
63
+ case Source::Order:
64
+ $this->order = $this->invoiceSource->getSource();
65
+ break;
66
+ case Source::CreditNote:
67
+ $this->creditSlip = $this->invoiceSource->getSource();
68
+ $this->order = new Order($this->creditSlip->id_order);
69
+ break;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * {@inheritdoc}
75
+ */
76
+ protected function getCustomer()
77
+ {
78
+ $customer = new Customer($this->invoiceSource->getSource()->id_customer);
79
+ $invoiceAddress = new Address($this->order->id_address_invoice);
80
+
81
+ $result = array();
82
+
83
+ $this->addIfNotEmpty($result, 'contactyourid', $customer->id);
84
+ $this->addEmpty($result, 'companyname1', $invoiceAddress->company);
85
+ $result['fullname'] = $invoiceAddress->firstname . ' ' . $invoiceAddress->lastname;
86
+ $this->addEmpty($result, 'address1', $invoiceAddress->address1);
87
+ $this->addEmpty($result, 'address2', $invoiceAddress->address2);
88
+ $this->addEmpty($result, 'postalcode', $invoiceAddress->postcode);
89
+ $this->addEmpty($result, 'city', $invoiceAddress->city);
90
+ if ($invoiceAddress->id_country) {
91
+ $result['countrycode'] = Country::getIsoById($invoiceAddress->id_country);
92
+ }
93
+ $this->addIfNotEmpty($result, 'vatnumber', $invoiceAddress->vat_number);
94
+ // Add either mobile or phone number to 'telephone'.
95
+ $this->addIfNotEmpty($result, 'telephone', $invoiceAddress->phone);
96
+ $this->addIfNotEmpty($result, 'telephone', $invoiceAddress->phone_mobile);
97
+ $result['email'] = $customer->email;
98
+
99
+ return $result;
100
+ }
101
+
102
+ /**
103
+ * {@inheritdoc}
104
+ */
105
+ protected function searchProperty($property)
106
+ {
107
+ $invoiceAddress = new Address($this->order->id_address_invoice);
108
+ $value = $this->getProperty($property, $invoiceAddress);
109
+ if (empty($value)) {
110
+ $customer = new Customer($this->invoiceSource->getSource()->id_customer);
111
+ $value = $this->getProperty($property, $customer);
112
+ }
113
+ if (empty($value)) {
114
+ $value = parent::searchProperty($property);
115
+ }
116
+ return $value;
117
+ }
118
+
119
+ /**
120
+ * {@inheritdoc}
121
+ */
122
+ protected function getInvoiceNumber($invoiceNumberSource)
123
+ {
124
+ $result = $this->invoiceSource->getReference();
125
+ if ($invoiceNumberSource === ConfigInterface::InvoiceNrSource_ShopInvoice && $this->invoiceSource->getType() === Source::Order && !empty($this->order->invoice_number)) {
126
+ $result = Configuration::get('PS_INVOICE_PREFIX', (int) $this->order->id_lang, null, $this->order->id_shop) . sprintf('%06d', $this->order->invoice_number);
127
+ }
128
+ return $result;
129
+ }
130
+
131
+ /**
132
+ * {@inheritdoc}
133
+ */
134
+ protected function getInvoiceDate($dateToUse)
135
+ {
136
+ $result = substr($this->invoiceSource->getSource()->date_add, 0, strlen('2000-01-01'));
137
+ // Invoice_date is filled with "0000-00-00 00:00:00", so use invoice
138
+ // number instead to check for existence of the invoice.
139
+ if ($dateToUse == ConfigInterface::InvoiceDate_InvoiceCreate && $this->invoiceSource->getType() === Source::Order && !empty($this->order->invoice_number)) {
140
+ $result = substr($this->order->invoice_date, 0, strlen('2000-01-01'));
141
+ }
142
+ return $result;
143
+ }
144
+
145
+ /**
146
+ * {@inheritdoc}
147
+ */
148
+ protected function getPaymentState()
149
+ {
150
+ // Assumption: credit slips are always in a paid state.
151
+ if (($this->invoiceSource->getType() === Source::Order && $this->order->hasBeenPaid()) || $this->invoiceSource->getType() === Source::CreditNote) {
152
+ $result = ConfigInterface::PaymentStatus_Paid;
153
+ } else {
154
+ $result = ConfigInterface::PaymentStatus_Due;
155
+ }
156
+ return $result;
157
+ }
158
+
159
+ /**
160
+ * {@inheritdoc}
161
+ */
162
+ protected function getPaymentDate()
163
+ {
164
+ if ($this->invoiceSource->getType() === Source::Order) {
165
+ $paymentDate = null;
166
+ foreach ($this->order->getOrderPaymentCollection() as $payment) {
167
+ /** @var OrderPayment $payment */
168
+ if ($payment->date_add && (!$paymentDate || $payment->date_add > $paymentDate)) {
169
+ $paymentDate = $payment->date_add;
170
+ }
171
+ }
172
+ } else {
173
+ // Assumption: last modified date is date of actual reimbursement.
174
+ $paymentDate = $this->creditSlip->date_upd;
175
+ }
176
+
177
+ $result = $paymentDate ? substr($paymentDate, 0, strlen('2000-01-01')) : null;
178
+ return $result;
179
+ }
180
+
181
+ /**
182
+ * {@inheritdoc}
183
+ *
184
+ * This override provides the values meta-invoice-amountinc and
185
+ * meta-invoice-amount.
186
+ */
187
+ protected function getInvoiceTotals()
188
+ {
189
+ $sign = $this->getSign();
190
+ if ($this->invoiceSource->getType() === Source::Order) {
191
+ $amount = $this->order->getTotalProductsWithoutTaxes()
192
+ + $this->order->total_shipping_tax_excl
193
+ + $this->order->total_wrapping_tax_excl
194
+ - $this->order->total_discounts_tax_excl;
195
+ $amountInc = $this->order->getTotalProductsWithTaxes()
196
+ + $this->order->total_shipping_tax_incl
197
+ + $this->order->total_wrapping_tax_incl
198
+ - $this->order->total_discounts_tax_incl;
199
+ } else {
200
+ // On credit notes, the amount ex VAT will not have been corrected for
201
+ // discounts that are subtracted from the refund. This will be corrected
202
+ // later in getDiscountLinesCreditNote().
203
+ /** @noinspection PhpUndefinedFieldInspection */
204
+ $amount = $this->creditSlip->total_products_tax_excl
205
+ + $this->creditSlip->total_shipping_tax_excl;
206
+ /** @noinspection PhpUndefinedFieldInspection */
207
+ $amountInc = $this->creditSlip->total_products_tax_incl
208
+ + $this->creditSlip->total_shipping_tax_incl;
209
+ }
210
+
211
+ return array(
212
+ 'meta-invoice-amountinc' => $sign * $amountInc,
213
+ 'meta-invoice-amount' => $sign * $amount,
214
+ );
215
+ }
216
+
217
+ /**
218
+ * {@inheritdoc}
219
+ */
220
+ protected function getItemLines()
221
+ {
222
+ $result = array();
223
+ if ($this->invoiceSource->getType() === Source::Order) {
224
+ // Note: getOrderDetailTaxes() is new in 1.6.1.0.
225
+ $lines = method_exists($this->order, 'getOrderDetailTaxes')
226
+ ? $this->mergeProductLines($this->order->getProductsDetail(), $this->order->getOrderDetailTaxes())
227
+ : $this->order->getProductsDetail();
228
+ } else {
229
+ $lines = $this->creditSlip->getOrdersSlipProducts($this->invoiceSource->getId(), $this->order);
230
+ }
231
+
232
+ foreach ($lines as $line) {
233
+ $result[] = $this->getItemLine($line);
234
+ }
235
+ return $result;
236
+ }
237
+
238
+ /**
239
+ * Merges the product and tax details arrays.
240
+ *
241
+ * @param array $productLines
242
+ * @param array $taxLines
243
+ *
244
+ * @return array
245
+ */
246
+ public function mergeProductLines(array $productLines, array $taxLines)
247
+ {
248
+ $result = array();
249
+ // Key the product lines on id_order_detail, so we can easily add the tax
250
+ // lines in the 2nd loop.
251
+ foreach ($productLines as $productLine) {
252
+ $result[$productLine['id_order_detail']] = $productLine;
253
+ }
254
+ // Add the tax lines without overwriting existing entries (though in a
255
+ // consistent db the same keys should contain the same values).
256
+ foreach ($taxLines as $taxLine) {
257
+ $result[$taxLine['id_order_detail']] += $taxLine;
258
+ }
259
+ return $result;
260
+ }
261
+
262
+ /**
263
+ * Returns 1 item line, both for an order or credit slip.
264
+ *
265
+ * @param array $item
266
+ * An array of an OrderDetail line combined with a tax detail line OR
267
+ * an array with an OrderSlipDetail line.
268
+ *
269
+ * @return array
270
+ */
271
+ protected function getItemLine(array $item)
272
+ {
273
+ $result = array();
274
+ $sign = $this->getSign();
275
+
276
+ $this->addIfNotEmpty($result, 'itemnumber', $item['product_upc']);
277
+ $this->addIfNotEmpty($result, 'itemnumber', $item['product_ean13']);
278
+ $this->addIfNotEmpty($result, 'itemnumber', $item['product_supplier_reference']);
279
+ $this->addIfNotEmpty($result, 'itemnumber', $item['product_reference']);
280
+ $result['product'] = $item['product_name'];
281
+
282
+ // Prestashop does not support the margin scheme. So in a standard install
283
+ // this method will always return false. But if this method happens to
284
+ // return true anyway (customisation, hook), the costprice will trigger
285
+ // vattype = 5 for Acumulus.
286
+ if ($this->allowMarginScheme() && !empty($item['purchase_supplier_price'])) {
287
+ // Margin scheme:
288
+ // - Do not put VAT on invoice: send price incl VAT as unitprice.
289
+ // - But still send the VAT rate to Acumulus.
290
+ $result['unitprice'] = $sign * $item['unit_price_tax_incl'];
291
+ // Costprice > 0 is the trigger for Acumulus to use the margin scheme.
292
+ $result['costprice'] = $sign * $item['purchase_supplier_price'];
293
+ } else {
294
+ // Unit price is without VAT, so use product_price, not product_price_wt.
295
+ $result['unitprice'] = $sign * $item['unit_price_tax_excl'];
296
+ $result['unitpriceinc'] = $sign * $item['unit_price_tax_incl'];
297
+ $result['meta-line-price'] = $sign * $item['total_price_tax_excl'];
298
+ $result['meta-line-priceinc'] = $sign * $item['total_price_tax_incl'];
299
+ }
300
+ $result['quantity'] = $item['product_quantity'];
301
+ // The field 'rate' comes from order->getOrderDetailTaxes() and is only
302
+ // defined for orders and were not filled in or before PS1.6.1.1. So, check
303
+ // if the field is available.
304
+ // The fields 'unit_amount' and 'total_amount' (table order_detail_tax) are
305
+ // based on the discounted product price and thus cannot be used.
306
+ if (isset($item['rate'])) {
307
+ $result['vatrate'] = $item['rate'];
308
+ $result['meta-vatrate-source'] = Creator::VatRateSource_Exact;
309
+ if (!Number::floatsAreEqual($item['unit_amount'], $result['unitpriceinc'] - $result['unitprice'])) {
310
+ $result['meta-line-discount-vatamount'] = $item['unit_amount'] - ($result['unitpriceinc'] - $result['unitprice']);
311
+ }
312
+ } else {
313
+ $result += $this->getVatRangeTags($sign * ($item['unit_price_tax_incl'] - $item['unit_price_tax_excl']), $sign * $item['unit_price_tax_excl'], 0.02);
314
+ }
315
+ $result['meta-calculated-fields'] = 'vatamount';
316
+
317
+ return $result;
318
+ }
319
+
320
+ /**
321
+ * {@inheritdoc}
322
+ */
323
+ protected function getShippingLine()
324
+ {
325
+ $sign = $this->getSign();
326
+ $shippingEx = $sign * $this->invoiceSource->getSource()->total_shipping_tax_excl;
327
+ $shippingInc = $sign * $this->invoiceSource->getSource()->total_shipping_tax_incl;
328
+ $shippingVat = $shippingInc - $shippingEx;
329
+
330
+ if (!Number::isZero($shippingInc)) {
331
+ $result = array(
332
+ 'product' => $this->t('shipping_costs'),
333
+ 'unitprice' => $shippingEx,
334
+ 'unitpriceinc' => $shippingInc,
335
+ 'quantity' => 1,
336
+ ) + $this->getVatRangeTags($shippingVat, $shippingEx, 0.02);
337
+ $result['meta-calculated-fields'] = 'vatamount';
338
+ } else {
339
+ $carrier = new Carrier($this->order->id_carrier);
340
+ $description = $carrier->id_reference == 1 ? $this->t('pickup') : $this->t('free_shipping');
341
+ $result = array(
342
+ 'product' => $description,
343
+ 'unitprice' => 0,
344
+ 'unitpriceinc' => 0,
345
+ 'quantity' => 1,
346
+ 'vatamount' => 0,
347
+ 'vatrate' => null,
348
+ 'meta-vatrate-source' => Creator::VatRateSource_Completor,
349
+ );
350
+ }
351
+
352
+ return $result;
353
+ }
354
+
355
+ /**
356
+ * {@inheritdoc}
357
+ *
358
+ * This override returns can return an invoice line for orders. Credit slips
359
+ * cannot have a wrapping line.
360
+ */
361
+ protected function getGiftWrappingLine()
362
+ {
363
+ $result = array();
364
+ if ($this->invoiceSource->getType() === Source::Order && $this->order->gift && !Number::isZero($this->order->total_wrapping_tax_incl)) {
365
+ $wrappingEx = $this->order->total_wrapping_tax_excl;
366
+ $wrappingInc = $this->order->total_wrapping_tax_incl;
367
+ $wrappingVat = $wrappingInc - $wrappingEx;
368
+ $result = array(
369
+ 'product' => $this->t('gift_wrapping'),
370
+ 'unitprice' => $wrappingEx,
371
+ 'unitpriceinc' => $wrappingInc,
372
+ 'quantity' => 1,
373
+ ) + $this->getVatRangeTags($wrappingVat, $wrappingEx, 0.02);
374
+ $result['meta-calculated-fields'] = 'vatamount';
375
+ }
376
+ return $result;
377
+ }
378
+
379
+ /**
380
+ * In a Prestashop order the discount lines are specified in Order cart rules
381
+ *
382
+ * @return array[]
383
+ */
384
+ protected function getDiscountLinesOrder()
385
+ {
386
+ $result = array();
387
+
388
+ foreach ($this->order->getCartRules() as $line) {
389
+ $result[] = $this->getDiscountLineOrder($line);
390
+ }
391
+
392
+ return $result;
393
+ }
394
+
395
+ /**
396
+ * In a Prestashop order the discount lines are specified in Order cart rules
397
+ * that have, a.o, the following fields:
398
+ * - value: total amount inc VAT
399
+ * - value_tax_excl: total amount ex VAT
400
+ *
401
+ * @param array $line
402
+ *
403
+ * @return array
404
+ */
405
+ protected function getDiscountLineOrder(array $line)
406
+ {
407
+ $sign = $this->getSign();
408
+ $discountInc = -$sign * $line['value'];
409
+ $discountEx = -$sign * $line['value_tax_excl'];
410
+ $discountVat = $discountInc - $discountEx;
411
+ $result = array(
412
+ 'itemnumber' => $line['id_cart_rule'],
413
+ 'product' => $this->t('discount_code') . ' ' . $line['name'],
414
+ 'unitprice' => $discountEx,
415
+ 'unitpriceinc' => $discountInc,
416
+ 'quantity' => 1,
417
+ // If no match is found, this line may be split.
418
+ 'meta-strategy-split' => true,
419
+ // Assuming that the fixed discount amount was entered:
420
+ // - including VAT, the precision would be 0.01, 0.01.
421
+ // - excluding VAT, the precision would be 0.01, 0
422
+ // However, for a % discount, it will be: 0.02, 0.01, so use this.
423
+ ) + $this->getVatRangeTags($discountVat, $discountEx, 0.02);
424
+ $result['meta-calculated-fields'] = 'vatamount';
425
+
426
+ return $result;
427
+ }
428
+
429
+ /**
430
+ * In a Prestashop credit slip, the discounts are not visible anymore, but
431
+ * can be computed by looking at the difference between the value of
432
+ * total_products_tax_incl and the sum of the OrderSlipDetail amounts.
433
+ *
434
+ * @return array[]
435
+ */
436
+ protected function getDiscountLinesCreditNote()
437
+ {
438
+ $result = array();
439
+
440
+ // Get total amount credited.
441
+ /** @noinspection PhpUndefinedFieldInspection */
442
+ $creditSlipAmountInc = $this->creditSlip->total_products_tax_incl;
443
+
444
+ // Get sum of product lines.
445
+ $lines = $this->creditSlip->getOrdersSlipProducts($this->invoiceSource->getId(), $this->order);
446
+ $detailsAmountInc = array_reduce($lines, function ($sum, $item) {
447
+ $sum += $item['total_price_tax_incl'];
448
+ return $sum;
449
+ }, 0.0);
450
+
451
+ // We assume that if the total is smaller than the sum, a discount given on
452
+ // the original order has now been subtracted from the amount credited.
453
+ if (!Number::floatsAreEqual($creditSlipAmountInc, $detailsAmountInc, 0.05)
454
+ && $creditSlipAmountInc < $detailsAmountInc
455
+ ) {
456
+ // PS Error: total_products_tax_excl is not adjusted (whereas
457
+ // total_products_tax_incl is) when a discount is subtracted from the
458
+ // amount to be credited.
459
+ // So we cannot calculate the discount ex VAT ourselves.
460
+ // What we can try is the following: Get the order cart rules to see if
461
+ // 1 or all of those match the discount amount here.
462
+ $discountAmountInc = $detailsAmountInc - $creditSlipAmountInc;
463
+ $totalOrderDiscountInc = 0.0;
464
+ // Note: The sign of the entries in $orderDiscounts will be correct.
465
+ $orderDiscounts = $this->getDiscountLinesOrder();
466
+
467
+ foreach ($orderDiscounts as $key => $orderDiscount) {
468
+ if (Number::floatsAreEqual($orderDiscount['unitpriceinc'], $discountAmountInc)) {
469
+ // Return this single line.
470
+ $from = $to = $key;
471
+ break;
472
+ }
473
+ $totalOrderDiscountInc += $orderDiscount['unitpriceinc'];
474
+ if (Number::floatsAreEqual($totalOrderDiscountInc, $discountAmountInc)) {
475
+ // Return all lines up to here.
476
+ $from = 0;
477
+ $to = $key;
478
+ break;
479
+ }
480
+ }
481
+
482
+ if (isset($from) && isset($to)) {
483
+ $result = array_slice($orderDiscounts, $from, $to - $from + 1);
484
+ // Correct meta-invoice-amount.
485
+ $totalOrderDiscountEx = array_reduce($result, function ($sum, $item) {
486
+ $sum += $item['quantity'] * $item['unitprice'];
487
+ return $sum;
488
+ }, 0.0);
489
+ $this->invoice['customer']['invoice']['meta-invoice-amount'] += $totalOrderDiscountEx;
490
+ }
491
+ //else {
492
+ // We could not match a discount with the difference between the total
493
+ // amount credited and the sum of the products returned. A manual line
494
+ // will correct the invoice.
495
+ //}
496
+ }
497
+ return $result;
498
+ }
499
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Invoice/Source.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Invoice;
3
+
4
+ use Configuration;
5
+ use Context;
6
+ use Db;
7
+ use Order;
8
+ use OrderSlip;
9
+ use Siel\Acumulus\Invoice\Source as BaseSource;
10
+
11
+ /**
12
+ * Wraps a PrestaShop order in an invoice source object.
13
+ */
14
+ class Source extends BaseSource
15
+ {
16
+ // More specifically typed properties.
17
+ /** @var Order|OrderSLip */
18
+ protected $source;
19
+
20
+ /**
21
+ * {@inheritdoc}
22
+ */
23
+ protected function setSource()
24
+ {
25
+ if ($this->getType() === Source::Order) {
26
+ $this->source = new Order($this->id);
27
+ } else {
28
+ $this->source = new OrderSlip($this->id);
29
+ $this->addProperties();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ *
36
+ * This override returns the order reference or order slip id.
37
+ */
38
+ public function getReference()
39
+ {
40
+ return $this->getType() === Source::Order
41
+ ? $this->source->reference
42
+ : Configuration::get('PS_CREDIT_SLIP_PREFIX', Context::getContext()->language->id) . sprintf('%06d', $this->source->id);
43
+ }
44
+
45
+ /**
46
+ * Sets the id based on the loaded Order.
47
+ */
48
+ protected function setId()
49
+ {
50
+ $this->id = $this->source->id;
51
+ if ($this->getType() === Source::CreditNote) {
52
+ $this->addProperties();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Returns the status of this order.
58
+ *
59
+ * @return int
60
+ */
61
+ protected function getStatusOrder()
62
+ {
63
+ return $this->source->current_state;
64
+ }
65
+
66
+ /**
67
+ * Returns the status of this credit note.
68
+ *
69
+ * @return null
70
+ */
71
+ protected function getStatusCreditNote()
72
+ {
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * OrderSlip does store but not load the values total_products_tax_excl,
78
+ * total_shipping_tax_excl, total_products_tax_incl, and
79
+ * total_shipping_tax_incl. As we need them, we load them ourselves.
80
+ *
81
+ * @throws \PrestaShopDatabaseException
82
+ */
83
+ protected function addProperties()
84
+ {
85
+ $row = Db::getInstance()->executeS(sprintf("SELECT * FROM `%s` WHERE `%s` = %u",
86
+ _DB_PREFIX_ . OrderSlip::$definition['table'], OrderSlip::$definition['primary'], $this->id));
87
+ // Get 1st (and only) result.
88
+ $row = reset($row);
89
+ foreach ($row as $key => $value) {
90
+ if (!isset($this->source->$key)) {
91
+ $this->source->$key = $value;
92
+ }
93
+ }
94
+ }
95
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Shop;
3
+
4
+ use Db;
5
+ use Siel\Acumulus\PrestaShop\Invoice\Source;
6
+ use Siel\Acumulus\Shop\AcumulusEntryModel as BaseAcumulusEntryModel;
7
+
8
+ /**
9
+ * Implements the PrestaShop specific acumulus entry model class.
10
+ *
11
+ * In WooCommerce this data is stored as metadata. As such, the "records"
12
+ * returned here are an array of all metadata values, thus not filtered by
13
+ * Acumulus keys.
14
+ */
15
+ class AcumulusEntryModel extends BaseAcumulusEntryModel
16
+ {
17
+ /** @var string */
18
+ protected $tableName;
19
+
20
+ /**
21
+ * AcumulusEntryModel constructor.
22
+ */
23
+ public function __construct()
24
+ {
25
+ $this->tableName = _DB_PREFIX_ . 'acumulus_entry';
26
+ }
27
+
28
+ /**
29
+ * {@inheritdoc}
30
+ */
31
+ public function getByEntryId($entryId)
32
+ {
33
+ $result = Db::getInstance()->executeS(sprintf("SELECT * FROM `%s` WHERE id_entry = %u", $this->tableName, $entryId));
34
+ return count($result) === 1 ? reset($result) : null;
35
+ }
36
+
37
+ /**
38
+ * {@inheritdoc}
39
+ */
40
+ public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId)
41
+ {
42
+ $result = Db::getInstance()->executeS(sprintf("SELECT * FROM `%s` WHERE source_type = '%s' AND source_id = %u", $this->tableName, $invoiceSourceType, $invoiceSourceId));
43
+ return count($result) === 1 ? reset($result) : null;
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ */
49
+ protected function insert($invoiceSource, $entryId, $token, $created)
50
+ {
51
+ if ($invoiceSource->getType() === Source::Order) {
52
+ $shopId = $invoiceSource->getSource()->id_shop;
53
+ $shopGroupId = $invoiceSource->getSource()->id_shop_group;
54
+ } else {
55
+ $shopId = 0;
56
+ $shopGroupId = 0;
57
+ }
58
+ return Db::getInstance()->execute(sprintf("INSERT INTO `%s` (id_shop, id_shop_group, id_entry, token, source_type, source_id, updated) VALUES (%u, %u, %u, '%s', '%s', %u, '%s')",
59
+ $this->tableName, $shopId, $shopGroupId, $entryId, $token, $invoiceSource->getType(), $invoiceSource->getId(), $created));
60
+ }
61
+
62
+ /**
63
+ * {@inheritdoc}
64
+ */
65
+ protected function update($record, $entryId, $token, $updated)
66
+ {
67
+ return Db::getInstance()->execute(sprintf("UPDATE `%s` SET id_entry = %u, token = '%s', updated = '%s' WHERE id = %u",
68
+ $this->tableName, $entryId, $token, $updated, $record['id']));
69
+ }
70
+
71
+ /**
72
+ * {@inheritdoc}
73
+ */
74
+ protected function sqlNow()
75
+ {
76
+ return date('Y-m-d H:i:s');
77
+ }
78
+
79
+ /**
80
+ * {@inheritdoc}
81
+ */
82
+ public function install()
83
+ {
84
+ return Db::getInstance()->execute("CREATE TABLE IF NOT EXISTS `{$this->tableName}` (
85
+ `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
86
+ `id_shop` int(11) UNSIGNED NOT NULL DEFAULT '1',
87
+ `id_shop_group` int(11) UNSIGNED NOT NULL DEFAULT '1',
88
+ `id_entry` int(11) UNSIGNED NOT NULL,
89
+ `token` char(32) NOT NULL,
90
+ `source_type` varchar(32) NOT NULL,
91
+ `source_id` int(11) UNSIGNED NOT NULL,
92
+ `created` timestamp DEFAULT CURRENT_TIMESTAMP,
93
+ `updated` timestamp NOT NULL,
94
+ PRIMARY KEY (`id`),
95
+ UNIQUE INDEX `acumulus_idx_entry_id` (`id_entry`),
96
+ UNIQUE INDEX `acumulus_idx_source` (`source_id`, `source_type`)
97
+ )");
98
+ }
99
+
100
+ /**
101
+ * {@inheritdoc}
102
+ */
103
+ public function uninstall()
104
+ {
105
+ return Db::getInstance()->execute("DROP TABLE `{$this->tableName}`");
106
+ }
107
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/BatchForm.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Shop;
3
+
4
+ use Siel\Acumulus\Shop\BatchForm as BaseBatchForm;
5
+ use Tools;
6
+
7
+ /**
8
+ * Provides the Batch send form handling for the VirtueMart Acumulus module.
9
+ */
10
+ class BatchForm extends BaseBatchForm
11
+ {
12
+ public function isSubmitted()
13
+ {
14
+ return Tools::isSubmit('submitAdd');
15
+ }
16
+
17
+ /**
18
+ * {@inheritdoc}
19
+ */
20
+ protected function setFormValues()
21
+ {
22
+ parent::setFormValues();
23
+
24
+ // Prepend (checked) checkboxes with their collection name.
25
+ foreach ($this->getCheckboxKeys() as $checkboxName => $collectionName) {
26
+ if (isset($this->formValues[$checkboxName])) {
27
+ $this->formValues["{$collectionName}_{$checkboxName}"] = $this->formValues[$checkboxName];
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ */
35
+ public function getFieldDefinitions()
36
+ {
37
+ $result = parent::getFieldDefinitions();
38
+ $result['batchFieldsHeader']['icon'] = 'icon-envelope-alt';
39
+ if (isset($result['batchLogHeader'])) {
40
+ $result['batchLogHeader']['icon'] = 'icon-list';
41
+ }
42
+ $result['batchInfoHeader']['icon'] = 'icon-info-circle';
43
+ return $result;
44
+ }
45
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/ConfigForm.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Shop;
3
+
4
+ use Context;
5
+ use OrderState;
6
+ use Siel\Acumulus\Helpers\TranslatorInterface;
7
+ use Siel\Acumulus\Shop\Config;
8
+ use Siel\Acumulus\Shop\ConfigForm as BaseConfigForm;
9
+ use Siel\Acumulus\Shop\ConfigInterface;
10
+ use Tools;
11
+
12
+ /**
13
+ * Class ConfigForm processes and builds the settings form page for the
14
+ * PrestaShop Acumulus module.
15
+ */
16
+ class ConfigForm extends BaseConfigForm
17
+ {
18
+ /** @var string */
19
+ protected $moduleName;
20
+
21
+ /**
22
+ * ConfigForm constructor.
23
+ *
24
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
25
+ * @param \Siel\Acumulus\Shop\Config $config
26
+ */
27
+ public function __construct(TranslatorInterface $translator, Config $config)
28
+ {
29
+ parent::__construct($translator, $config);
30
+ $this->moduleName = 'acumulus';
31
+ }
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ *
36
+ * This override uses the PS way of checking if a form is submitted.
37
+ */
38
+ public function isSubmitted()
39
+ {
40
+ return Tools::isSubmit('submit' . $this->moduleName);
41
+ }
42
+
43
+ /**
44
+ * {@inheritdoc}
45
+ *
46
+ * This override ensures that array values are passed with the correct key
47
+ * to the PS form renderer.
48
+ */
49
+ public function getFormValues()
50
+ {
51
+ $result = parent::getFormValues();
52
+ $result['triggerOrderStatus[]'] = $result['triggerOrderStatus'];
53
+ return $result;
54
+ }
55
+
56
+ /**
57
+ * {@inheritdoc}
58
+ */
59
+ protected function setFormValues()
60
+ {
61
+ parent::setFormValues();
62
+
63
+ // Prepend (checked) checkboxes with their collection name.
64
+ foreach ($this->getCheckboxKeys() as $checkboxName => $collectionName) {
65
+ if (isset($this->formValues[$checkboxName])) {
66
+ $this->formValues["{$collectionName}_{$checkboxName}"] = $this->formValues[$checkboxName];
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * {@inheritdoc}
73
+ */
74
+ public function getFieldDefinitions()
75
+ {
76
+ $result = parent::getFieldDefinitions();
77
+ $result['accountSettingsHeader']['icon'] = 'icon-user';
78
+ if (isset($result['invoiceSettingsHeader'])) {
79
+ $result['invoiceSettingsHeader']['icon'] = 'icon-AdminParentPreferences';
80
+ $result['emailAsPdfSettingsHeader']['icon'] = 'icon-file-pdf-o';
81
+ }
82
+ $result['versionInformationHeader']['icon'] = 'icon-info-circle';
83
+ return $result;
84
+ }
85
+
86
+ /**
87
+ * {@inheritdoc}
88
+ */
89
+ protected function getShopOrderStatuses()
90
+ {
91
+ $states = OrderState::getOrderStates((int) Context::getContext()->language->id);
92
+ $result = array();
93
+ foreach ($states as $state) {
94
+ $result[$state['id_order_state']] = $state['name'];
95
+ }
96
+ return $result;
97
+ }
98
+
99
+ /**
100
+ * {@inheritdoc}
101
+ *
102
+ * This override removes the 'Use invoice sent' option as PS does not have
103
+ * an event on creating/sending the invoice.
104
+ *
105
+ * @todo: PS has the 'actionSetInvoice' event, can we use that?
106
+ * This event fires when the order state changes to a state that allows an
107
+ * invoice and on manually creating one via the adminOrdersController page.
108
+ */
109
+ protected function getTriggerInvoiceSendEventOptions()
110
+ {
111
+ $result = parent::getTriggerInvoiceSendEventOptions();
112
+ unset($result[ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate]);
113
+ return $result;
114
+ }
115
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/ConfigStore.php ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Shop;
3
+
4
+ use Acumulus;
5
+ use Configuration;
6
+ use Siel\Acumulus\Shop\ConfigStore as BaseConfigStore;
7
+
8
+ /**
9
+ * Implements the connection to the PrestaShop config component.
10
+ */
11
+ class ConfigStore extends BaSeConfigStore
12
+ {
13
+ const CONFIG_KEY = 'ACUMULUS_';
14
+
15
+ /**
16
+ * {@inheritdoc}
17
+ */
18
+ public function getShopEnvironment()
19
+ {
20
+ $environment = array(
21
+ 'moduleVersion' => Acumulus::$module_version,
22
+ 'shopName' => $this->shopName,
23
+ 'shopVersion' => _PS_VERSION_,
24
+ );
25
+ return $environment;
26
+ }
27
+
28
+ /**
29
+ * {@inheritdoc}
30
+ */
31
+ public function load(array $keys)
32
+ {
33
+ $result = array();
34
+ // Load the values from the web shop specific configuration.
35
+ foreach ($keys as $key) {
36
+ $dbKey = substr(static::CONFIG_KEY . $key, 0, 32);
37
+ $value = Configuration::get($dbKey);
38
+ // Do not overwrite defaults if no value is stored.
39
+ if ($value !== false) {
40
+ if (is_string($value) && strpos($value, '{') !== false) {
41
+ $unserialized = @unserialize($value);
42
+ if ($unserialized !== false) {
43
+ $value = $unserialized;
44
+ }
45
+ }
46
+ $result[$key] = $value;
47
+ }
48
+ }
49
+ return $result;
50
+ }
51
+
52
+ /**
53
+ * {@inheritdoc}
54
+ */
55
+ public function save(array $values)
56
+ {
57
+ $values = $this->saveCommon($values);
58
+
59
+ $result = true;
60
+ foreach ($values as $key => $value) {
61
+ if ($value !== null) {
62
+ $dbKey = substr(static::CONFIG_KEY . $key, 0, 32);
63
+ if (is_bool($value)) {
64
+ $value = $value ? 1 : 0;
65
+ } elseif (is_array($value)) {
66
+ $value = serialize($value);
67
+ }
68
+ $result = Configuration::updateValue($dbKey, $value) && $result;
69
+ }
70
+ }
71
+ return $result;
72
+ }
73
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/PrestaShop/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\PrestaShop\Shop;
3
+
4
+ use DateTime;
5
+ use Db;
6
+ use Hook;
7
+ use Order;
8
+ use OrderSlip;
9
+ use Siel\Acumulus\Invoice\Source as BaseSource;
10
+ use Siel\Acumulus\PrestaShop\Invoice\Source; // @todo: still needed?
11
+ use Siel\Acumulus\Shop\Config;
12
+ use Siel\Acumulus\Shop\InvoiceManager as BaseInvoiceManager;
13
+
14
+ class InvoiceManager extends BaseInvoiceManager
15
+ {
16
+ /** @var string */
17
+ protected $orderTableName;
18
+
19
+ /** @var string */
20
+ protected $orderSlipTableName;
21
+
22
+ /**
23
+ * {@inheritdoc}
24
+ */
25
+ public function __construct(Config $config)
26
+ {
27
+ parent::__construct($config);
28
+ $this->orderTableName = _DB_PREFIX_ . Order::$definition['table'];
29
+ $this->orderSlipTableName = _DB_PREFIX_ . OrderSlip::$definition['table'];
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ */
35
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
36
+ {
37
+ switch ($invoiceSourceType) {
38
+ case Source::Order:
39
+ $key = Order::$definition['primary'];
40
+ $ids = Db::getInstance()->executeS(sprintf("SELECT `%s` FROM `%s` WHERE `%s` BETWEEN %u AND %u", $key, $this->orderTableName, $key, $InvoiceSourceIdFrom, $InvoiceSourceIdTo));
41
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $this->getCol($ids, $key));
42
+ case Source::CreditNote:
43
+ $key = OrderSlip::$definition['primary'];
44
+ $ids = Db::getInstance()->executeS(sprintf("SELECT `%s` FROM `%s` WHERE `%s` BETWEEN %u AND %u", $key, $this->orderSlipTableName, $key, $InvoiceSourceIdFrom, $InvoiceSourceIdTo));
45
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $this->getCol($ids, $key));
46
+ }
47
+ return array();
48
+ }
49
+
50
+ /**
51
+ * {@inheritdoc}
52
+ */
53
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
54
+ {
55
+ switch ($invoiceSourceType) {
56
+ case Source::Order:
57
+ $key = Order::$definition['primary'];
58
+ $reference = 'reference';
59
+ $ids = Db::getInstance()->executeS(sprintf("SELECT `%s` FROM `%s` WHERE `%s` BETWEEN '%s' AND '%s'", $key, $this->orderTableName, $reference, pSQL($InvoiceSourceReferenceFrom), pSQL($InvoiceSourceReferenceTo)));
60
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $this->getCol($ids, $key));
61
+ case Source::CreditNote:
62
+ return $this->getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo);
63
+ }
64
+ return array();
65
+ }
66
+
67
+ /**
68
+ * {@inheritdoc}
69
+ */
70
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
71
+ {
72
+ $dateFrom = $dateFrom->format('c');
73
+ $dateTo = $dateTo->format('c');
74
+ switch ($invoiceSourceType) {
75
+ case Source::Order:
76
+ $ids = Order::getOrdersIdByDate($dateFrom, $dateTo);
77
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $ids);
78
+ case Source::CreditNote:
79
+ $ids = OrderSlip::getSlipsIdByDate($dateFrom, $dateTo);
80
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $ids);
81
+ }
82
+ return array();
83
+ }
84
+
85
+ /**
86
+ * {@inheritdoc}
87
+ *
88
+ * This PrestaShop override executes the 'actionAcumulusInvoiceCreated' hook.
89
+ */
90
+ protected function triggerInvoiceCreated(array &$invoice, BaseSource $invoiceSource)
91
+ {
92
+ Hook::exec('actionAcumulusInvoiceCreated', array('invoice' => &$invoice, 'source' => $invoiceSource), null, true);
93
+ }
94
+
95
+ /**
96
+ * {@inheritdoc}
97
+ *
98
+ * This PrestaShop override executes the 'actionAcumulusInvoiceCompleted' hook.
99
+ */
100
+ protected function triggerInvoiceCompleted(array &$invoice, BaseSource $invoiceSource)
101
+ {
102
+ Hook::exec('actionAcumulusInvoiceCompleted', array('invoice' => &$invoice, 'source' => $invoiceSource), null, true);
103
+ }
104
+
105
+ /**
106
+ * {@inheritdoc}
107
+ *
108
+ * This PrestaShop override executes the 'actionAcumulusInvoiceSent' hook.
109
+ */
110
+ protected function triggerInvoiceSent(array $invoice, BaseSource $invoiceSource, array $result)
111
+ {
112
+ // @todo: not by ref + also pass result.
113
+ Hook::exec('actionAcumulusInvoiceSent', array('invoice' => &$invoice, 'source' => $invoiceSource), null, true);
114
+ }
115
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ /**
5
+ * Represents acumulus entry records that ties orders or credit notes from the
6
+ * web shop to entries in Acumulus.
7
+ *
8
+ * Acumulus identifies entries by their entry id (boekstuknummer in het
9
+ * Nederlands). To access an entry via the API, one must also supply a token
10
+ * that is generated based on the contents of the entry. This entry id and token
11
+ * are stored together with an id for the order or credit note from the web
12
+ * shop.
13
+ *
14
+ * Usages (not (all of them are) yet implemented):
15
+ * - Prevent that an invoice for a given order or credit note is sent twice.
16
+ * - Show additional information on order list screens
17
+ * - Update payment status
18
+ * - Resend Acumulus invoice PDF.
19
+ */
20
+ abstract class AcumulusEntryModel
21
+ {
22
+ /**
23
+ * Returns the Acumulus entry record for the given entry id.
24
+ *
25
+ * @param string $entryId
26
+ * the entry id to look up.
27
+ *
28
+ * @return array|object|null
29
+ * Acumulus entry record for the given entry id or null if the entry id is
30
+ * unknown.
31
+ */
32
+ abstract public function getByEntryId($entryId);
33
+
34
+ /**
35
+ * Returns the Acumulus entry record for the given invoice source.
36
+ *
37
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
38
+ * The source object (order, credit note) for which the invoice was created.
39
+ *
40
+ * @return array|object|null
41
+ * Acumulus entry record for the given invoice source or null if no invoice
42
+ * has yet been created in Acumulus for this invoice source.
43
+ */
44
+ public function getByInvoiceSource($invoiceSource)
45
+ {
46
+ return $this->getByInvoiceSourceId($invoiceSource->getType(), $invoiceSource->getId());
47
+ }
48
+
49
+ /**
50
+ * Returns the Acumulus entry record for the given invoice source.
51
+ *
52
+ * @param string $invoiceSourceType
53
+ * The type of the invoice source
54
+ * @param string $invoiceSourceId
55
+ * The id of the invoice source for which the invoice was created.
56
+ *
57
+ * @return array|object|null
58
+ * Acumulus entry record for the given invoice source or null if no invoice
59
+ * has yet been created in Acumulus for this invoice source.
60
+ */
61
+ abstract public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId);
62
+
63
+ /**
64
+ * Saves the Acumulus entry for the given order in the web shop's database.
65
+ *
66
+ * This default implementation calls getByInvoiceSource() to determine whether
67
+ * to subsequently call insert() or update().
68
+ *
69
+ * So normally, a child class should implement insert() and update() and not
70
+ * override this method.
71
+ *
72
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
73
+ * The source object (order, credit note) for which the invoice was created.
74
+ * @param $entryId
75
+ * The Acumulus entry Id assigned to the invoice for this order.
76
+ * @param $token
77
+ * The Acumulus token to be used to access the invoice for this order via
78
+ * the Acumulus API.
79
+ *
80
+ * @return bool
81
+ * Success.
82
+ */
83
+ public function save($invoiceSource, $entryId, $token)
84
+ {
85
+ $now = $this->sqlNow();
86
+ $record = $this->getByInvoiceSource($invoiceSource);
87
+ if ($record == null) {
88
+ $this->insert($invoiceSource, $entryId, $token, $now);
89
+ } else {
90
+ $this->update($record, $entryId, $token, $now);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Returns the current time in a format that the actual database layer accepts
96
+ * as a timestamp.
97
+ *
98
+ * @return int|string
99
+ */
100
+ abstract protected function sqlNow();
101
+
102
+ /**
103
+ * Inserts an Acumulus entry for the given order in the web shop's database.
104
+ *
105
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
106
+ * The source object (order, credit note) for which the invoice was created.
107
+ * @param $entryId
108
+ * The Acumulus entry Id assigned to the invoice for this order.
109
+ * @param $token
110
+ * The Acumulus token to be used to access the invoice for this order via
111
+ * the Acumulus API.
112
+ * @param int|string $created
113
+ * The creation time (= current time), in the format as the actual database
114
+ * layer expects for a timestamp.
115
+ *
116
+ * @return bool
117
+ * Success.
118
+ */
119
+ abstract protected function insert($invoiceSource, $entryId, $token, $created);
120
+
121
+ /**
122
+ * Updates the Acumulus entry for the given invoice source.
123
+ *
124
+ * @param array|object $record
125
+ * The existing record for the invoice source to be updated.
126
+ * @param string $entryId
127
+ * The new Acumulus entry id for the invoice source.
128
+ * @param string $token
129
+ * The new Acumulus token for the invoice source.
130
+ * @param int|string $updated
131
+ * The update time (= current time), in the format as the actual database
132
+ * layer expects for a timestamp.
133
+ *
134
+ * @return bool
135
+ * Success.
136
+ */
137
+ abstract protected function update($record, $entryId, $token, $updated);
138
+
139
+ /**
140
+ * @return bool
141
+ */
142
+ abstract public function install();
143
+
144
+ /**
145
+ * @return bool
146
+ */
147
+ abstract public function uninstall();
148
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchForm.php ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\Helpers\Form;
6
+ use Siel\Acumulus\Helpers\TranslatorInterface;
7
+ use Siel\Acumulus\Invoice\Translations as InvoiceTranslations;
8
+
9
+ /**
10
+ * Provides batch form handling.
11
+ *
12
+ * Shop specific overrides should - of course - implement the abstract method:
13
+ * - none
14
+ * Should typically override:
15
+ * - none
16
+ * And may optionally (have to) override:
17
+ * - systemValidate()
18
+ * - getDateFormat
19
+ * - getShopDateFormat()
20
+ * - isSubmitted()
21
+ * - setSubmittedValues()
22
+ */
23
+ class BatchForm extends Form
24
+ {
25
+ /** @var \Siel\Acumulus\Shop\InvoiceManager */
26
+ protected $invoiceManager;
27
+
28
+ /** @var array */
29
+ protected $log;
30
+
31
+ /**
32
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
33
+ * @param \Siel\Acumulus\Shop\InvoiceManager $invoiceManager
34
+ */
35
+ public function __construct(TranslatorInterface $translator, InvoiceManager $invoiceManager)
36
+ {
37
+ parent::__construct($translator);
38
+
39
+ $translations = new InvoiceTranslations();
40
+ $this->translator->add($translations);
41
+
42
+ $translations = new BatchFormTranslations();
43
+ $this->translator->add($translations);
44
+
45
+ $this->log = array();
46
+ $this->invoiceManager = $invoiceManager;
47
+ }
48
+
49
+ protected function getDefaultFormValues()
50
+ {
51
+ $result = parent::getDefaultFormValues();
52
+ if (!empty($this->log)) {
53
+ $result['log'] = implode("\n", $this->log);
54
+ }
55
+ return $result;
56
+ }
57
+
58
+ protected function validate()
59
+ {
60
+ $invoiceSourceTypes = $this->invoiceManager->getSupportedInvoiceSourceTypes();
61
+ if (empty($this->submittedValues['invoice_source_type'])) {
62
+ $this->errorMessages['invoice_source_type'] = $this->t('message_validate_batch_source_type_required');
63
+ } else if (!in_array($this->submittedValues['invoice_source_type'], $invoiceSourceTypes)) {
64
+ $this->errorMessages['invoice_source_type'] = $this->t('message_validate_batch_source_type_invalid');
65
+ }
66
+
67
+ if ($this->submittedValues['invoice_source_reference_from'] === '' && $this->submittedValues['date_from'] === '') {
68
+ // Either a range of order id's or a range of dates should be entered.
69
+ $this->errorMessages['invoice_source_reference_from'] = $this->t(count($invoiceSourceTypes) === 1 ? 'message_validate_batch_reference_or_date_1' : 'message_validate_batch_reference_or_date_2');
70
+ } else if ($this->submittedValues['invoice_source_reference_from'] !== '' && $this->submittedValues['date_from'] !== '') {
71
+ // Not both ranges should be entered.
72
+ $this->errorMessages['date_from'] = $this->t(count($invoiceSourceTypes) === 1 ? 'message_validate_batch_reference_and_date_1' : 'message_validate_batch_reference_and_date_2');
73
+ } else if ($this->submittedValues['invoice_source_reference_from'] !== '') {
74
+ // Date from is empty, we go for a range of order ids.
75
+ // (We ignore any date to value.)
76
+ // Single id or range of ids?
77
+ if ($this->submittedValues['invoice_source_reference_to'] !== '' && $this->submittedValues['invoice_source_reference_to'] < $this->submittedValues['invoice_source_reference_from']) {
78
+ // order id to is smaller than order id from.
79
+ $this->errorMessages['invoice_source_reference_to'] = $this->t('message_validate_batch_bad_order_range');
80
+ }
81
+ } else /*if ($this->submittedValues['date_to'] !== '') */ {
82
+ // Range of dates has been filled in.
83
+ // We ignore any order # to value.
84
+ $dateFormat = $this->getDateFormat();
85
+ if (!DateTime::createFromFormat($dateFormat, $this->submittedValues['date_from'])) {
86
+ // Date from not a valid date.
87
+ $this->errorMessages['date_from'] = sprintf($this->t('message_validate_batch_bad_date_from'), $this->getShopDateFormat());
88
+ }
89
+ if ($this->submittedValues['date_to']) {
90
+ if (!DateTime::createFromFormat($dateFormat, $this->submittedValues['date_to'])) {
91
+ // Date to not a valid date.
92
+ $this->errorMessages['date_to'] = sprintf($this->t('message_validate_batch_bad_date_to'), $this->getShopDateFormat());
93
+ } else if ($this->submittedValues['date_to'] < $this->submittedValues['date_from']) {
94
+ // date to is smaller than date from
95
+ $this->errorMessages['date_to'] = $this->t('message_validate_batch_bad_date_range');
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * {@inheritdoc}
103
+ *
104
+ * Sends the invoices as defined by the form values to Acumulus.
105
+ */
106
+ protected function execute()
107
+ {
108
+ $type = $this->getFormValue('invoice_source_type');
109
+ if ($this->getFormValue('invoice_source_reference_from') !== '') {
110
+ // Retrieve by order reference range.
111
+ $from = $this->getFormValue('invoice_source_reference_from');
112
+ $to = $this->getFormValue('invoice_source_reference_to') ? $this->getFormValue('invoice_source_reference_to') : $from;
113
+ $invoiceSources = $this->invoiceManager->getInvoiceSourcesByReferenceRange($type, $from, $to);
114
+ } else {
115
+ // Retrieve by order date.
116
+ $dateFormat = $this->getDateFormat();
117
+ $from = DateTime::createFromFormat($dateFormat, $this->getFormValue('date_from'));
118
+ $from->setTime(0, 0, 0);
119
+ $to = $this->getFormValue('date_to') ? DateTime::createFromFormat($dateFormat, $this->getFormValue('date_to')) : clone $from;
120
+ $to->setTime(23, 59, 59);
121
+ $invoiceSources = $this->invoiceManager->getInvoiceSourcesByDateRange($type, $from, $to);
122
+ }
123
+
124
+ if (count($invoiceSources) === 0) {
125
+ $this->log[$type] = sprintf($this->t('message_form_empty_range'), $this->t($type));
126
+ $this->setFormValue('result', $this->log[$type]);
127
+ $result = true;
128
+ } else {
129
+ $result = $this->invoiceManager->sendMultiple($invoiceSources, (bool) $this->getFormValue('force_send'), $this->log);
130
+ }
131
+
132
+ // Set formValue for log in case form values are already queried.
133
+ $logText = implode("\n", $this->log);
134
+ $this->setFormValue('log', $logText);
135
+ return $result;
136
+ }
137
+
138
+ /**
139
+ * {@inheritdoc}
140
+ */
141
+ public function getFieldDefinitions()
142
+ {
143
+ $fields = array();
144
+
145
+ $invoiceSourceTypes = $this->invoiceManager->getSupportedInvoiceSourceTypes();
146
+ if (count($invoiceSourceTypes) === 1) {
147
+ // Make it a hidden field.
148
+ $invoiceSourceTypeField = array(
149
+ 'type' => 'hidden',
150
+ 'value' => reset($invoiceSourceTypes),
151
+ );
152
+ } else {
153
+ $options = array();
154
+ foreach ($invoiceSourceTypes as $invoiceSourceType) {
155
+ $options[$invoiceSourceType] = ucfirst($this->t($invoiceSourceType));
156
+ }
157
+ $invoiceSourceTypeField = array(
158
+ 'type' => 'radio',
159
+ 'label' => $this->t('field_invoice_source_type'),
160
+ 'options' => $options,
161
+ 'attributes' => array(
162
+ 'required' => true,
163
+ ),
164
+ );
165
+ }
166
+ // 1st fieldset: Batch options.
167
+ $fields['batchFieldsHeader'] = array(
168
+ 'type' => 'fieldset',
169
+ 'legend' => $this->t('batchFieldsHeader'),
170
+ 'fields' => array(
171
+ 'invoice_source_type' => $invoiceSourceTypeField,
172
+ 'invoice_source_reference_from' => array(
173
+ 'type' => 'text',
174
+ 'label' => $this->t('field_invoice_source_reference_from'),
175
+ ),
176
+ 'invoice_source_reference_to' => array(
177
+ 'type' => 'text',
178
+ 'label' => $this->t('field_invoice_source_reference_to'),
179
+ 'description' => count($invoiceSourceTypes) === 1 ? $this->t('desc_invoice_source_reference_from_to_1') : $this->t('desc_invoice_source_reference_from_to_2'),
180
+ ),
181
+ 'date_from' => array(
182
+ 'type' => 'date',
183
+ 'label' => $this->t('field_date_from'),
184
+ 'format' => $this->getShopDateFormat(),
185
+ ),
186
+ 'date_to' => array(
187
+ 'type' => 'date',
188
+ 'label' => $this->t('field_date_to'),
189
+ 'description' => sprintf($this->t('desc_date_from_to'), $this->getShopDateFormat()),
190
+ 'format' => $this->getShopDateFormat(),
191
+ ),
192
+ 'force_send' => array(
193
+ 'type' => 'checkbox',
194
+ 'label' => $this->t('field_options'),
195
+ 'description' => $this->t('desc_batch_options'),
196
+ 'options' => array(
197
+ 'force_send' => $this->t('option_force_send'),
198
+ ),
199
+ ),
200
+ ),
201
+ );
202
+
203
+ // 2nd fieldset: Batch log.
204
+ if ($this->isSubmitted() && !empty($this->submittedValues) && $this->isValid()) {
205
+ // Set formValue for log as value in case form values are not yet queried.
206
+ $fields['batchLogHeader'] = array(
207
+ 'type' => 'fieldset',
208
+ 'legend' => $this->t('batchLogHeader'),
209
+ 'fields' => array(
210
+ 'log' => array(
211
+ 'type' => 'textarea',
212
+ 'attributes' => array(
213
+ 'readonly' => true,
214
+ 'rows' => max(5, min(10, count($this->log))),
215
+ 'style' => 'box-sizing: border-box; width: 100%; min-width: 48em;',
216
+ ),
217
+ ),
218
+ ),
219
+ );
220
+ if (!empty($this->log)) {
221
+ $logText = implode("\n", $this->log);
222
+ $this->formValues['log'] = $logText;
223
+ $fields['batchLogHeader']['fields']['log']['value'] = $logText;
224
+ }
225
+ }
226
+
227
+ // 3rd fieldset: Batch info.
228
+ $fields['batchInfoHeader'] = array(
229
+ 'type' => 'fieldset',
230
+ 'legend' => $this->t('batchInfoHeader'),
231
+ 'fields' => array(
232
+ 'info' => array(
233
+ 'type' => 'markup',
234
+ 'value' => $this->t('batch_info'),
235
+ 'attributes' => array(
236
+ 'readonly' => true,
237
+ ),
238
+ ),
239
+ ),
240
+ );
241
+
242
+ return $fields;
243
+ }
244
+
245
+ /**
246
+ * {@inheritdoc}
247
+ */
248
+ protected function getCheckboxKeys()
249
+ {
250
+ return array(
251
+ 'force_send' => 'force_send',
252
+ );
253
+ }
254
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchFormTranslations.php ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for the batch form.
8
+ */
9
+ class BatchFormTranslations extends TranslationCollection
10
+ {
11
+ protected $nl = array(
12
+ 'batch_form_title' => 'Acumulus | Batch verzending',
13
+ 'batch_form_header' => 'Verzend facturen naar Acumulus',
14
+
15
+ 'button_send' => 'Verzenden',
16
+ 'button_cancel' => 'Annuleren',
17
+
18
+ 'batchFieldsHeader' => 'Batchgewijs verzenden van facturen naar Acumulus',
19
+ 'field_invoice_source_type' => 'Factuurtype',
20
+ 'field_invoice_source_reference_from' => '# van',
21
+ 'field_invoice_source_reference_to' => '# tot',
22
+ 'desc_invoice_source_reference_from_to_1' => 'Vul de reeks bestelnummers in die u naar Acumulus wilt verzenden. Als u slechts 1 factuur wilt verzenden hoeft u alleen het \'# van\' in te vullen. Laat beide velden leeg als u op datum wilt verzenden.',
23
+ 'desc_invoice_source_reference_from_to_2' => 'Vul de reeks bestel of creditnotanummers in die u naar Acumulus wilt verzenden. Als u slechts 1 factuur wilt verzenden hoeft u alleen het \'# van\' in te vullen. Laat beide velden leeg als u op datum wilt verzenden.',
24
+ 'field_date_from' => 'Datum van',
25
+ 'field_date_to' => 'Datum tot',
26
+ 'desc_date_from_to' => 'Vul de periode in waarvan u de facuren naar Acumulus wilt verzenden (verwacht formaat %1$s). De selectie vindt plaats op basis van de datum van de meest recente wijziging aan de bestelling of creditnota. Als u slechts de facturen van 1 dag wilt verzenden hoeft u alleen de \'Datum van\' in te vullen. Laat beide velden leeg als u op nummer wilt verzenden.',
27
+ 'field_options' => 'Opties',
28
+ 'option_force_send' => 'Forceer verzenden',
29
+ 'desc_batch_options' => 'Facturen die binnen de reeks vallen maar al naar Acumulus verstuurd zijn, worden standaard niet verzonden. Door deze optie aan te vinken forceert u het nogmaals versturen van deze facturen.',
30
+ 'batchLogHeader' => 'Resultaten',
31
+ 'batchInfoHeader' => 'Uitgebreide informatie',
32
+ 'batch_info' => <<<LONGSTRING
33
+ <p>Met dit formulier kunt u de facturen van een aantal orders of creditnota's in
34
+ één keer versturen.
35
+ Dit is vooral handig als u deze koppeling net heeft geïnstalleerd want normaal
36
+ gesproken heeft het automatisch versturen de voorkeur.</p>
37
+ <p><strong>Performance: het versturen van een factuur kan tot enige seconden
38
+ duren.
39
+ Geef daarom niet teveel facturen in één keer op.
40
+ U kunt dan een time-out krijgen, waardoor het resultaat van de laatst verstuurde
41
+ factuur niet opgeslagen wordt.</strong></p>
42
+ <p><strong>LET OP: Het gebruik van de optie 'Forceer verzenden' is op eigen
43
+ risico. Door het nogmaals handmatig versturen van facturen kan uw administratie
44
+ ontregeld raken.</strong></p>
45
+ <p>Het versturen van orders gaat net als het automatisch versturen:</p>
46
+ <ul style="list-style: inside disc;">
47
+ <li>De factuur wordt op exact dezelfde wijze aangemaakt als bij het automatisch
48
+ versturen.</li>
49
+ <li>Als er facturen zijn die fouten bevatten ontvangt u een mail per factuur.
50
+ </li>
51
+ <li>Als u een event handler heeft geregistreerd voor het 'AcumulusInvoiceAdd"
52
+ event (of hook of actie) wordt die voor alle facturen die verzonden gaan worden
53
+ uitgevoerd.</li>
54
+ </ul>
55
+ <p>Dit formulier bevindt zich in een experimentele status.
56
+ Het werkt in zijn huidige vorm, maar als u de behoefte heeft om de reeks van
57
+ facturen op een andere manier te willen aangeven, laat dit ons dan weten.</p>
58
+ <p>Standaard worden facuren die al naar Acumulus verzonden zijn, niet opnieuw
59
+ verstuurd. Dit kunt u forceren door die optie aan te vinken, maar let op: in
60
+ Acumulus wordt dit als een nieuwe factuur gezien. U dient zelf uw boekhouding te
61
+ ontdubbelen.
62
+ Merk ook nog op dat deze beveiliging alleen werkt voor facturen die sinds de
63
+ installatie van de versie met uitgiftedatum begin september 2014 van deze
64
+ koppeling verstuurd zijn.
65
+ In oudere versies werd nog niet bijgehouden voor welke orders en creditnota's al
66
+ een factuur verzonden was.</p>
67
+ LONGSTRING
68
+ ,
69
+
70
+ 'message_validate_batch_source_type_required' => 'U dient een Factuurtype te selecteren.',
71
+ 'message_validate_batch_source_type_invalid' => 'U dient een bestaand factuurtype te selecteren.',
72
+ 'message_validate_batch_reference_or_date_1' => 'U dient of een reeks van bestelnummers of een reeks van datums in te vullen.',
73
+ 'message_validate_batch_reference_or_date_2' => 'U dient of een reeks van bestel of creditnotanummers of een reeks van datums in te vullen.',
74
+ 'message_validate_batch_reference_and_date_1' => 'U kunt niet en een reeks van bestelnummers en een reeks van datums invullen.',
75
+ 'message_validate_batch_reference_and_date_2' => 'U kunt niet en een reeks van bestel of creditnotanummers en een reeks van datums invullen.',
76
+ 'message_validate_batch_bad_date_from' => 'U dient een correcte \'Datum van\' in te vullen (verwacht formaat: %1$s).',
77
+ 'message_validate_batch_bad_date_to' => 'U dient een correcte \'Datum tot\' in te vullen (verwacht formaat %1$s).',
78
+ 'message_validate_batch_bad_date_range' => '\'Datum tot\' dient na \'Datum van\' te liggen.',
79
+ 'message_validate_batch_bad_order_range' => '\'# tot\' dient groter te zijn dan \'# van\'.',
80
+
81
+ 'message_form_empty_range' => 'De door u opgegeven reeks bevat geen enkele %1$s.',
82
+ 'message_form_success' => 'De facturen zijn succesvol verzonden. Zie het resultatenoverzicht voor eventuele opmerkingen en waarschuwingen.',
83
+ 'message_form_error' => 'Er zijn fouten opgetreden bij het versturen van de facturen. Zie het resultatenoverzicht voor meer informatie over de fouten.',
84
+
85
+ );
86
+
87
+ protected $en = array(
88
+ 'batch_form_title' => 'Acumulus | Send batch',
89
+ 'batch_form_header' => 'Send invoices to Acumulus',
90
+
91
+ 'button_send' => 'Send',
92
+ 'button_cancel' => 'Cancel',
93
+
94
+ 'batchFieldsHeader' => 'Send a batch of invoices to Acumulus',
95
+ 'field_invoice_source_type' => 'Invoice type',
96
+ 'field_invoice_source_reference_from' => '# from',
97
+ 'field_invoice_source_reference_to' => '# to',
98
+ 'desc_invoice_source_reference_from_to_1' => 'Enter the range of order numbers you want to send to Acumulus. If you only want to send 1 invoice, you only have to fill in the \'# from\' field. Leave empty if you want to send by date.',
99
+ 'desc_invoice_source_reference_from_to_2' => 'Enter the range of order or credit note numbers you want to send to Acumulus. If you only want to send 1 invoice, you only have to fill in the \'# from\' field. Leave empty if you want to send by date.',
100
+ 'field_date_from' => 'Date from',
101
+ 'field_date_to' => 'Date to',
102
+ 'desc_date_from_to' => 'Enter the period over which you want to send invoices to Acumulus (expected format: %1$s). If you want to send the invoices of 1 day, only fill in the \'Date from\' field. Leave empty if you want to send by id.',
103
+ 'field_options' => 'Options',
104
+ 'option_force_send' => 'Force sending',
105
+ 'desc_batch_options' => 'Invoices that fall within the range but are already sent to Acumulus will normally not be sent again. By checking this option these orders will be sent again.',
106
+ 'batchLogHeader' => 'Results',
107
+ 'batchInfoHeader' => 'Additional information',
108
+
109
+ 'message_validate_batch_source_type_required' => 'Please select an invoice type.',
110
+ 'message_validate_batch_source_type_invalid' => 'Please select an existing invoice type.',
111
+ 'message_validate_batch_reference_or_date_1' => 'Fill in a range of order numbers or a range of dates.',
112
+ 'message_validate_batch_reference_or_date_2' => 'Fill in a range of order/credit note numbers or a range of dates.',
113
+ 'message_validate_batch_reference_and_date_1' => 'Either fill in a range of order numbers OR a range of dates, not both.',
114
+ 'message_validate_batch_reference_and_date_2' => 'Either fill in a range of order/credit note numbers OR a range of dates, not both.',
115
+ 'message_validate_batch_bad_date_from' => 'Incorrect \'Date from\' (expected format: %1$s).',
116
+ 'message_validate_batch_bad_date_to' => 'Incorrect \'Date to\' (expected format: %1$s).',
117
+ 'message_validate_batch_bad_date_range' => '\'Date to\' should be after \'Date from\'.',
118
+ 'message_validate_batch_bad_order_range' => '\'# to\' should to be greater than \'# from\'.',
119
+
120
+ 'message_form_empty_range' => 'The range you defined does not contain any %1$s.',
121
+ 'message_form_success' => 'The invoices were sent successfully. See the results overview for any remarks or warnings.',
122
+ 'message_form_error' => 'Errors during sending the invoices. See the results overview for more information on the errors.',
123
+
124
+ );
125
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/BatchTranslations.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for classes in the \Siel\Acumulus\Shop namespace:
8
+ * - InvoiceManager
9
+ */
10
+ class BatchTranslations extends TranslationCollection
11
+ {
12
+ protected $nl = array(
13
+ // Batch log messages.
14
+ 'message_batch_send_1_success' => 'Factuur voor %1$s %2$s is succesvol verzonden.',
15
+ 'message_batch_send_1_errors' => 'Fout bij het versturen van de factuur voor %1$s %2$s.',
16
+ 'message_batch_send_1_warnings' => 'Waarschuwingen bij het versturen van de factuur voor %1$s %2$s.',
17
+ 'message_batch_send_1_skipped' => 'Factuur voor %1$s %2$s is overgeslagen omdat deze al verstuurd is.',
18
+ 'message_batch_send_1_prevented_invoiceCreated' => 'Factuur voor %1$s %2$s is overgeslagen omdat het versturen tegengehouden is door het event \'AcumulusInvoiceCreated\'.',
19
+ 'message_batch_send_1_prevented_invoiceCompleted' => 'Factuur voor %1$s %2$s is overgeslagen omdat het versturen tegengehouden is door het event \'AcumulusInvoiceCompleted\'.',
20
+
21
+ );
22
+
23
+ protected $en = array(
24
+ 'message_batch_send_1_success' => 'Successfully sent invoice for %1$s %2$s.',
25
+ 'message_batch_send_1_errors' => 'Error while sending invoice for %1$s %2$s.',
26
+ 'message_batch_send_1_warnings' => 'Warnings while sending invoice for %1$s %2$s.',
27
+ 'message_batch_send_1_skipped' => 'Skipped invoice for %1$s %2$s (already sent).',
28
+ 'message_batch_send_1_prevented_invoiceCreated' => 'Skipped invoice for %1$s %2$s (sending prevented by event \'AcumulusInvoiceCreated\').',
29
+ 'message_batch_send_1_prevented_invoiceCompleted' => 'Skipped invoice for %1$s %2$s (sending prevented by event \'AcumulusInvoiceCompleted\').',
30
+
31
+ );
32
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/Config.php ADDED
@@ -0,0 +1,808 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use ReflectionClass;
5
+ use Siel\Acumulus\Helpers\Log;
6
+ use Siel\Acumulus\Invoice\Completor;
7
+ use Siel\Acumulus\Invoice\ConfigInterface as InvoiceConfigInterface;
8
+ use Siel\Acumulus\Web\ConfigInterface as ServiceConfigInterface;
9
+
10
+ /**
11
+ * Gives common code in this package uniform access to the settings for this
12
+ * extension, hiding all the web shop specific implementations of their config
13
+ * store.
14
+ *
15
+ * This class implements all <...ConfigInterface>s and makes, via a ConfigStore,
16
+ * use of the shop specific configuration functionality to store this
17
+ * configuration in a persistent way.
18
+ *
19
+ * This class als implements the injector interface to allow other classes to
20
+ * easily get the correct derived classes of the base classes.
21
+ */
22
+ class Config implements ConfigInterface, InvoiceConfigInterface, ServiceConfigInterface, InjectorInterface
23
+ {
24
+ /** @const string */
25
+ const baseNamespace = '\\Siel\\Acumulus';
26
+
27
+ /** @var array[]|null */
28
+ protected $keyInfo;
29
+
30
+ /** @var bool */
31
+ protected $isLoaded;
32
+
33
+ /** @var array */
34
+ protected $values;
35
+
36
+ /** @var array */
37
+ protected $instances;
38
+
39
+ /** @var string The namespace for the current shop. */
40
+ protected $shopNamespace;
41
+
42
+ /** @var string The language to display texts in. */
43
+ protected $language;
44
+
45
+ /**
46
+ * Constructor.
47
+ *
48
+ * @param string $shopNamespace
49
+ * @param string $language
50
+ */
51
+ public function __construct($shopNamespace, $language)
52
+ {
53
+ $this->keyInfo = null;
54
+ $this->isLoaded = false;
55
+ $this->values = array();
56
+ $this->instances = array();
57
+ $this->shopNamespace = static::baseNamespace . '\\' . $shopNamespace;
58
+ $this->language = $language;
59
+ $this->getLog();
60
+ }
61
+
62
+ /**
63
+ * Helper method to translate strings.
64
+ *
65
+ * @param string $key
66
+ * The key to get a translation for.
67
+ *
68
+ * @return string
69
+ * The translation for the given key or the key itself if no translation
70
+ * could be found.
71
+ */
72
+ protected function t($key)
73
+ {
74
+ return $this->getTranslator()->get($key);
75
+ }
76
+
77
+ /**
78
+ * {@inheritdoc}
79
+ */
80
+ public function getTranslator()
81
+ {
82
+ return $this->getInstance('Translator', 'Helpers', array($this->language));
83
+ }
84
+
85
+ /**
86
+ * {@inheritdoc}
87
+ */
88
+ public function getLog()
89
+ {
90
+ return $this->getInstance('Log', 'Helpers', array($this));
91
+ }
92
+
93
+ /**
94
+ * {@inheritdoc}
95
+ */
96
+ public function getService()
97
+ {
98
+ return $this->getInstance('Service', 'Web', array($this, $this->getTranslator()));
99
+ }
100
+
101
+ /**
102
+ * {@inheritdoc}
103
+ */
104
+ public function getSource($invoiceSourceType, $invoiceSourceOrId)
105
+ {
106
+ return $this->getInstance('Source', 'Invoice', array($invoiceSourceType, $invoiceSourceOrId), true);
107
+ }
108
+
109
+ /**
110
+ * {@inheritdoc}
111
+ */
112
+ public function getCompletor()
113
+ {
114
+ return new Completor($this, $this->getTranslator(), $this->getService());
115
+ }
116
+
117
+ /**
118
+ * {@inheritdoc}
119
+ */
120
+ public function getCreator()
121
+ {
122
+ return $this->getInstance('Creator', 'Invoice', array($this, $this->getTranslator()));
123
+ }
124
+
125
+ /**
126
+ * {@inheritdoc}
127
+ */
128
+ public function getMailer()
129
+ {
130
+ return $this->getInstance('Mailer', 'Helpers', array($this, $this->getTranslator(), $this->getService()));
131
+ }
132
+
133
+ /**
134
+ * {@inheritdoc}
135
+ */
136
+ public function getConfigStore()
137
+ {
138
+ return $this->getInstance('ConfigStore', 'Shop', array($this->shopNamespace));
139
+ }
140
+
141
+ /**
142
+ * {@inheritdoc}
143
+ */
144
+ public function getManager()
145
+ {
146
+ return $this->getInstance('InvoiceManager', 'Shop', array($this));
147
+ }
148
+
149
+ /**
150
+ * {@inheritdoc}
151
+ */
152
+ public function getAcumulusEntryModel()
153
+ {
154
+ return $this->getInstance('AcumulusEntryModel', 'Shop');
155
+ }
156
+
157
+ /**
158
+ * {@inheritdoc}
159
+ */
160
+ public function getForm($type)
161
+ {
162
+ $arguments = array($this->getTranslator());
163
+ switch (strtolower($type)) {
164
+ case 'config':
165
+ $arguments[] = $this;
166
+ break;
167
+ case 'batch':
168
+ $arguments[] = $this->getManager();
169
+ break;
170
+ default:
171
+ $this->getLog()->error('Config::getForm(%s): unknown form type', $type);
172
+ break;
173
+ }
174
+ return $this->getInstance(ucfirst($type) . 'Form', 'Shop', $arguments);
175
+ }
176
+
177
+ /**
178
+ * {@inheritdoc}
179
+ */
180
+ public function getFormRenderer()
181
+ {
182
+ return $this->getInstance('FormRenderer', 'Helpers');
183
+ }
184
+
185
+ /**
186
+ * Returns an instance of the given class.
187
+ *
188
+ * The class is taken from the same namespace as the configStore property.
189
+ * Only 1 instance is created per class.
190
+ *
191
+ * @param string $class
192
+ * The name of the class without namespace. The namespace is taken from the
193
+ * configStore object.
194
+ * @param string $subNamespace
195
+ * The sub namespace (within the shop namespace) in which the class resides.
196
+ * @param array $constructorArgs
197
+ * An array of arguments to pass to the constructor, may be an empty array.
198
+ * @param bool $newInstance
199
+ * Whether to create a new instance or reuse an already existing instance
200
+ *
201
+ * @return object
202
+ *
203
+ * @throws \ReflectionException
204
+ */
205
+ protected function getInstance($class, $subNamespace, array $constructorArgs = array(), $newInstance = false)
206
+ {
207
+ if (!isset($this->instances[$class]) || $newInstance) {
208
+ // Try shop namespace.
209
+ $fqClass = $this->tryNsInstance($class, $subNamespace, $this->shopNamespace);
210
+
211
+ // Try CMS namespace if it exists.
212
+ if (empty($fqClass)) {
213
+ $cmsNamespace = $this->getCmsNamespace();
214
+ if (!empty($cmsNamespace)) {
215
+ $fqClass = $this->tryNsInstance($class, $subNamespace, $cmsNamespace);
216
+ }
217
+ }
218
+
219
+ // Try base namespace.
220
+ if (empty($fqClass)) {
221
+ $fqClass = $this->tryNsInstance($class, $subNamespace, static::baseNamespace);
222
+ }
223
+
224
+ // Use ReflectionClass to pass an argument list.
225
+ if (empty($constructorArgs)) {
226
+ // PHP5.3: exception when class has no constructor and
227
+ // newInstanceArgs() is called.
228
+ $this->instances[$class] = new $fqClass();
229
+ }
230
+ else {
231
+ $reflector = new ReflectionClass($fqClass);
232
+ $this->instances[$class] = $reflector->newInstanceArgs($constructorArgs);
233
+ }
234
+ }
235
+ return $this->instances[$class];
236
+ }
237
+
238
+ protected function tryNsInstance($class, $subNamespace, $namespace)
239
+ {
240
+ $fqClass = $namespace . '\\' . $subNamespace . '\\' . $class;
241
+ return class_exists($fqClass) ? $fqClass : '';
242
+ }
243
+
244
+ /**
245
+ * Returns the namespace for the current cms.
246
+ *
247
+ * @return string
248
+ * The namespace for the current cms or the empty string if the current shop
249
+ * is not contained in a CMS namespace.
250
+ */
251
+ protected function getCmsNamespace()
252
+ {
253
+ // Get parent namespace of the shop namespace.
254
+ $cmsNamespaceEnd = strrpos($this->shopNamespace, '\\');
255
+ $cmsNamespace = substr($this->shopNamespace, 0, (int) $cmsNamespaceEnd);
256
+ // But if that is Acumulus there's no CMS namespace.
257
+ if (substr($cmsNamespace, -strlen('\\Acumulus')) === '\\Acumulus') {
258
+ $cmsNamespace = '';
259
+ }
260
+ return $cmsNamespace;
261
+ }
262
+
263
+ /**
264
+ * Loads the configuration from the actual configuration provider.
265
+ */
266
+ protected function load()
267
+ {
268
+ if (!$this->isLoaded) {
269
+ $this->values = array_merge($this->getDefaults(), $this->getConfigStore()->load($this->getKeys()));
270
+ $this->castValues();
271
+ $this->isLoaded = true;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Saves the configuration to the actual configuration provider.
277
+ *
278
+ * @param array $values
279
+ * A keyed array that contains the values to store, this may be a subset of
280
+ * the possible keys.
281
+ *
282
+ * @return bool
283
+ * Success.
284
+ */
285
+ public function save(array $values)
286
+ {
287
+ $result = $this->getConfigStore()->save($values);
288
+ $this->isLoaded = false;
289
+ // Sync internal values.
290
+ $this->load();
291
+ return $result;
292
+ }
293
+
294
+ /**
295
+ * Returns the value of the specified configuration value.
296
+ *
297
+ * @param string $key
298
+ * The requested configuration value
299
+ *
300
+ * @return mixed
301
+ * The value of the given configuration value or null if not defined. This
302
+ * will be a simple type (string, int, bool) or a keyed array with simple
303
+ * values.
304
+ */
305
+ protected function get($key)
306
+ {
307
+ $this->load();
308
+ return isset($this->values[$key]) ? $this->values[$key] : null;
309
+ }
310
+
311
+ /**
312
+ * Sets the internal value of the specified configuration key.
313
+ *
314
+ * This value will not be stored, use save() for that.
315
+ *
316
+ * @param string $key
317
+ * The configuration value to set.
318
+ * @param mixed $value
319
+ * The new value for the configuration key.
320
+ *
321
+ * @return mixed
322
+ * The old value.
323
+ */
324
+ protected function set($key, $value)
325
+ {
326
+ $this->load();
327
+ $oldValue = isset($this->values[$key]) ? $this->values[$key] : null;
328
+ $this->values[$key] = $value;
329
+ return $oldValue;
330
+ }
331
+
332
+ /**
333
+ * Sets the value for the debug setting.
334
+ *
335
+ * @param int $debug
336
+ *
337
+ * @return int
338
+ * The old value.
339
+ */
340
+ public function setDebug($debug)
341
+ {
342
+ return $this->set('debug', (int) $debug);
343
+ }
344
+
345
+ /**
346
+ * Sets the log level.
347
+ *
348
+ * @param int $logLevel
349
+ *
350
+ * @return int
351
+ * The old value.
352
+ */
353
+ public function setLogLevel($logLevel)
354
+ {
355
+ return $this->set('logLevel', (int) $logLevel);
356
+ }
357
+
358
+ /**
359
+ * @inheritdoc
360
+ */
361
+ public function getBaseUri()
362
+ {
363
+ return $this->get('baseUri');
364
+ }
365
+
366
+ /**
367
+ * @inheritdoc
368
+ */
369
+ public function getApiVersion()
370
+ {
371
+ return $this->get('apiVersion');
372
+ }
373
+
374
+ /**
375
+ * @inheritdoc
376
+ */
377
+ public function getEnvironment()
378
+ {
379
+ return $this->getSettingsByGroup('environment');
380
+ }
381
+
382
+ /**
383
+ * @inheritdoc
384
+ */
385
+ public function getDebug()
386
+ {
387
+ return $this->get('debug');
388
+ }
389
+
390
+ /**
391
+ * @inheritdoc
392
+ */
393
+ public function getLogLevel()
394
+ {
395
+ return $this->get('logLevel');
396
+ }
397
+
398
+ /**
399
+ * @inheritdoc
400
+ */
401
+ public function getOutputFormat()
402
+ {
403
+ return $this->get('outputFormat');
404
+ }
405
+
406
+ /**
407
+ * @inheritdoc
408
+ */
409
+ public function getCredentials()
410
+ {
411
+ $result = $this->getSettingsByGroup('credentials');
412
+ // No separate key for now.
413
+ $result['emailonwarning'] = $result['emailonerror'];
414
+ return $result;
415
+ }
416
+
417
+ /**
418
+ * @inheritdoc
419
+ */
420
+ public function getCustomerSettings()
421
+ {
422
+ return $this->getSettingsByGroup('customer');
423
+ }
424
+
425
+ /**
426
+ * @inheritdoc
427
+ */
428
+ public function getInvoiceSettings()
429
+ {
430
+ return $this->getSettingsByGroup('invoice');
431
+ }
432
+
433
+ /**
434
+ * @inheritdoc
435
+ */
436
+ public function getEmailAsPdfSettings()
437
+ {
438
+ return $this->getSettingsByGroup('emailaspdf');
439
+ }
440
+
441
+ /**
442
+ * @inheritdoc
443
+ */
444
+ public function getShopSettings()
445
+ {
446
+ return $this->getSettingsByGroup('shop');
447
+ }
448
+
449
+ /**
450
+ * @inheritdoc
451
+ */
452
+ public function getShopEventSettings()
453
+ {
454
+ return $this->getSettingsByGroup('event');
455
+ }
456
+
457
+ /**
458
+ * @inheritdoc
459
+ */
460
+ public function getOtherSettings()
461
+ {
462
+ return $this->getSettingsByGroup('other');
463
+ }
464
+
465
+ /**
466
+ * Get all settings belonging to the same group.
467
+ *
468
+ * @param string $group
469
+ *
470
+ * @return array
471
+ * An array of settings.
472
+ */
473
+ protected function getSettingsByGroup($group)
474
+ {
475
+ $result = array();
476
+ foreach ($this->getKeyInfo() as $key => $keyInfo) {
477
+ if ($keyInfo['group'] === $group) {
478
+ $result[$key] = $this->get($key);
479
+ }
480
+ }
481
+ return $result;
482
+ }
483
+
484
+ /**
485
+ * Casts the values to their correct types.
486
+ *
487
+ * Values that come from a submitted form are all strings. Values that come
488
+ * from the config store might be NULL. However, internally we work with
489
+ * booleans or integers. So after reading from the config store or form, we
490
+ * cast the values to their expected types.
491
+ */
492
+ protected function castValues()
493
+ {
494
+ foreach ($this->getKeyInfo() as $key => $keyInfo) {
495
+ if (array_key_exists($key, $this->values)) {
496
+ switch ($keyInfo['type']) {
497
+ case 'string':
498
+ if (!is_string($this->values[$key])) {
499
+ $this->values[$key] = (string) $this->values[$key];
500
+ }
501
+ break;
502
+ case 'int':
503
+ if (!is_int($this->values[$key])) {
504
+ $this->values[$key] = (int) $this->values[$key];
505
+ }
506
+ break;
507
+ case 'bool':
508
+ if (!is_bool($this->values[$key])) {
509
+ $this->values[$key] = (bool) $this->values[$key];
510
+ }
511
+ break;
512
+ case 'array':
513
+ if (!is_array($this->values[$key])) {
514
+ $this->values[$key] = array($this->values[$key]);
515
+ }
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Returns a list of keys that are stored in the shop specific config store.
524
+ *
525
+ * @return array
526
+ */
527
+ public function getKeys()
528
+ {
529
+ $result = $this->getKeyInfo();
530
+ array_filter($result, function ($item) {
531
+ return $item['group'] != 'environment';
532
+ });
533
+ return array_keys($result);
534
+ }
535
+
536
+ /**
537
+ * Returns a set of default values for the various config settings.
538
+ *
539
+ * @return array
540
+ */
541
+ protected function getDefaults()
542
+ {
543
+ $result = $this->getKeyInfo();
544
+ $result = array_map(function ($item) {
545
+ return $item['default'];
546
+ }, $result);
547
+ return $result;
548
+ }
549
+
550
+ /**
551
+ * The hostname of the current server.
552
+ *
553
+ * Used for a default email address.
554
+ *
555
+ * @return string
556
+ */
557
+ public function getHostName()
558
+ {
559
+ $hostName = parse_url($_SERVER['REQUEST_URI'], PHP_URL_HOST);
560
+ if ($hostName) {
561
+ if ($pos = strpos($hostName, 'www.') !== false) {
562
+ $hostName = substr($hostName, $pos + strlen('www.'));
563
+ }
564
+ } else {
565
+ $hostName = 'example.com';
566
+ }
567
+ return $hostName;
568
+ }
569
+
570
+ /**
571
+ * Returns information (group and type) about the keys that are stored in the
572
+ * store config.
573
+ *
574
+ * @return array
575
+ * A keyed array with information (group and type) about the keys that are
576
+ * stored in the store config.
577
+ */
578
+ protected function getKeyInfo()
579
+ {
580
+ if ($this->keyInfo === null) {
581
+ $hostName = $this->getHostName();
582
+ $curlVersion = curl_version();
583
+ $shopDefaults = $this->getConfigStore()->getShopEnvironment();
584
+
585
+ $this->keyInfo = array(
586
+ 'baseUri' => array(
587
+ 'group' => 'environment',
588
+ 'type' => 'string',
589
+ 'default' => ServiceConfigInterface::baseUri,
590
+ ),
591
+ 'apiVersion' => array(
592
+ 'group' => 'environment',
593
+ 'type' => 'string',
594
+ 'default' => ServiceConfigInterface::apiVersion,
595
+ ),
596
+ 'outputFormat' => array(
597
+ 'group' => 'environment',
598
+ 'type' => 'string',
599
+ 'default' => ServiceConfigInterface::outputFormat,
600
+ ),
601
+ 'libraryVersion' => array(
602
+ 'group' => 'environment',
603
+ 'type' => 'string',
604
+ 'default' => ServiceConfigInterface::libraryVersion,
605
+ ),
606
+ 'moduleVersion' => array(
607
+ 'group' => 'environment',
608
+ 'type' => 'string',
609
+ 'default' => $shopDefaults['moduleVersion'],
610
+ ),
611
+ 'shopName' => array(
612
+ 'group' => 'environment',
613
+ 'type' => 'string',
614
+ 'default' => $shopDefaults['shopName'],
615
+ ),
616
+ 'shopVersion' => array(
617
+ 'group' => 'environment',
618
+ 'type' => 'string',
619
+ 'default' => $shopDefaults['shopVersion'],
620
+ ),
621
+ 'phpVersion' => array(
622
+ 'group' => 'environment',
623
+ 'type' => 'string',
624
+ 'default' => phpversion(),
625
+ ),
626
+ 'os' => array(
627
+ 'group' => 'environment',
628
+ 'type' => 'string',
629
+ 'default' => php_uname(),
630
+ ),
631
+ 'curlVersion' => array(
632
+ 'group' => 'environment',
633
+ 'type' => 'string',
634
+ 'default' => "{$curlVersion['version']} (ssl: {$curlVersion['ssl_version']}; zlib: {$curlVersion['libz_version']})",
635
+ ),
636
+ 'jsonVersion' => array(
637
+ 'group' => 'environment',
638
+ 'type' => 'string',
639
+ 'default' => phpversion('json'),
640
+ ),
641
+ 'contractcode' => array(
642
+ 'group' => 'credentials',
643
+ 'type' => 'string',
644
+ 'default' => '',
645
+ ),
646
+ 'username' => array(
647
+ 'group' => 'credentials',
648
+ 'type' => 'string',
649
+ 'default' => '',
650
+ ),
651
+ 'password' => array(
652
+ 'group' => 'credentials',
653
+ 'type' => 'string',
654
+ 'default' => '',
655
+ ),
656
+ 'emailonerror' => array(
657
+ 'group' => 'credentials',
658
+ 'type' => 'string',
659
+ 'default' => '',
660
+ ),
661
+ 'defaultCustomerType' => array(
662
+ 'group' => 'customer',
663
+ 'type' => 'int',
664
+ 'default' => 0,
665
+ ),
666
+ 'sendCustomer' => array(
667
+ 'group' => 'customer',
668
+ 'type' => 'bool',
669
+ 'default' => true,
670
+ ),
671
+ 'genericCustomerEmail' => array(
672
+ 'group' => 'customer',
673
+ 'type' => 'string',
674
+ 'default' => 'consumer@' . $hostName,
675
+ ),
676
+ 'overwriteIfExists' => array(
677
+ 'group' => 'customer',
678
+ 'type' => 'bool',
679
+ 'default' => true,
680
+ ),
681
+ 'salutation' => array(
682
+ 'group' => 'customer',
683
+ 'type' => 'string',
684
+ 'default' => '',
685
+ ),
686
+ 'defaultAccountNumber' => array(
687
+ 'group' => 'invoice',
688
+ 'type' => 'int',
689
+ 'default' => 0,
690
+ ),
691
+ 'defaultCostCenter' => array(
692
+ 'group' => 'invoice',
693
+ 'type' => 'int',
694
+ 'default' => 0,
695
+ ),
696
+ 'defaultInvoiceTemplate' => array(
697
+ 'group' => 'invoice',
698
+ 'type' => 'int',
699
+ 'default' => 0,
700
+ ),
701
+ 'defaultInvoicePaidTemplate' => array(
702
+ 'group' => 'invoice',
703
+ 'type' => 'int',
704
+ 'default' => 0,
705
+ ),
706
+ 'removeEmptyShipping' => array(
707
+ 'group' => 'invoice',
708
+ 'type' => 'bool',
709
+ 'default' => false,
710
+ ),
711
+ // @todo: add to UI?
712
+ 'addMissingAmountLine' => array(
713
+ 'group' => 'invoice',
714
+ 'type' => 'bool',
715
+ 'default' => true,
716
+ ),
717
+ // @todo: add to UI if shop does support it (PS?, WC?).
718
+ 'useMargin' => array(
719
+ 'group' => 'invoice',
720
+ 'type' => 'bool',
721
+ 'default' => false,
722
+ ),
723
+ // @todo: add these 3 to UI for shops where this is used (OC2, could others use this?).
724
+ 'optionsAllOn1Line' => array(
725
+ 'group' => 'invoice',
726
+ 'type' => 'int',
727
+ 'default' => 2,
728
+ ),
729
+ 'optionsAllOnOwnLine' => array(
730
+ 'group' => 'invoice',
731
+ 'type' => 'int',
732
+ 'default' => 4,
733
+ ),
734
+ 'optionsMaxLength' => array(
735
+ 'group' => 'invoice',
736
+ 'type' => 'int',
737
+ 'default' => 120,
738
+ ),
739
+ 'digitalServices' => array(
740
+ 'group' => 'shop',
741
+ 'type' => 'int',
742
+ 'default' => InvoiceConfigInterface::DigitalServices_Unknown,
743
+ ),
744
+ 'vatFreeProducts' => array(
745
+ 'group' => 'shop',
746
+ 'type' => 'int',
747
+ 'default' => InvoiceConfigInterface::VatFreeProducts_Unknown,
748
+ ),
749
+ 'invoiceNrSource' => array(
750
+ 'group' => 'shop',
751
+ 'type' => 'int',
752
+ 'default' => InvoiceConfigInterface::InvoiceNrSource_ShopInvoice,
753
+ ),
754
+ 'dateToUse' => array(
755
+ 'group' => 'shop',
756
+ 'type' => 'int',
757
+ 'default' => InvoiceConfigInterface::InvoiceDate_InvoiceCreate,
758
+ ),
759
+ 'triggerInvoiceSendEvent' => array(
760
+ 'group' => 'event',
761
+ 'type' => 'int',
762
+ 'default' => ConfigInterface::TriggerInvoiceSendEvent_None,
763
+ ),
764
+ 'triggerOrderStatus' => array(
765
+ 'group' => 'event',
766
+ 'type' => 'array',
767
+ 'default' => array(),
768
+ ),
769
+ 'emailAsPdf' => array(
770
+ 'group' => 'emailaspdf',
771
+ 'type' => 'bool',
772
+ 'default' => false,
773
+ ),
774
+ 'emailBcc' => array(
775
+ 'group' => 'emailaspdf',
776
+ 'type' => 'string',
777
+ 'default' => '',
778
+ ),
779
+ 'emailFrom' => array(
780
+ 'group' => 'emailaspdf',
781
+ 'type' => 'string',
782
+ 'default' => '',
783
+ ),
784
+ 'subject' => array(
785
+ 'group' => 'emailaspdf',
786
+ 'type' => 'string',
787
+ 'default' => '',
788
+ ),
789
+ 'confirmReading' => array(
790
+ 'group' => 'emailaspdf',
791
+ 'type' => 'bool',
792
+ 'default' => false,
793
+ ),
794
+ 'debug' => array(
795
+ 'group' => 'other',
796
+ 'type' => 'int',
797
+ 'default' => ServiceConfigInterface::Debug_None,
798
+ ),
799
+ 'logLevel' => array(
800
+ 'group' => 'other',
801
+ 'type' => 'int',
802
+ 'default' => Log::Error,
803
+ ),
804
+ );
805
+ }
806
+ return $this->keyInfo;
807
+ }
808
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigForm.php ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\Form;
5
+ use Siel\Acumulus\Helpers\Log;
6
+ use Siel\Acumulus\Helpers\TranslatorInterface;
7
+ use Siel\Acumulus\Invoice\ConfigInterface as InvoiceConfigInterface;
8
+ use Siel\Acumulus\Web\ConfigInterface as WebConfigInterface;
9
+
10
+ /**
11
+ * Provides basic config form handling.
12
+ *
13
+ * Shop specific overrides should - of course - implement the abstract method:
14
+ * - getShopOrderStatuses()
15
+ * Should typically override:
16
+ * - getInvoiceNrSourceOptions()
17
+ * - getDateToUseOptions()
18
+ * - getTriggerInvoiceSendEventOptions()
19
+ * And may optionally (have to) override:
20
+ * - systemValidate()
21
+ * - isSubmitted()
22
+ * - setSubmittedValues()
23
+ */
24
+ abstract class ConfigForm extends Form
25
+ {
26
+ /** @var \Siel\Acumulus\Shop\Config */
27
+ protected $acumulusConfig;
28
+
29
+ /**
30
+ * @var array
31
+ * Contact types picklist result, used to test the connection, storing it in
32
+ * this property prevents another webservice call.
33
+ */
34
+ protected $contactTypes;
35
+
36
+ /**
37
+ * Constructor.
38
+ *
39
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
40
+ * @param Config $config
41
+ */
42
+ public function __construct(TranslatorInterface $translator, Config $config)
43
+ {
44
+ parent::__construct($translator);
45
+
46
+ $translations = new ConfigFormTranslations();
47
+ $this->translator->add($translations);
48
+
49
+ $this->acumulusConfig = $config;
50
+ }
51
+
52
+ /**
53
+ * {@inheritdoc}
54
+ *
55
+ * This is the set of values as are stored in the config.
56
+ */
57
+ protected function getDefaultFormValues()
58
+ {
59
+ return $this->acumulusConfig->getCredentials() + $this->acumulusConfig->getShopSettings() + $this->acumulusConfig->getShopEventSettings() + $this->acumulusConfig->getCustomerSettings() + $this->acumulusConfig->getInvoiceSettings() + $this->acumulusConfig->getEmailAsPdfSettings() + $this->acumulusConfig->getOtherSettings();
60
+ }
61
+
62
+ /**
63
+ * {@inheritdoc}
64
+ *
65
+ * The results are restricted to the known config keys.
66
+ */
67
+ protected function setSubmittedValues()
68
+ {
69
+ $postedValues = $this->getPostedValues();
70
+ // Check if the full form was displayed or only the account details.
71
+ $fullForm = array_key_exists('salutation', $postedValues);
72
+ foreach ($this->acumulusConfig->getKeys() as $key) {
73
+ if (!$this->addIfIsset($this->submittedValues, $key, $postedValues)) {
74
+ // Add unchecked checkboxes, but only if the full form was displayed as
75
+ // all checkboxes on this form appear in the full form only.
76
+ if ($fullForm && $this->isCheckboxKey($key)) {
77
+ $this->submittedValues[$key] = '';
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * {@inheritdoc}
85
+ */
86
+ protected function validate()
87
+ {
88
+ $regexpEmail = '/^[^@<>,; "\']+@([^.@ ,;]+\.)+[^.@ ,;]+$/';
89
+ $regexpMultiEmail = '/^[^@<>,; "\']+@([^.@ ,;]+\.)+[^.@ ,;]+([,;][^@<>,; "\']+@([^.@ ,;]+\.)+[^.@ ,;]+)*$/';
90
+
91
+ if (empty($this->submittedValues['contractcode'])) {
92
+ $this->errorMessages['contractcode'] = $this->t('message_validate_contractcode_0');
93
+ } elseif (!is_numeric($this->submittedValues['contractcode'])) {
94
+ $this->errorMessages['contractcode'] = $this->t('message_validate_contractcode_1');
95
+ }
96
+ if (empty($this->submittedValues['username'])) {
97
+ $this->errorMessages['username'] = $this->t('message_validate_username_0');
98
+ }
99
+ if (empty($this->submittedValues['password'])) {
100
+ $this->errorMessages['password'] = $this->t('message_validate_password_0');
101
+ }
102
+ if (empty($this->submittedValues['emailonerror'])) {
103
+ $this->errorMessages['emailonerror'] = $this->t('message_validate_email_1');
104
+ } else if (!preg_match($regexpEmail, $this->submittedValues['emailonerror'])) {
105
+ $this->errorMessages['emailonerror'] = $this->t('message_validate_email_0');
106
+ }
107
+ if (!empty($this->submittedValues['genericCustomerEmail']) && !preg_match($regexpEmail, $this->submittedValues['genericCustomerEmail'])) {
108
+ $this->errorMessages['genericCustomerEmail'] = $this->t('message_validate_email_2');
109
+ }
110
+ if (!empty($this->submittedValues['emailFrom']) && !preg_match($regexpEmail, $this->submittedValues['emailFrom'])) {
111
+ $this->errorMessages['emailFrom'] = $this->t('message_validate_email_4');
112
+ }
113
+ if (!empty($this->submittedValues['emailBcc']) && !preg_match($regexpMultiEmail, $this->submittedValues['emailBcc'])) {
114
+ $this->errorMessages['emailBcc'] = $this->t('message_validate_email_3');
115
+ }
116
+ if (isset($this->submittedValues['emailAsPdf']) && (bool) $this->submittedValues['emailAsPdf'] && (!array_key_exists('sendCustomer', $this->submittedValues) || !(bool) $this->submittedValues['sendCustomer'])) {
117
+ $this->errorMessages['conflicting_options'] = $this->t('message_validate_conflicting_options');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * {@inheritdoc}
123
+ *
124
+ * Saves the submitted and validated form values in the configuration store.
125
+ */
126
+ protected function execute()
127
+ {
128
+ return $this->acumulusConfig->save($this->submittedValues);
129
+ }
130
+
131
+ /**
132
+ * {@inheritdoc}
133
+ *
134
+ * This override returns the config form. At the minimum, this includes the
135
+ * account settings. If these are OK, the other settings are included as well.
136
+ */
137
+ public function getFieldDefinitions()
138
+ {
139
+ $fields = array();
140
+
141
+ // 1st fieldset: Acumulus account settings.
142
+ $fields['accountSettingsHeader'] = array(
143
+ 'type' => 'fieldset',
144
+ 'legend' => $this->t('accountSettingsHeader'),
145
+ // Account fields.
146
+ 'fields' => array(
147
+ 'contractcode' => array(
148
+ 'type' => 'text',
149
+ 'label' => $this->t('field_code'),
150
+ 'attributes' => array(
151
+ 'required' => true,
152
+ 'size' => 20,
153
+ ),
154
+ ),
155
+ 'username' => array(
156
+ 'type' => 'text',
157
+ 'label' => $this->t('field_username'),
158
+ 'attributes' => array(
159
+ 'required' => true,
160
+ 'size' => 20,
161
+ ),
162
+ ),
163
+ 'password' => array(
164
+ 'type' => 'password',
165
+ 'label' => $this->t('field_password'),
166
+ 'attributes' => array(
167
+ 'required' => true,
168
+ 'size' => 20,
169
+ ),
170
+ ),
171
+ 'emailonerror' => array(
172
+ 'type' => 'email',
173
+ 'label' => $this->t('field_email'),
174
+ 'description' => $this->t('desc_email'),
175
+ 'attributes' => array(
176
+ 'required' => true,
177
+ 'size' => 20,
178
+ ),
179
+ ),
180
+ ),
181
+ );
182
+
183
+ $message = $this->checkAccountSettings();
184
+ $accountOk = empty($message);
185
+
186
+ // 2nd fieldset: message or invoice sending related fields.
187
+ $fields['invoiceSettingsHeader'] = array(
188
+ 'type' => 'fieldset',
189
+ 'legend' => $this->t('invoiceSettingsHeader'),
190
+ );
191
+ if (!$accountOk) {
192
+ $fields['invoiceSettingsHeader']['fields'] = array(
193
+ 'invoiceMessage' => array(
194
+ 'type' => 'markup',
195
+ 'value' => $message,
196
+ ),
197
+ );
198
+ } else {
199
+ $invoiceNrSourceOptions = $this->getInvoiceNrSourceOptions();
200
+ if (count($invoiceNrSourceOptions) === 1) {
201
+ // Make it a hidden field.
202
+ $invoiceNrSourceField = array(
203
+ 'type' => 'hidden',
204
+ 'value' => reset($invoiceNrSourceOptions),
205
+ );
206
+ } else {
207
+ $invoiceNrSourceField = array(
208
+ 'type' => 'radio',
209
+ 'label' => $this->t('field_invoiceNrSource'),
210
+ 'description' => $this->t('desc_invoiceNrSource'),
211
+ 'options' => $invoiceNrSourceOptions,
212
+ 'attributes' => array(
213
+ 'required' => true,
214
+ ),
215
+ );
216
+ }
217
+
218
+ $dateToUseOptions = $this->getDateToUseOptions();
219
+ if (count($dateToUseOptions) === 1) {
220
+ // Make it a hidden field.
221
+ $dateToUseField = array(
222
+ 'type' => 'hidden',
223
+ 'value' => reset($dateToUseOptions),
224
+ );
225
+ } else {
226
+ $dateToUseField = array(
227
+ 'type' => 'radio',
228
+ 'label' => $this->t('field_dateToUse'),
229
+ 'description' => $this->t($this->t('desc_dateToUse')),
230
+ 'options' => $dateToUseOptions,
231
+ 'attributes' => array(
232
+ 'required' => true,
233
+ ),
234
+ );
235
+ }
236
+
237
+ $triggerInvoiceSendEventOptions = $this->getTriggerInvoiceSendEventOptions();
238
+ if (count($triggerInvoiceSendEventOptions) === 1) {
239
+ // Make it a hidden field.
240
+ $triggerInvoiceSendEventField = array(
241
+ 'type' => 'hidden',
242
+ 'value' => reset($triggerInvoiceSendEventOptions),
243
+ );
244
+ } else {
245
+ $triggerInvoiceSendEventField = array(
246
+ 'type' => 'radio',
247
+ 'label' => $this->t('field_triggerInvoiceSendEvent'),
248
+ 'description' => $this->t($this->t('desc_triggerInvoiceSendEvent')),
249
+ 'options' => $triggerInvoiceSendEventOptions,
250
+ 'attributes' => array(
251
+ 'required' => true,
252
+ ),
253
+ );
254
+ }
255
+
256
+ $fields['invoiceSettingsHeader']['fields'] = array(
257
+ 'digitalServices' => array(
258
+ 'type' => 'radio',
259
+ 'label' => $this->t('field_digitalServices'),
260
+ 'description' => $this->t('desc_digitalServices'),
261
+ 'options' => $this->getDigitalServicesOptions(),
262
+ 'attributes' => array(
263
+ 'required' => true,
264
+ ),
265
+ ),
266
+ 'vatFreeProducts' => array(
267
+ 'type' => 'radio',
268
+ 'label' => $this->t('field_vatFreeProducts'),
269
+ 'description' => $this->t('desc_vatFreeProducts'),
270
+ 'options' => $this->getVatFreeProductsOptions(),
271
+ 'attributes' => array(
272
+ 'required' => true,
273
+ ),
274
+ ),
275
+ 'invoiceNrSource' => $invoiceNrSourceField,
276
+ 'dateToUse' => $dateToUseField,
277
+ 'defaultCustomerType' => array(
278
+ 'type' => 'select',
279
+ 'label' => $this->t('field_defaultCustomerType'),
280
+ 'options' => $this->picklistToOptions($this->contactTypes, 'contacttypes', 0, $this->t('option_empty')),
281
+ ),
282
+ 'salutation' => array(
283
+ 'type' => 'text',
284
+ 'label' => $this->t('field_salutation'),
285
+ 'description' => $this->t('desc_salutation'),
286
+ 'attributes' => array(
287
+ 'size' => 30,
288
+ ),
289
+ ),
290
+ 'clientData' => array(
291
+ 'type' => 'checkbox',
292
+ 'label' => $this->t('field_clientData'),
293
+ 'description' => $this->t('desc_clientData'),
294
+ 'options' => array(
295
+ 'sendCustomer' => $this->t('option_sendCustomer'),
296
+ 'overwriteIfExists' => $this->t('option_overwriteIfExists'),
297
+ ),
298
+ ),
299
+ 'defaultAccountNumber' => array(
300
+ 'type' => 'select',
301
+ 'label' => $this->t('field_defaultAccountNumber'),
302
+ 'description' => $this->t('desc_defaultAccountNumber'),
303
+ 'options' => $this->picklistToOptions($this->acumulusConfig->getService()->getPicklistAccounts(), 'accounts', 0, $this->t('option_empty')),
304
+ ),
305
+ 'defaultCostCenter' => array(
306
+ 'type' => 'select',
307
+ 'label' => $this->t('field_defaultCostCenter'),
308
+ 'description' => $this->t('desc_defaultCostCenter'),
309
+ 'options' => $this->picklistToOptions($this->acumulusConfig->getService()->getPicklistCostCenters(), 'costcenters', 0, $this->t('option_empty')),
310
+ ),
311
+ 'defaultInvoiceTemplate' => array(
312
+ 'type' => 'select',
313
+ 'label' => $this->t('field_defaultInvoiceTemplate'),
314
+ 'options' => $this->picklistToOptions($invoiceTemplates = $this->acumulusConfig->getService()->getPicklistInvoiceTemplates(), 'invoicetemplates', 0, $this->t('option_empty')),
315
+ ),
316
+ 'defaultInvoicePaidTemplate' => array(
317
+ 'type' => 'select',
318
+ 'label' => $this->t('field_defaultInvoicePaidTemplate'),
319
+ 'description' => $this->t('desc_defaultInvoiceTemplates'),
320
+ 'options' => $this->picklistToOptions($invoiceTemplates, 'invoicetemplates', 0, $this->t('option_same_template')),
321
+ ),
322
+ 'removeEmptyShipping' => array(
323
+ 'type' => 'checkbox',
324
+ 'label' => $this->t('field_removeEmptyShipping'),
325
+ 'description' => $this->t('desc_removeEmptyShipping'),
326
+ 'options' => array(
327
+ 'removeEmptyShipping' => $this->t('option_removeEmptyShipping'),
328
+ ),
329
+ ),
330
+ 'triggerInvoiceSendEvent' => $triggerInvoiceSendEventField,
331
+ );
332
+ if (array_key_exists(ConfigInterface::TriggerInvoiceSendEvent_OrderStatus, $triggerInvoiceSendEventOptions)) {
333
+ $options = $this->getOrderStatusesList();
334
+ $fields['invoiceSettingsHeader']['fields']['triggerOrderStatus'] = array(
335
+ 'name' => 'triggerOrderStatus[]',
336
+ 'type' => 'select',
337
+ 'label' => $this->t('field_triggerOrderStatus'),
338
+ 'description' => $this->t('desc_triggerOrderStatus'),
339
+ 'options' => $options,
340
+ 'attributes' => array(
341
+ 'multiple' => true,
342
+ 'size' => min(count($options), 8),
343
+ ),
344
+ );
345
+ }
346
+ }
347
+
348
+ // 3rd fieldset: email as PDF settings.
349
+ if ($accountOk) {
350
+ $fields['emailAsPdfSettingsHeader'] = array(
351
+ 'type' => 'fieldset',
352
+ 'legend' => $this->t('emailAsPdfSettingsHeader'),
353
+ 'description' => $this->t('desc_emailAsPdfInformation'),
354
+ 'fields' => array(
355
+ 'emailAsPdf' => array(
356
+ 'type' => 'checkbox',
357
+ 'label' => $this->t('field_emailAsPdf'),
358
+ 'description' => $this->t('desc_emailAsPdf'),
359
+ 'options' => array(
360
+ 'emailAsPdf' => $this->t('option_emailAsPdf'),
361
+ ),
362
+ ),
363
+ 'emailFrom' => array(
364
+ 'type' => 'email',
365
+ 'label' => $this->t('field_emailFrom'),
366
+ 'description' => $this->t('desc_emailFrom'),
367
+ 'attributes' => array(
368
+ 'size' => 30,
369
+ ),
370
+ ),
371
+ 'emailBcc' => array(
372
+ 'type' => 'email',
373
+ 'label' => $this->t('field_emailBcc'),
374
+ 'description' => $this->t('desc_emailBcc'),
375
+ 'attributes' => array(
376
+ 'multiple' => true,
377
+ 'size' => 30,
378
+ ),
379
+ ),
380
+ 'subject' => array(
381
+ 'type' => 'text',
382
+ 'label' => $this->t('field_subject'),
383
+ 'description' => $this->t('desc_subject'),
384
+ 'attributes' => array(
385
+ 'size' => 60,
386
+ ),
387
+ ),
388
+ ),
389
+ );
390
+ }
391
+
392
+ // 4th fieldset: Acumulus version information.
393
+ $env = $this->acumulusConfig->getEnvironment();
394
+ $fields['versionInformationHeader'] = array(
395
+ 'type' => 'fieldset',
396
+ 'legend' => $this->t('versionInformationHeader'),
397
+ 'fields' => array(
398
+ 'debug' => array(
399
+ 'type' => 'radio',
400
+ 'label' => $this->t('field_debug'),
401
+ 'description' => $this->t('desc_debug'),
402
+ 'options' => array(
403
+ WebConfigInterface::Debug_None => $this->t('option_debug_1'),
404
+ WebConfigInterface::Debug_SendAndLog => $this->t('option_debug_2'),
405
+ WebConfigInterface::Debug_TestMode => $this->t('option_debug_4'),
406
+ WebConfigInterface::Debug_StayLocal => $this->t('option_debug_3'),
407
+ ),
408
+ 'attributes' => array(
409
+ 'required' => true,
410
+ ),
411
+ ),
412
+ 'logLevel' => array(
413
+ 'type' => 'radio',
414
+ 'label' => $this->t('field_logLevel'),
415
+ 'description' => $this->t('desc_logLevel'),
416
+ 'options' => array(
417
+ Log::None => $this->t('option_logLevel_0'),
418
+ Log::Error => $this->t('option_logLevel_1'),
419
+ Log::Warning => $this->t('option_logLevel_2'),
420
+ Log::Notice => $this->t('option_logLevel_3'),
421
+ Log::Debug => $this->t('option_logLevel_4'),
422
+ ),
423
+ 'attributes' => array(
424
+ 'required' => true,
425
+ ),
426
+ ),
427
+ 'versionInformation' => array(
428
+ 'type' => 'markup',
429
+ 'value' => "<p>Application: Acumulus module {$env['moduleVersion']}; Library: {$env['libraryVersion']}; Shop: {$env['shopName']} {$env['shopVersion']};<br>" .
430
+ "Environment: PHP {$env['phpVersion']}; Curl: {$env['curlVersion']}; JSON: {$env['jsonVersion']}; OS: {$env['os']}.</p>",
431
+ ),
432
+ 'versionInformationDesc' => array(
433
+ 'type' => 'markup',
434
+ 'value' => $this->t('desc_versionInformation'),
435
+ ),
436
+
437
+ ),
438
+ );
439
+
440
+ return $fields;
441
+ }
442
+
443
+ /**
444
+ * Checks if the account settings are known and correct by trying to download
445
+ * a picklist.
446
+ *
447
+ * @return string
448
+ * Message to show in the 2nd and 3rd fieldset. Empty if successful.
449
+ */
450
+ protected function checkAccountSettings()
451
+ {
452
+ // Check if we can retrieve a picklist. This indicates if the account
453
+ // settings are known and correct.
454
+ $message = '';
455
+ $this->contactTypes = null;
456
+ $credentials = $this->acumulusConfig->getCredentials();
457
+ if (!empty($credentials['contractcode']) && !empty($credentials['username']) && !empty($credentials['password'])) {
458
+ $this->contactTypes = $this->acumulusConfig->getService()->getPicklistContactTypes();
459
+ if (!empty($this->contactTypes['errors'])) {
460
+ $message = $this->t($this->contactTypes['errors'][0]['code'] == 401 ? 'message_error_auth' : 'message_error_comm');
461
+ $this->errorMessages += $this->acumulusConfig->getService()->resultToMessages($this->contactTypes, false);
462
+ }
463
+ } else {
464
+ // First fill in your account details.
465
+ $message = $this->t('message_auth_unknown');
466
+ }
467
+ return $message;
468
+ }
469
+
470
+ /**
471
+ * Converts a picklist response into an options list.
472
+ *
473
+ * @param array $picklist
474
+ * The picklist result structure.
475
+ * @param string $key
476
+ * The key in the picklist result structure under which the actual results
477
+ * can be found.
478
+ * @param string|null $emptyValue
479
+ * The value to use for an empty selection.
480
+ * @param string|null $emptyText
481
+ * The label to use for an empty selection.
482
+ *
483
+ * @return array
484
+ */
485
+ protected function picklistToOptions(array $picklist, $key, $emptyValue = null, $emptyText = null)
486
+ {
487
+ $result = array();
488
+
489
+ if ($emptyValue !== null) {
490
+ $result[$emptyValue] = $emptyText;
491
+ }
492
+ if (!empty($key)) {
493
+ // Take the results under the key. This is to be able to follow the
494
+ // structure returned by the picklist services.
495
+ $picklist = $picklist[$key];
496
+ }
497
+ array_walk($picklist, function ($value) use (&$result) {
498
+ list($optionValue, $optionText) = array_values($value);
499
+ $result[$optionValue] = $optionText;
500
+ });
501
+
502
+ return $result;
503
+ }
504
+
505
+ /**
506
+ * {@inheritdoc}
507
+ */
508
+ protected function getCheckboxKeys()
509
+ {
510
+ return array(
511
+ 'sendCustomer' => 'clientData',
512
+ 'overwriteIfExists' => 'clientData',
513
+ 'removeEmptyShipping' => 'removeEmptyShipping',
514
+ 'emailAsPdf' => 'emailAsPdf',
515
+ );
516
+ }
517
+
518
+ /**
519
+ * Returns an option list of all order statuses including an empty choice.
520
+ *
521
+ * Do not override this method but implement getShopOrderStatuses() instead.
522
+ *
523
+ * @return array
524
+ * An options array of all order statuses.
525
+ */
526
+ protected function getOrderStatusesList()
527
+ {
528
+ $result = array();
529
+
530
+ $result['0'] = $this->t('option_empty_triggerOrderStatus');
531
+ $result += $this->getShopOrderStatuses();
532
+
533
+ return $result;
534
+ }
535
+
536
+ /**
537
+ * Returns an option list of all shop order statuses.
538
+ *
539
+ * @return array
540
+ * An array of all shop order statuses, with the key being the ID for
541
+ * the dropdown item and the value being the label for the dropdown item.
542
+ */
543
+ abstract protected function getShopOrderStatuses();
544
+
545
+ /**
546
+ * Returns a list of valid sources that can be used as invoice number.
547
+ *
548
+ * This may differ per shop as not all shops support invoices as a separate
549
+ * entity.
550
+ *
551
+ * Overrides should typically return a subset of the constants defined in this
552
+ * base implementation, but including at least
553
+ * ConfigInterface::InvoiceNrSource_Acumulus.
554
+ *
555
+ * @return array
556
+ * An array keyed by the option values and having translated descriptions as
557
+ * values.
558
+ */
559
+ protected function getInvoiceNrSourceOptions()
560
+ {
561
+ return array(
562
+ InvoiceConfigInterface::InvoiceNrSource_ShopInvoice => $this->t('option_invoiceNrSource_1'),
563
+ InvoiceConfigInterface::InvoiceNrSource_ShopOrder => $this->t('option_invoiceNrSource_2'),
564
+ InvoiceConfigInterface::InvoiceNrSource_Acumulus => $this->t('option_invoiceNrSource_3'),
565
+ );
566
+ }
567
+
568
+ /**
569
+ * Returns a list of valid date sources that can be used as invoice date.
570
+ *
571
+ * This may differ per shop as not all shops support invoices as a separate
572
+ * entity.
573
+ *
574
+ * Overrides should typically return a subset of the constants defined in this
575
+ * base implementation, but including at least
576
+ * ConfigInterface::InvoiceDate_Transfer.
577
+ *
578
+ * @return array
579
+ * An array keyed by the option values and having translated descriptions as
580
+ * values.
581
+ */
582
+ protected function getDateToUseOptions()
583
+ {
584
+ return array(
585
+ InvoiceConfigInterface::InvoiceDate_InvoiceCreate => $this->t('option_dateToUse_1'),
586
+ InvoiceConfigInterface::InvoiceDate_OrderCreate => $this->t('option_dateToUse_2'),
587
+ InvoiceConfigInterface::InvoiceDate_Transfer => $this->t('option_dateToUse_3'),
588
+ );
589
+ }
590
+
591
+ /**
592
+ * Returns a list of events that can trigger the automatic sending of an
593
+ * invoice.
594
+ *
595
+ * This may differ per shop as not all shops define events for all moments
596
+ * that can be used to trigger the sending of an invoice.
597
+ *
598
+ * Overrides should typically return a subset of the constants defined in this
599
+ * base implementation. The return array may be empty or only contain
600
+ * ConfigInterface::TriggerInvoiceSendEvent_None, to indicate that no
601
+ * automatic sending is possible (shop does not define any event like model).
602
+ *
603
+ * @return array
604
+ * An array keyed by the option values and having translated descriptions as
605
+ * values.
606
+ */
607
+ protected function getTriggerInvoiceSendEventOptions()
608
+ {
609
+ return array(
610
+ ConfigInterface::TriggerInvoiceSendEvent_None => $this->t('option_triggerInvoiceSendEvent_0'),
611
+ ConfigInterface::TriggerInvoiceSendEvent_OrderStatus => $this->t('option_triggerInvoiceSendEvent_1'),
612
+ ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate => $this->t('option_triggerInvoiceSendEvent_2'),
613
+ );
614
+ }
615
+
616
+ /**
617
+ * Returns a list of options for the digital services field.
618
+ *
619
+ * @return array
620
+ * An array keyed by the option values and having translated descriptions as
621
+ * values.
622
+ */
623
+ protected function getDigitalServicesOptions()
624
+ {
625
+ return array(
626
+ InvoiceConfigInterface::DigitalServices_Both => $this->t('option_digitalServices_1'),
627
+ InvoiceConfigInterface::DigitalServices_No => $this->t('option_digitalServices_2'),
628
+ InvoiceConfigInterface::DigitalServices_Only => $this->t('option_digitalServices_3'),
629
+ );
630
+ }
631
+
632
+ /**
633
+ * Returns a list of options for the vat free products field.
634
+ *
635
+ * @return array
636
+ * An array keyed by the option values and having translated descriptions as
637
+ * values.
638
+ */
639
+ protected function getVatFreeProductsOptions()
640
+ {
641
+ return array(
642
+ InvoiceConfigInterface::VatFreeProducts_Both => $this->t('option_vatFreeProducts_1'),
643
+ InvoiceConfigInterface::VatFreeProducts_No => $this->t('option_vatFreeProducts_2'),
644
+ InvoiceConfigInterface::VatFreeProducts_Only => $this->t('option_vatFreeProducts_3'),
645
+ );
646
+ }
647
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigFormTranslations.php ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for the configuration form.
8
+ */
9
+ class ConfigFormTranslations extends TranslationCollection
10
+ {
11
+ protected $nl = array(
12
+ 'config_form_title' => 'Acumulus | Instellingen',
13
+ 'config_form_header' => 'Acumulus instellingen',
14
+
15
+ 'message_validate_contractcode_0' => 'Het veld Contractcode is verplicht, vul de contractcode in die u ook gebruikt om in te loggen op Acumulus.',
16
+ 'message_validate_contractcode_1' => 'Het veld Contractcode is een numeriek veld, vul de contractcode in die u ook gebruikt om in te loggen op Acumulus.',
17
+ 'message_validate_username_0' => 'Het veld Gebruikersnaam is verplicht, vul de gebruikersnaam in die u ook gebruikt om in te loggen op Acumulus.',
18
+ 'message_validate_password_0' => 'Het veld Wachtwoord is verplicht, vul het wachtwoord in dat u ook gebruikt om in te loggen op Acumulus.',
19
+ 'message_validate_email_0' => 'Het veld Email is geen valide e-mailadres, vul uw eigen e-mailadres in.',
20
+ 'message_validate_email_1' => 'Het veld Email is verplicht, vul uw eigen e-mailadres in.',
21
+ 'message_validate_email_2' => 'Het veld (fictieve klant) Email is geen valide e-mailadres, vul een correct e-mailadres in.',
22
+ 'message_validate_email_3' => 'Het veld BCC is geen valide e-mailadres, vul een correct e-mailadres in.',
23
+ 'message_validate_email_4' => 'Het veld Afzender is geen valide e-mailadres, vul een correct e-mailadres in.',
24
+ 'message_validate_conflicting_options' => 'Als u geen klantgegevens naar Acumulus verstuurt, kunt u Acumulus geen PDF factuur laten versturen. Pas één van beide opties aan.',
25
+
26
+ 'message_form_success' => 'De instellingen zijn opgeslagen.',
27
+ 'message_form_error' => 'Er is een fout opgetreden bij het opslaan van de instellingen',
28
+ 'message_uninstall' => 'Wilt u de configuratie-instellingen verwijderen?',
29
+
30
+ 'message_error_auth' => 'Uw Acumulus account gegevens zijn onjuist. Zodra u de correcte gevens hebt ingevuld, worden hier de overige instellingen getoond.',
31
+ 'message_error_comm' => 'Er is een fout opgetreden bij het ophalen van uw gegevens van Acumulus. Probeer het later nog eens. Zodra de verbinding hersteld is worden hier de overige instellingen getoond.',
32
+ 'message_auth_unknown' => 'Zodra u uw Acumulus accountgegevens hebt ingevuld, worden hier de overige instellingen getoond.',
33
+
34
+ 'accountSettingsHeader' => 'Uw Acumulus account',
35
+ 'field_code' => 'Contractcode',
36
+ 'field_username' => 'Gebruikersnaam',
37
+ 'field_password' => 'Wachtwoord',
38
+ 'field_email' => 'E-mail',
39
+ 'desc_email' => 'Het e-mailadres waarop u geïnformeerd wordt over fouten die zijn opgetreden tijdens het versturen van facturen. Omdat deze module niet kan weten of het vanuit een beheerdersscherm is aangeroepen, zal het geen berichten op het scherm plaatsen. Daarom is het invullen van een e-mailadres verplicht.',
40
+
41
+ 'invoiceSettingsHeader' => 'Uw factuurinstellingen',
42
+ 'option_empty' => 'Maak uw keuze',
43
+
44
+ 'field_digitalServices' => 'Verkoopt u digitale diensten?',
45
+ 'option_digitalServices_1' => 'Zowel digitale diensten als normale producten.',
46
+ 'option_digitalServices_2' => 'Alleen producten die onder Nederlandse BTW vallen.',
47
+ 'option_digitalServices_3' => 'Alleen digitale diensten die met buitenlandse BTW belast moeten worden voor buitenlandse klanten.',
48
+ 'desc_digitalServices' => 'Geef aan of u in uw winkel digitale diensten aanbiedt waarbij u buitenlandse BTW moet hanteren voor EU klanten.
49
+ Zie <a href="http://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/zakelijk/btw/zakendoen_met_het_buitenland/goederen_en_diensten_naar_andere_eu_landen/btw_berekenen_bij_diensten/wijziging_in_digitale_diensten_vanaf_2015/wijziging_in_digitale_diensten_vanaf_2015">Belastingdienst: diensten naar andere EU landen</a>.
50
+ Met behulp van deze instelling kan de koppeling beter: het type factuur bepalen; controles uitvoeren; en BTW tarieven, voor zover berekend op basis van afgeronde bedragen, terugrekenen.',
51
+
52
+ 'field_vatFreeProducts' => 'Verkoopt u van BTW vrijgestelde producten of diensten?',
53
+ 'option_vatFreeProducts_1' => 'Zowel BTW vrije als aan BTW onderhevige producten en/of diensten.',
54
+ 'option_vatFreeProducts_2' => 'Alleen aan BTW onderhevige producten en/of diensten.',
55
+ 'option_vatFreeProducts_3' => 'Alleen producten of diensten die van BTW vrijgesteld zijn.',
56
+ 'desc_vatFreeProducts' => 'Geef aan of u in uw winkel producten en/of diensten aanbiedt die vrijgesteld zijn van BTW, bv. onderwijs.
57
+ Met behulp van deze instelling kan de koppeling beter het type factuur bepalen en controles uitvoeren.',
58
+
59
+ 'field_invoiceNrSource' => 'Factuurnummer',
60
+ 'option_invoiceNrSource_1' => 'Gebruik het factuurnummer van uw webwinkel. Let op: als er nog geen factuur aan een bestelling gekoppeld is, zal het bestelnummer gebruikt worden!',
61
+ 'option_invoiceNrSource_2' => 'Gebruik het bestelnummer van uw webwinkel.',
62
+ 'option_invoiceNrSource_3' => 'Laat Acumulus het factuurnummer bepalen.',
63
+ 'desc_invoiceNrSource' => 'U kunt hier kiezen welk nummer Acumulus als factuurnummer moet gebruiken.',
64
+
65
+ 'field_dateToUse' => 'Factuurdatum',
66
+ 'option_dateToUse_1' => 'Gebruik de aanmaakdatum van de factuur. Let op: als er nog geen factuur aan uw bestelling gekoppeld is, zal de aanmaakdatum van de bestelling gebruikt worden!',
67
+ 'option_dateToUse_2' => 'Gebruik de aanmaakdatum van de bestelling.',
68
+ 'option_dateToUse_3' => 'Gebruik de datum van het overzetten.',
69
+ 'desc_dateToUse' => 'U kunt hier kiezen welke datum de factuur in Acumulus moet krijgen.',
70
+
71
+ 'field_salutation' => 'Aanhef',
72
+ 'desc_salutation' => 'U kunt hier de aanhef specificeren zoals u die op de Acumulus factuur wilt hebben. U kunt [#property] gebruiken om een waarde uit de order of klantgegevens te gebruiken, b.v. "Beste [#firstName],".',
73
+
74
+ 'field_clientData' => 'Klantadresgegevens',
75
+ 'option_sendCustomer' => 'Uw (niet zakelijke) klanten automatisch aan uw relaties in Acumulus toevoegen.',
76
+ 'option_overwriteIfExists' => 'Overschrijf bestaande adresgegevens.',
77
+ 'desc_clientData' => 'Binnen Acumulus is het mogelijk om uw klantrelaties te beheren.
78
+ Deze koppeling voegt automatisch uw klanten aan het relatieoverzicht van Acumulus toe.
79
+ Dit is niet altijd gewenst en kunt u voorkomen door de eerste optie uit te zetten.
80
+ Hierdoor worden alle transacties van consumenten binnen uw webwinkel onder 1 vaste fictieve relatie ingeboekt in Acumulus.
81
+ De tweede optie moet u alleen uitzetten als u direct in Acumulus adresgegevens van uw webwinkel-klanten bijwerkt.
82
+ Als u de eerste optie heeft uitgezet, geldt de tweede optie alleen voor uw zakelijke klanten.',
83
+
84
+ 'field_defaultCustomerType' => 'Importeer klanten als',
85
+
86
+ 'field_defaultAccountNumber' => 'Bankrekeningnummer',
87
+ 'desc_defaultAccountNumber' => 'Maakt u binnen Acumulus gebruik van meerdere rekeningen en wilt u alle bestellingen uit uw webwinkel op een specifieke rekening binnen laten komen, kies dan hier het bankrekeningnummer.',
88
+
89
+ 'field_defaultCostCenter' => 'Kostenplaats',
90
+ 'desc_defaultCostCenter' => 'Maakt u binnen Acumulus gebruik van meerdere kostenplaatsen en wilt u alle bestellingen uit uw webwinkel op een specifieke kostenplaats binnen laten komen, kies dan hier de kostenplaats.',
91
+
92
+ 'field_defaultInvoiceTemplate' => 'Factuur-sjabloon (niet betaald)',
93
+ 'field_defaultInvoicePaidTemplate' => 'Factuur-sjabloon (betaald)',
94
+ 'option_same_template' => 'Zelfde sjabloon als voor niet betaald',
95
+ 'desc_defaultInvoiceTemplates' => 'Maakt u binnen Acumulus gebruik van meerdere factuur-sjablonen en wilt u alle bestellingen uit uw webwinkel op een specifieke factuursjabloon printen, kies dan hier de factuur-sjablonen voor niet betaalde respectievelijk betaalde bestellingen.',
96
+
97
+ 'field_removeEmptyShipping' => 'Verzendkosten',
98
+ 'option_removeEmptyShipping' => 'Verstuur geen "gratis verzending" of "zelf afhalen" regels.',
99
+ 'desc_removeEmptyShipping' => 'Omdat Acumulus pakbonnen kan printen, verstuurt deze extensie normaal gesproken altijd een factuurregel met de verzendkosten/methode, zelfs met gratis verzending of zelf afhalen.
100
+ Vink deze optie aan als u geen regel op uw factuur of pakbon wil voor gratis verzending of zelf afhalen.',
101
+
102
+ 'field_triggerInvoiceSendEvent' => 'Moment van versturen',
103
+ 'option_triggerInvoiceSendEvent_0' => 'Niet automatisch versturen.',
104
+ 'option_triggerInvoiceSendEvent_1' => 'Als een bestelling de hieronder door u gekozen status bereikt.',
105
+ 'option_triggerInvoiceSendEvent_2' => 'Als de factuur wordt aangemaakt voor deze bestelling.',
106
+ 'desc_triggerInvoiceSendEvent' => 'U kunt hier kiezen op welk moment de factuur wordt verstuurd. Als u voor "Niet automatisch overzetten" kiest, moet u de facturen zelf overzetten m.b.v. het batchformulier.',
107
+ 'option_empty_triggerOrderStatus' => 'Niet automatisch versturen',
108
+ 'field_triggerOrderStatus' => 'Bestelstatus',
109
+ 'desc_triggerOrderStatus' => 'U kunt hier kiezen bij welke bestelstatussen facturen worden overgezet naar Acumulus. Als u meerdere statussen kiest wordt een order overgezet zodra deze één van de gekozen statussen bereikt. Een order wordt maximaal 1 keer overgezet. Deze koppeling gebruikt alleen gegevens van de bestelling, dus u kunt elke status kiezen. De factuur hoeft dus nog niet aangemaakt te zijn.',
110
+
111
+ 'emailAsPdfSettingsHeader' => 'PDF Factuur',
112
+ 'desc_emailAsPdfInformation' => 'Bij het versturen van bestellinggegevens naar Acumulus, kunt u Acumulus een PDF factuur laten versturen naar uw klant. Deze wordt direct verstuurd naar het door de klant opgegeven emailadres.',
113
+
114
+ 'field_emailAsPdf' => 'Optie inschakelen',
115
+ 'option_emailAsPdf' => 'Verstuur de factuur als PDF vanuit Acumulus.',
116
+ 'desc_emailAsPdf' => 'Als u deze optie aanvinkt, kunt u de overige opties gebruiken om de emailverzending aan uw wensen aan te passen. Het bericht in de email body kunt u niet hier instellen, dat kunt u in Acumulus doen onder "Beheer - Factuur-sjablonen".',
117
+
118
+ 'field_emailFrom' => 'Afzender',
119
+ 'desc_emailFrom' => 'Het email adres dat als afzender gebruikt moet worden. Als u dit leeg laat wordt het emailadres uit het Acumulus sjabloon gebruikt. Wij adviseren dit veld leeg te laten.',
120
+
121
+ 'field_emailBcc' => 'BCC',
122
+ 'desc_emailBcc' => 'Additionele emailadressen om de factuur naar toe te sturen, bv. het emailadres van uw eigen administratie-afdeling. Als u dit leeg laat wordt de factuur alleen naar de klant verstuurd.',
123
+
124
+ 'field_subject' => 'Onderwerp',
125
+ 'desc_subject' => 'Het onderwerp van de email. Als u dit leeg laat wordt "Factuur [factuurnummer] Bestelling [bestelnummer]" gebruikt. U kunt [#b] gebruiken om het bestelnummer in de onderwerpregel te plaatsen en [#f] voor het factuurnummer (van de webshop, niet van Acumulus).',
126
+
127
+ 'versionInformationHeader' => 'Informatie over deze module',
128
+ 'desc_versionInformation' => 'Vermeld aub deze gegevens bij een supportverzoek.',
129
+
130
+ 'field_debug' => 'Support en debug',
131
+ 'option_debug_1' => 'Verzend berichten naar Acumulus en ontvang alleen een mail bij fouten of waarschuwingen.',
132
+ 'option_debug_2' => 'Verzend berichten naar Acumulus en ontvang een mail met het verzonden en ontvangen bericht.',
133
+ 'option_debug_3' => 'Verzend geen berichten naar Acumulus, verstuur alleen een mail met het bericht dat verstuurd zou worden.',
134
+ 'option_debug_4' => 'Verzend berichten naar Acumulus maar Acumulus zal alleen de invoer controleren op fouten en waarschuwingen en geen veranderingen opslaan.',
135
+ 'desc_debug' => 'U kunt hier een support mode kiezen. Kies voor de eerste optie tenzij u i.v.m. een supportverzoek bent geïnstrueerd om iets anders te kiezen.',
136
+
137
+ 'field_logLevel' => 'Logniveau',
138
+ 'option_logLevel_0' => 'Log geen enkel bericht.',
139
+ 'option_logLevel_1' => 'Log alleen foutmeldingen.',
140
+ 'option_logLevel_2' => 'Log foutmeldingen en waarschuwingen.',
141
+ 'option_logLevel_3' => 'Log foutmeldingen, waarschuwingen en mededelingen.',
142
+ 'option_logLevel_4' => 'Log foutmeldingen, waarschuwingen, mededelingen en communicatieberichten.',
143
+ 'desc_logLevel' => 'U kunt hier een logniveau kiezen. Kies voor de 1e of 2e optie tenzij u i.v.m. een supportverzoek bent geïnstrueerd om iets anders te kiezen.',
144
+
145
+ );
146
+
147
+ protected $en = array(
148
+ 'config_form_title' => 'Acumulus | Settings',
149
+ 'config_form_header' => 'Acumulus settings',
150
+
151
+ 'button_save' => 'Save',
152
+ 'button_cancel' => 'Cancel',
153
+
154
+ 'message_validate_contractcode_0' => 'The field Contract code is required, please fill in the contract code you use to log in to Acumulus.',
155
+ 'message_validate_contractcode_1' => 'The field Contract code is a numeric field, please fill in the contract code you use to log in to Acumulus.',
156
+ 'message_validate_username_0' => 'The field User name is required, please fill in the user name you use to log in to Acumulus.',
157
+ 'message_validate_password_0' => 'The field Password is required, please fill in the password you use to log in to Acumulus.',
158
+ 'message_validate_email_0' => 'The field Email is not a valid e-mail address, please fill in your own e-mail address.',
159
+ 'message_validate_email_1' => 'The field Email is required, please fill in your own e-mail address.',
160
+ 'message_validate_email_2' => 'The field (fictitious customer) Email is not a valid e-mail address, please fill in a correct e-mail address.',
161
+ 'message_validate_email_3' => 'The field BCC is not a valid e-mail address, please fill in a valid e-mail address.',
162
+ 'message_validate_email_4' => 'The field Sender is not a valid e-mail address, please fill in a valid e-mail address.',
163
+ 'message_validate_conflicting_options' => 'If you don\'t send customer data to Acumulus, Acumulus cannot send PDF invoices. Change one of the options.',
164
+
165
+ 'message_form_success' => 'The settings are saved.',
166
+ 'message_form_error' => 'an error occurred wile saving the settings.',
167
+ 'message_uninstall' => 'Are you sure to delete the configuration settings?',
168
+
169
+ 'message_error_auth' => 'Your Acumulus connection settings are incorrect. Please check them. After you have entered the correct connection settings the other settings will be shown as well.',
170
+ 'message_error_comm' => 'The module encountered an error retrieving your Acumulus configuration. Please try again. When the connection is restored the other settings will be shown as well.',
171
+ 'message_auth_unknown' => 'When your Acumulus connection settings are filled in, the other settings will be shown as well.',
172
+
173
+ 'accountSettingsHeader' => 'Your Acumulus connection settings',
174
+ 'field_code' => 'Contract code',
175
+ 'field_username' => 'User name',
176
+ 'field_password' => 'Password',
177
+ 'field_email' => 'E-mail',
178
+ 'desc_email' => 'The e-mail address at which you will be informed about any errors that occur during invoice sending. As this module cannot know if it is called from an interactive administrator screen, it will not display any messages in the user interface. Therefore you have to fill in an e-mail address.',
179
+
180
+ 'invoiceSettingsHeader' => 'Your invoice settings',
181
+ 'option_empty' => 'Select one',
182
+
183
+ 'field_digitalServices' => 'Do you sell digital services?',
184
+ 'option_digitalServices_1' => 'Both digitals services and normal products.',
185
+ 'option_digitalServices_2' => 'Only products that are subject to dutch VAT.',
186
+ 'option_digitalServices_3' => 'Only digital services subject to the regulations concerning using foreign VAT rates.',
187
+ 'desc_digitalServices' => 'Select whether your store offers digital services that are subject to foreign VAT for clients in other EU countries.
188
+ See <a href="http://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/zakelijk/btw/zakendoen_met_het_buitenland/goederen_en_diensten_naar_andere_eu_landen/btw_berekenen_bij_diensten/wijziging_in_digitale_diensten_vanaf_2015/wijziging_in_digitale_diensten_vanaf_2015">Dutch tax office: services to other EU countries (in dutch)</a>.
189
+ Using this setting this plugin can better determine the invoice type; perform some validations; and extract exact VAT rates where they are calculated using rounded amounts.',
190
+
191
+ 'field_vatFreeProducts' => 'Do you sell VAT free products or services?',
192
+ 'option_vatFreeProducts_1' => 'Both VAT free and VAT liable products or services.',
193
+ 'option_vatFreeProducts_2' => 'Only products or services that are VAT liable.',
194
+ 'option_vatFreeProducts_3' => 'Only VAT free products or services.',
195
+ 'desc_vatFreeProducts' => 'Select whether your store offers products or services that are VAT free, e.g. education.
196
+ Using this setting this plugin can better determine the invoice type and perform some validations.',
197
+
198
+ 'field_invoiceNrSource' => 'Invoice number',
199
+ 'option_invoiceNrSource_1' => 'Use the web shop invoice number. Note: if no invoice has been created for the order yet, the order number will be used!',
200
+ 'option_invoiceNrSource_2' => 'Use the web shop order number as invoice number.',
201
+ 'option_invoiceNrSource_3' => 'Have Acumulus create an invoice number.',
202
+ 'desc_invoiceNrSource' => 'Select which number to use for the invoice in Acumulus.',
203
+
204
+ 'field_dateToUse' => 'Invoice date',
205
+ 'option_dateToUse_1' => 'Use the invoice date. Note: if no invoice has been created for the order yet, the order create date will be used!',
206
+ 'option_dateToUse_2' => 'Use the order create date.',
207
+ 'option_dateToUse_3' => 'Use the transfer date.',
208
+ 'desc_dateToUse' => 'Select which date to use for the invoice in Acumulus.',
209
+
210
+ 'field_salutation' => 'Salutations',
211
+ 'desc_salutation' => 'Specify the salutations for the Acumulus invoice. You can use [#property] to get a property value out of the order or customer values, e.g. "Dear [#firstName],".',
212
+
213
+ 'field_clientData' => 'Customer address data',
214
+ 'option_sendCustomer' => 'Send consumer client records to Acumulus.',
215
+ 'option_overwriteIfExists' => 'Overwrite existing address data.',
216
+ 'desc_clientData' => 'Acumulus allows you to store client data.
217
+ This extension automatically sends client data to Acumulus.
218
+ If you don\'t want this, uncheck this option.
219
+ All consumer invoices will be booked on one and the same fictitious client.
220
+ You should uncheck the second option if you edit customer address data manually in Acumulus.
221
+ If you unchecked the first option, the second option only applies to business clients.',
222
+
223
+ 'field_defaultCustomerType' => 'Create customers as',
224
+
225
+ 'field_defaultAccountNumber' => 'Bank account number',
226
+ 'desc_defaultAccountNumber' => 'Select the (bank) account number at which you want to receive all your order payments.',
227
+
228
+ 'field_defaultCostCenter' => 'Cost center',
229
+ 'desc_defaultCostCenter' => 'Select the cost center to assign your orders to.',
230
+
231
+ 'field_defaultInvoiceTemplate' => 'Invoice template (due)',
232
+ 'field_defaultInvoicePaidTemplate' => 'Invoice template (paid)',
233
+ 'option_same_template' => 'Same template as for due',
234
+ 'desc_defaultInvoiceTemplates' => 'Select the invoice templates to print your web shop orders with due respectively paid orders.',
235
+
236
+ 'field_removeEmptyShipping' => 'Shipping costs',
237
+ 'option_removeEmptyShipping' => 'Do not send free shipping or in store pick-up lines on invoices.',
238
+ 'desc_removeEmptyShipping' => 'To allow Acumulus to print packing slips, this extension normally always sends a shipping line, even with free shipping or in store pickup.
239
+ If you don\'t want this, check this option.',
240
+
241
+ 'field_triggerInvoiceSendEvent' => 'Send the invoice to Acumulus',
242
+ 'option_triggerInvoiceSendEvent_0' => 'Do not send automatically.',
243
+ 'option_triggerInvoiceSendEvent_1' => 'When an order reaches the state as defined below.',
244
+ 'option_triggerInvoiceSendEvent_2' => 'When the invoice gets created for this order.',
245
+ 'desc_triggerInvoiceSendEvent' => 'Select when to send the invoice to Acumulus. If you select "Do not send automatically" you will have to use the send batch form.',
246
+
247
+ 'option_empty_triggerOrderStatus' => 'Do not send automatically',
248
+ 'field_triggerOrderStatus' => 'Order state',
249
+ 'desc_triggerOrderStatus' => 'Select the order states at which orders will be sent to Acumulus. If you select multiple states, an order will be sent as soon as one of the selected states is reached. An order will only be sent once. This extension only uses order data. so you may select any status. The invoice does not already have to be created.',
250
+
251
+ 'emailAsPdfSettingsHeader' => 'PDF Invoice',
252
+ 'desc_emailAsPdfInformation' => 'On sending the order details to Acumulus, Acumulus can send a PDF invoice to your customer. The mail will be sent to the clients\' email address.',
253
+
254
+ 'field_emailAsPdf' => 'Enable the feature',
255
+ 'option_emailAsPdf' => 'Have Acumulus send the invoice as PDF.',
256
+ 'desc_emailAsPdf' => 'If you check this option, you can use the other options below to configure the emails to your preferences. However, to configure the text in them mail body, go to Acumulus to "Beheer - Factuur-sjablonen".',
257
+
258
+ 'field_emailFrom' => 'Sender',
259
+ 'desc_emailFrom' => 'The email address to use as sender. If you leave this empty, the email address of the Acumulus template will be used. We advice you to leave this empty.',
260
+
261
+ 'field_emailBcc' => 'BCC',
262
+ 'desc_emailBcc' => 'Additional email addresses to send the invoice to, e.g. the email address of your own administration department. If you leave this empty the invoice email will only be sent to your client.',
263
+
264
+ 'field_subject' => 'Subject',
265
+ 'desc_subject' => 'The subject line of the email. If you leave this empty "Invoice [invoice#] Order [order#]" will be used. You can use [#b] to place the order number in the subject and [#f] for the invoice number (from the webshop, not Acumulus).',
266
+
267
+ 'versionInformationHeader' => 'Module information',
268
+ 'desc_versionInformation' => 'Please mention this information with any support request.',
269
+
270
+ 'field_debug' => 'Debug and support',
271
+ 'option_debug_1' => 'Send messages to Acumulus and only receive a mail when there are errors or warnings.',
272
+ 'option_debug_2' => 'Send messages to Acumulus and receive a mail with the results.',
273
+ 'option_debug_3' => 'Do not send messages to Acumulus, but receive a mail with the message as would have been sent.',
274
+ 'option_debug_4' => 'Send messages to Acumulus, but Acumulus will only check the input for errors and warnings, not store any changes.',
275
+ 'desc_debug' => 'Select a debug mode. Choose for the 1st option unless otherwise instructed by support staff.',
276
+
277
+ 'field_logLevel' => 'Log level',
278
+ 'option_logLevel_0' => 'Don\'t log any message.',
279
+ 'option_logLevel_1' => 'Only log error messages.',
280
+ 'option_logLevel_2' => 'Log error messages and warnings.',
281
+ 'option_logLevel_3' => 'Log error messages, warnings and notices.',
282
+ 'option_logLevel_4' => 'Log error messages, warnings, notices and communication messages.',
283
+ 'desc_logLevel' => 'Select a log level. Choose for the 1st or 2nd option unless otherwise instructed by support staff.',
284
+
285
+ );
286
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigInterface.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ /**
5
+ * Defines an interface to retrieve shop specific configuration settings.
6
+ *
7
+ * Configuration is stored in the host environment, normally a web shop.
8
+ * This interface abstracts from how a specific web shop does so.
9
+ */
10
+ interface ConfigInterface
11
+ {
12
+ // Web shop configuration related constants.
13
+ const TriggerInvoiceSendEvent_None = 0;
14
+ const TriggerInvoiceSendEvent_OrderStatus = 1;
15
+ const TriggerInvoiceSendEvent_InvoiceCreate = 2;
16
+
17
+ /**
18
+ * Returns the set of settings related to reacting to shop events.
19
+ *
20
+ * @return array
21
+ * A keyed array with the keys:
22
+ * - triggerInvoiceSendEvent
23
+ * - triggerOrderStatus
24
+ */
25
+ public function getShopEventSettings();
26
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigStore.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\Log;
5
+
6
+ /**
7
+ * Defines an interface to access the shop specific's config store.
8
+ */
9
+ abstract class ConfigStore implements ConfigStoreInterface
10
+ {
11
+ /** @var string */
12
+ protected $shopName;
13
+
14
+ /**
15
+ * ConfigStore constructor.
16
+ *
17
+ * @param string $shopNamespace
18
+ */
19
+ public function __construct($shopNamespace)
20
+ {
21
+ $pos = strrpos($shopNamespace, '\\');
22
+ $this->shopName = $pos !== false ? substr($shopNamespace, $pos + 1) : $shopNamespace;
23
+ }
24
+
25
+ /**
26
+ * Performs common save tasks.
27
+ *
28
+ * @param array $values
29
+ *
30
+ * @return array
31
+ */
32
+ protected function saveCommon(array $values)
33
+ {
34
+ $copy = $values;
35
+ if (!empty($copy['password'])) {
36
+ $copy['password'] = 'REMOVED FOR SECURITY';
37
+ }
38
+ Log::getInstance()->notice('ConfigStore::save(): saving %s', serialize($copy));
39
+
40
+ // Remove password if not sent along.
41
+ if (array_key_exists('password', $values) && empty($values['password'])) {
42
+ unset($values['password']);
43
+ }
44
+ return $values;
45
+ }
46
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfigStoreInterface.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ /**
5
+ * Defines an interface to access the shop specific's config store.
6
+ */
7
+ interface ConfigStoreInterface
8
+ {
9
+ /**
10
+ * Returns an array with shop specific environment settings.
11
+ *
12
+ * @return array
13
+ * An array with keys:
14
+ * - moduleVersion
15
+ * - shopName
16
+ * - shopVersion
17
+ */
18
+ public function getShopEnvironment();
19
+
20
+ /**
21
+ * Loads the configuration from the actual configuration provider.
22
+ *
23
+ * @param array $keys
24
+ * An array of keys that are expected to be loaded.
25
+ *
26
+ * @return array
27
+ * An array with the raw (not necessarily casted) configuration values keyed
28
+ * by their name. Note that these values will overwrite the default values,
29
+ * so no NULL values should be returned, though other "empty" values (false,
30
+ * 0) may be returned.
31
+ */
32
+ public function load(array $keys);
33
+
34
+ /**
35
+ * Stores the values to the actual configuration provider.
36
+ *
37
+ * @param array $values
38
+ * A keyed array that contains the values to store. Note that values may be
39
+ * "empty", eg 0 or false. Only NULL values may be ignored
40
+ *
41
+ * @return bool
42
+ * Success.
43
+ */
44
+ public function save(array $values);
45
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfirmUninstallForm.php ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @file
4
+ * Contains ${NAMESPACE}${NAME}.
5
+ */
6
+
7
+ namespace Siel\Acumulus\Shop;
8
+
9
+ use Siel\Acumulus\Helpers\Form;
10
+ use Siel\Acumulus\Helpers\TranslatorInterface;
11
+
12
+ /**
13
+ * Class ConfirmUninstallForm
14
+ */
15
+ class ConfirmUninstallForm extends Form
16
+ {
17
+ /**
18
+ * Constructor.
19
+ *
20
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
21
+ */
22
+ public function __construct(TranslatorInterface $translator)
23
+ {
24
+ parent::__construct($translator);
25
+
26
+ $translations = new ConfirmUninstallFormTranslations();
27
+ $this->translator->add($translations);
28
+ }
29
+
30
+ /**
31
+ * Executes the form action on valid form submission.
32
+ *
33
+ * Override to implement the actual form handling, like saving values.
34
+ *
35
+ * @return bool
36
+ * Success.
37
+ */
38
+ protected function execute()
39
+ {
40
+ // @todo: Implement execute() method.
41
+ }
42
+
43
+ /**
44
+ * Internal version of getFields();
45
+ *
46
+ * Internal method, do not call directly.
47
+ * DO override this method not getFields().
48
+ *
49
+ * @return array[]
50
+ * The definition of the form.
51
+ */
52
+ protected function getFieldDefinitions()
53
+ {
54
+ $fields = array();
55
+
56
+ // 1st fieldset: Confirm uninstall message.
57
+ $fields['uninstallHeader'] = array(
58
+ 'type' => 'fieldset',
59
+ 'legend' => $this->t('uninstallHeader'),
60
+ 'fields' => array(
61
+ 'uninstall_message' => array(
62
+ 'type' => 'markup',
63
+ 'value' => $this->t('desc_uninstall'),
64
+ ),
65
+ ),
66
+ );
67
+
68
+ return $fields;
69
+ }
70
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ConfirmUninstallFormTranslations.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for the confirm uninstall form.
8
+ */
9
+ class ConfirmUninstallFormTranslations extends TranslationCollection
10
+ {
11
+ protected $nl = array(
12
+ 'uninstallHeader' => 'Bevestig verwijderen',
13
+ 'desc_uninstall' => '<p>De module is uitgeschakeld. Maak een keuze of u ook alle data en instellingen wilt verwijderen of dat u deze (voorlopig) wilt bewaren.</p>',
14
+ );
15
+
16
+ protected $en = array(
17
+ 'uninstallHeader' => 'Confirm uninstall',
18
+ 'desc_uninstall' => '<p>The module has been disabled. Choose whether you also want to delete all data and settings or if you want to keep these for now.</p>',
19
+ );
20
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/InjectorInterface.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ /**
5
+ * InjectorInterface defines an interface to retrieve:
6
+ * - Instances of web shop specific overrides of the base classes and interfaces
7
+ * that are defined in the common package.
8
+ * - Singleton instances from other namespaces.
9
+ * - Instances that require some injection arguments in their constructor, that
10
+ * the implementing object can pass.
11
+ */
12
+ interface InjectorInterface
13
+ {
14
+ /**
15
+ * @return \Siel\Acumulus\Helpers\TranslatorInterface
16
+ */
17
+ public function getTranslator();
18
+
19
+ /**
20
+ * @return \Siel\Acumulus\Web\Service
21
+ */
22
+ public function getService();
23
+
24
+ /**
25
+ * Creates a wrapper object for a source object identified by the given
26
+ * parameters.
27
+ *
28
+ * @param string $invoiceSourceType
29
+ * The type of the invoice source to create.
30
+ * @param string|object|array $invoiceSourceOrId
31
+ * The invoice source itself or its id to create a Source wrapper for.
32
+ *
33
+ * @return \Siel\Acumulus\Invoice\Source
34
+ * A wrapper object around a shop specific invoice source object.
35
+ */
36
+ public function getSource($invoiceSourceType, $invoiceSourceOrId);
37
+
38
+ /**
39
+ * @return \Siel\Acumulus\Invoice\Completor
40
+ */
41
+ public function getCompletor();
42
+
43
+ /**
44
+ * @return \Siel\Acumulus\Invoice\Creator
45
+ */
46
+ public function getCreator();
47
+
48
+ /**
49
+ * @return \Siel\Acumulus\Helpers\Mailer
50
+ */
51
+ public function getMailer();
52
+
53
+ /**
54
+ * @return \Siel\Acumulus\Shop\ConfigStoreInterface
55
+ */
56
+ public function getConfigStore();
57
+
58
+ /**
59
+ * @return \Siel\Acumulus\Shop\InvoiceManager
60
+ */
61
+ public function getManager();
62
+
63
+ /**
64
+ * @return \Siel\Acumulus\Shop\AcumulusEntryModel
65
+ */
66
+ public function getAcumulusEntryModel();
67
+
68
+ /**
69
+ * @param string $type
70
+ * The type of form requested.
71
+ *
72
+ * @return \Siel\Acumulus\Helpers\Form
73
+ *
74
+ * @todo: start using this in all plugins.
75
+ */
76
+ public function getForm($type);
77
+
78
+ /**
79
+ * @return \Siel\Acumulus\Helpers\FormRenderer
80
+ *
81
+ * @todo: start using this in all plugins.
82
+ */
83
+ public function getFormRenderer();
84
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\Invoice\Source;
6
+ use Siel\Acumulus\Web\ConfigInterface as WebConfigInterface;
7
+
8
+ /**
9
+ * Provides functionality to manage invoices.
10
+ */
11
+ abstract class InvoiceManager
12
+ {
13
+ /** @var \Siel\Acumulus\Shop\Config */
14
+ protected $config;
15
+
16
+ /** @var \Siel\Acumulus\Invoice\Completor */
17
+ protected $completor;
18
+
19
+ /**
20
+ * @param Config $config
21
+ */
22
+ public function __construct(Config $config)
23
+ {
24
+ $this->config = $config;
25
+
26
+ $translations = new BatchTranslations();
27
+ $config->getTranslator()->add($translations);
28
+ }
29
+
30
+ /**
31
+ * Helper method to translate strings.
32
+ *
33
+ * @param string $key
34
+ * The key to get a translation for.
35
+ *
36
+ * @return string
37
+ * The translation for the given key or the key itself if no translation
38
+ * could be found.
39
+ */
40
+ protected function t($key)
41
+ {
42
+ return $this->config->getTranslator()->get($key);
43
+ }
44
+
45
+ /**
46
+ * Returns a list of invoice source types supported by this shop.
47
+ *
48
+ * The default implementation returns order and credit note. Override if the
49
+ * specific shop supports other types or does not support credit notes.
50
+ *
51
+ * @return string[]
52
+ * The list of supported invoice source types.
53
+ */
54
+ public function getSupportedInvoiceSourceTypes()
55
+ {
56
+ return array(
57
+ Source::Order,
58
+ Source::CreditNote,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Returns a list of existing invoice sources for the given id range.
64
+ *
65
+ * @param string $invoiceSourceType
66
+ * @param string $InvoiceSourceIdFrom
67
+ * @param string $InvoiceSourceIdTo
68
+ *
69
+ * @return \Siel\Acumulus\Invoice\Source[]
70
+ * An array of invoice sources of the given source type.
71
+ */
72
+ abstract public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo);
73
+
74
+ /**
75
+ * Returns a list of existing invoice sources for the given reference range.
76
+ *
77
+ * Should be overridden when the reference is not the internal id.
78
+ *
79
+ * @param string $invoiceSourceType
80
+ * @param string $InvoiceSourceReferenceFrom
81
+ * @param string $InvoiceSourceReferenceTo
82
+ *
83
+ * @return \Siel\Acumulus\Invoice\Source[]
84
+ * An array of invoice sources of the given source type.
85
+ */
86
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
87
+ {
88
+ return $this->getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo);
89
+ }
90
+
91
+ /**
92
+ * Returns a list of existing invoice sources for the given date range.
93
+ *
94
+ * @param string $invoiceSourceType
95
+ * @param DateTime $dateFrom
96
+ * @param DateTime $dateTo
97
+ *
98
+ * @return \Siel\Acumulus\Invoice\Source[]
99
+ * An array of invoice sources of the given source type.
100
+ */
101
+ abstract public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo);
102
+
103
+ /**
104
+ * Creates a set of Invoice Sources given their ids or shop specific sources.
105
+ *
106
+ * @param string $invoiceSourceType
107
+ * @param array $idsOrSources
108
+ *
109
+ * @return \Siel\Acumulus\Invoice\Source[]
110
+ * A non keyed array with invoice Sources.
111
+ */
112
+ public function getSourcesByIdsOrSources($invoiceSourceType, array $idsOrSources)
113
+ {
114
+ $results = array();
115
+ foreach ($idsOrSources as $sourceId) {
116
+ $results[] = $this->getSourceByIdOrSource($invoiceSourceType, $sourceId);
117
+ }
118
+ return $results;
119
+ }
120
+
121
+ /**
122
+ * Creates a source given its type and id.
123
+ *
124
+ * @param string $invoiceSourceType
125
+ * @param int|array|object $idOrSource
126
+ *
127
+ * @return \Siel\Acumulus\Invoice\Source
128
+ * An invoice Source.
129
+ */
130
+ protected function getSourceByIdOrSource($invoiceSourceType, $idOrSource)
131
+ {
132
+ return $this->config->getSource($invoiceSourceType, $idOrSource);
133
+ }
134
+
135
+ /**
136
+ * Sends multiple invoices to Acumulus.
137
+ *
138
+ * @param \Siel\Acumulus\Invoice\Source[] $invoiceSources
139
+ * @param bool $forceSend
140
+ * @param string[] $log
141
+ *
142
+ * @return bool
143
+ * Success.
144
+ */
145
+ public function sendMultiple(array $invoiceSources, $forceSend, array &$log)
146
+ {
147
+ $this->config->getTranslator()->add(new BatchTranslations());
148
+ $errorLogged = false;
149
+ $success = true;
150
+ $time_limit = ini_get('max_execution_time');
151
+ /** @var Source $invoiceSource */
152
+ foreach ($invoiceSources as $invoiceSource) {
153
+ // Try to keep the script running, but note that other systems involved,
154
+ // think the (Apache) web server, may have their own time-out.
155
+ // Use @ to prevent messages like "Warning: set_time_limit(): Cannot set
156
+ // max execution time limit due to system policy in ...".
157
+ if (!@set_time_limit($time_limit) && !$errorLogged) {
158
+ $this->config->getLog()->warning('InvoiceManager::sendMultiple(): could not set time limit.');
159
+ $errorLogged = true;
160
+ }
161
+
162
+ $status = $this->send($invoiceSource, $forceSend);
163
+ switch ($status) {
164
+ case WebConfigInterface::Status_Success:
165
+ $message = 'message_batch_send_1_success';
166
+ break;
167
+ case WebConfigInterface::Status_Errors:
168
+ case WebConfigInterface::Status_Exception:
169
+ $message = 'message_batch_send_1_errors';
170
+ $success = false;
171
+ break;
172
+ case WebConfigInterface::Status_Warnings:
173
+ $message = 'message_batch_send_1_warnings';
174
+ break;
175
+ case WebConfigInterface::Status_NotSent:
176
+ $message = 'message_batch_send_1_skipped';
177
+ break;
178
+ case WebConfigInterface::Status_SendingPrevented_InvoiceCreated:
179
+ $message = 'message_batch_send_1_prevented_invoiceCreated';
180
+ break;
181
+ case WebConfigInterface::Status_SendingPrevented_InvoiceCompleted:
182
+ $message = 'message_batch_send_1_prevented_invoiceCompleted';
183
+ break;
184
+ default:
185
+ $message = "Status unknown $status (sending invoice for %1\$s %2\$s)";
186
+ break;
187
+ }
188
+ $log[$invoiceSource->getId()] = sprintf($this->t($message), $this->t($invoiceSource->getType()), $invoiceSource->getReference());
189
+ }
190
+ return $success;
191
+ }
192
+
193
+ /**
194
+ * Processes an invoice source status change event.
195
+ *
196
+ * For now we don't look at the status for credit notes: they are always sent.
197
+ *
198
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
199
+ * The source whose status changed.
200
+ *
201
+ * @return int
202
+ * Status, one of the WebConfigInterface::Status_ constants.
203
+ */
204
+ public function sourceStatusChange(Source $invoiceSource)
205
+ {
206
+ $status = $invoiceSource->getStatus();
207
+ $statusString = $status === null ? 'null' : (string) $status;
208
+ $this->config->getLog()->notice('InvoiceManager::sourceStatusChange(%s %d, %s)', $invoiceSource->getType(), $invoiceSource->getId(), $statusString);
209
+ $result = WebConfigInterface::Status_NotSent;
210
+ $shopEventSettings = $this->config->getShopEventSettings();
211
+ if ($invoiceSource->getType() === Source::CreditNote
212
+ || ($shopEventSettings['triggerInvoiceSendEvent'] == Config::TriggerInvoiceSendEvent_OrderStatus
213
+ && in_array($status, $shopEventSettings['triggerOrderStatus']))
214
+ ) {
215
+ $result = $this->send($invoiceSource, false);
216
+ } else {
217
+ $this->config->getLog()->notice('InvoiceManager::sourceStatusChange(%s %d, %s): not sending triggerEvent = %d, triggerOrderStatus = [%s]',
218
+ $invoiceSource->getType(),
219
+ $invoiceSource->getId(),
220
+ $statusString,
221
+ $shopEventSettings['triggerInvoiceSendEvent'],
222
+ is_array($shopEventSettings['triggerOrderStatus']) ? implode(',', $shopEventSettings['triggerOrderStatus']) : 'no array'
223
+ );
224
+ }
225
+ return $result;
226
+ }
227
+
228
+ /**
229
+ * Processes an invoice create event.
230
+ *
231
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
232
+ * The source for which a shop invoice was created.
233
+ *
234
+ * @return int
235
+ * Status
236
+ */
237
+ public function invoiceCreate(Source $invoiceSource)
238
+ {
239
+ $this->config->getLog()->notice('InvoiceManager::invoiceCreate(%s %d)', $invoiceSource->getType(), $invoiceSource->getId());
240
+ $result = WebConfigInterface::Status_NotSent;
241
+ $shopEventSettings = $this->config->getShopEventSettings();
242
+ if ($shopEventSettings['triggerInvoiceSendEvent'] == Config::TriggerInvoiceSendEvent_InvoiceCreate) {
243
+ $result = $this->send($invoiceSource, false);
244
+ }
245
+ return $result;
246
+ }
247
+
248
+ /**
249
+ * Creates and sends an invoice to Acumulus for an order.
250
+ *
251
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
252
+ * The source object (order, credit note) for which the invoice was created.
253
+ * @param bool $forceSend
254
+ * force sending the invoice even if an invoice has already been sent for
255
+ * the given order.
256
+ *
257
+ * @return int
258
+ * Status.
259
+ */
260
+ public function send(Source $invoiceSource, $forceSend = false)
261
+ {
262
+ $result = WebConfigInterface::Status_NotSent;
263
+ if ($forceSend || !$this->config->getAcumulusEntryModel()->getByInvoiceSource($invoiceSource)) {
264
+ $invoice = $this->config->getCreator()->create($invoiceSource);
265
+
266
+ $this->triggerInvoiceCreated($invoice, $invoiceSource);
267
+
268
+ if ($invoice !== null) {
269
+ $localMessages = array();
270
+ $invoice = $this->config->getCompletor()->complete($invoice, $invoiceSource, $localMessages);
271
+
272
+ $this->triggerInvoiceCompleted($invoice, $invoiceSource);
273
+
274
+ if ($invoice !== null) {
275
+ $service = $this->config->getService();
276
+ $result = $service->invoiceAdd($invoice);
277
+ $result = $service->mergeLocalMessages($result, $localMessages);
278
+
279
+ $this->triggerInvoiceSent($invoice, $invoiceSource, $result);
280
+
281
+ // Check if an entryid was created and store entry id and token.
282
+ if (!empty($result['invoice']['entryid'])) {
283
+ $this->config->getAcumulusEntryModel()->save($invoiceSource, $result['invoice']['entryid'], $result['invoice']['token']);
284
+ }
285
+
286
+ // Send a mail if there are messages.
287
+ $messages = $service->resultToMessages($result);
288
+ if (!empty($messages)) {
289
+ $this->config->getLog()->notice('InvoiceManager::send(%s %d, %s) result: %s',
290
+ $invoiceSource->getType(), $invoiceSource->getId(), $forceSend ? 'true' : 'false', $service->messagesToText($messages));
291
+ $this->mailInvoiceAddResult($result, $messages, $invoiceSource);
292
+ }
293
+
294
+ $result = $result['status'];
295
+ } else {
296
+ $result = WebConfigInterface::Status_SendingPrevented_InvoiceCompleted;
297
+ $this->config->getLog()->notice('InvoiceManager::send(%s %d, %s): invoiceCompleted prevented sending',
298
+ $invoiceSource->getType(), $invoiceSource->getId(), $forceSend ? 'true' : 'false');
299
+ }
300
+ } else {
301
+ $result = WebConfigInterface::Status_SendingPrevented_InvoiceCreated;
302
+ $this->config->getLog()->notice('InvoiceManager::send(%s %d, %s): invoiceCreated prevented sending',
303
+ $invoiceSource->getType(), $invoiceSource->getId(), $forceSend ? 'true' : 'false');
304
+ }
305
+ } else {
306
+ $this->config->getLog()->notice('InvoiceManager::send(%s %d, %s): not sent',
307
+ $invoiceSource->getType(), $invoiceSource->getId(), $forceSend ? 'true' : 'false');
308
+ }
309
+ return $result;
310
+ }
311
+
312
+ /**
313
+ * Sends an email with the results of a sent invoice.
314
+ *
315
+ * The mail is sent to the shop administrator (emailonerror setting).
316
+ *
317
+ * @param array $result
318
+ * @param string[] $messages
319
+ * @param \Siel\Acumulus\Invoice\Source $invoiceSource
320
+ *
321
+ * @return bool
322
+ * Success.
323
+ */
324
+ protected function mailInvoiceAddResult(array $result, array $messages, Source $invoiceSource)
325
+ {
326
+ return $this->config->getMailer()->sendInvoiceAddMailResult($result, $messages, $invoiceSource->getType(), $invoiceSource->getReference());
327
+ }
328
+
329
+ /**
330
+ * Triggers an event that an invoice for Acumulus has been created and is
331
+ * ready to be completed and sent.
332
+ *
333
+ * This allows to inject custom behavior to alter the invoice just before
334
+ * completing and sending.
335
+ *
336
+ * @param array $invoice
337
+ * The invoice that has been created.
338
+ * @param Source $invoiceSource
339
+ * The source object (order, credit note) for which the invoice was created.
340
+ */
341
+ protected function triggerInvoiceCreated(array &$invoice, Source $invoiceSource)
342
+ {
343
+ // Default implementation: no event.
344
+ }
345
+
346
+ /**
347
+ * Triggers an event that an invoice for Acumulus has been created and
348
+ * completed and is ready to be sent.
349
+ *
350
+ * This allows to inject custom behavior to alter the invoice just before
351
+ * sending.
352
+ *
353
+ * @param array $invoice
354
+ * The invoice that has been created and completed.
355
+ * @param Source $invoiceSource
356
+ * The source object (order, credit note) for which the invoice was created.
357
+ */
358
+ protected function triggerInvoiceCompleted(array &$invoice, Source $invoiceSource)
359
+ {
360
+ // Default implementation: no event.
361
+ }
362
+
363
+ /**
364
+ * Triggers an event after an invoice for Acumulus has been sent.
365
+ *
366
+ * This allows to inject custom behavior to react to invoice sending.
367
+ *
368
+ * @param array $invoice
369
+ * The invoice that has been sent.
370
+ * @param Source $invoiceSource
371
+ * The source object (order, credit note) for which the invoice was sent.
372
+ * @param array $result
373
+ * The result as sent back by Acumulus. This array contains the following
374
+ * keys:
375
+ * - invoice: array
376
+ * - invoicenumber: string
377
+ * - token: string
378
+ * - entryid: string
379
+ * - errors: array
380
+ * - error: array
381
+ * - code: string
382
+ * - codetag: string
383
+ * - message: string
384
+ * - counterrors: int
385
+ * - warnings: array
386
+ * - warning: array
387
+ * - code: string
388
+ * - codetag: string
389
+ * - message: string
390
+ * - countwarnings: int
391
+ */
392
+ protected function triggerInvoiceSent(array $invoice, Source $invoiceSource, array $result)
393
+ {
394
+ // Default implementation: no event.
395
+ }
396
+
397
+ /**
398
+ * Returns the given DateTime in a format that the actual database layer
399
+ * accepts for comparison in a SELECT query.
400
+ *
401
+ * This default implementation returns the DateTime as a string in ISO format
402
+ * (yyyy-mm-dd hh:mm:ss).
403
+ *
404
+ * @param \DateTime $date
405
+ *
406
+ * @return int|string
407
+ */
408
+ protected function getSqlDate(DateTime $date)
409
+ {
410
+ return $date->format('Y-m-d H:i:s');
411
+ }
412
+
413
+ /**
414
+ * Helper method to retrieve the values from 1 column of a query result.
415
+ *
416
+ * @param array $dbResults
417
+ * @param string $key
418
+ *
419
+ * @return int[]
420
+ */
421
+ protected function getCol(array $dbResults, $key)
422
+ {
423
+ $results = array();
424
+ foreach ($dbResults as $dbResult) {
425
+ $results[] = (int) $dbResult[$key];
426
+ }
427
+ return $results;
428
+ }
429
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Shop/ModuleTranslations.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Shop;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains module related translations, like module name, buttons, requirement
8
+ * messages, and save/cancel/failure messages.
9
+ */
10
+ class ModuleTranslations extends TranslationCollection
11
+ {
12
+ protected $nl = array(
13
+ // Linking into the shop's extension system, standard buttons and messages
14
+ 'extensions' => 'Extensies',
15
+ 'modules' => 'Modules',
16
+ 'page_title' => 'Acumulus instellingen',
17
+ 'module_name' => 'Acumulus',
18
+ 'module_description' => 'Verstuurt uw facturen automatisch naar Acumulus',
19
+ 'text_home' => 'Home',
20
+ 'button_settings' => 'Instellingen',
21
+ 'button_save' => 'Opslaan',
22
+ 'button_back' => 'Terug naar overzicht',
23
+ 'button_confirm_uninstall' => 'Ja, verwijder data en instellingen',
24
+ 'button_cancel_uninstall' => 'Nee, alleen uitschakelen, bewaar data en instellingen',
25
+ 'button_cancel' => 'Annuleren',
26
+ 'button_send' => 'Verzenden',
27
+ 'message_config_saved' => 'De instellingen zijn opgeslagen.',
28
+ 'message_uninstall' => 'Wilt u de configuratie-instellingen verwijderen?',
29
+
30
+ // Requirements
31
+ 'message_error_req_curl' => 'Voor het gebruik van deze module dient de CURL PHP extensie actief te zijn op uw server.',
32
+ 'message_error_req_xml' => 'Voor het gebruik van deze module met het output format XML, dient de SimpleXML PHP extensie actief te zijn op uw server.',
33
+ 'message_error_req_dom' => 'Voor het gebruik van deze module dient de DOM PHP extensie actief te zijn op uw server.',
34
+
35
+ );
36
+
37
+ protected $en = array(
38
+ 'extensions' => 'Extensions',
39
+ 'modules' => 'Modules',
40
+ 'page_title' => 'Acumulus settings',
41
+ 'module_name' => 'Acumulus',
42
+ 'module_description' => 'Automatically sends your invoices to Acumulus',
43
+ 'text_home' => 'Home',
44
+ 'button_settings' => 'Settings',
45
+ 'button_save' => 'Save',
46
+ 'button_back' => 'Back to list',
47
+ 'button_confirm_uninstall' => 'Yes, uninstall data and settings',
48
+ 'button_cancel_uninstall' => 'No, disable only, keep data and settings',
49
+ 'button_cancel' => 'Cancel',
50
+ 'button_send' => 'Send',
51
+ 'message_config_saved' => 'The settings are saved.',
52
+ 'message_uninstall' => 'Are you sure that you want to delete the configuration settings?',
53
+
54
+ // Requirements
55
+ 'message_error_req_curl' => 'The CURL PHP extension needs to be activated on your server for this module to work.',
56
+ 'message_error_req_xml' => 'The SimpleXML extension needs to be activated on your server for this module to be able to work with the XML format.',
57
+ 'message_error_req_dom' => 'The DOM PHP extension needs to be activated on your server for this module to work.',
58
+
59
+ );
60
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Communicator.php ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Web;
3
+
4
+ use DOMDocument;
5
+ use DOMElement;
6
+ use Exception;
7
+ use LibXMLError;
8
+
9
+ /**
10
+ * Communication implements the communication with the Acumulus WebAPI.
11
+ *
12
+ * It offers:
13
+ * - Conversion between array and XML.
14
+ * - Conversion from Json to array.
15
+ * - (https) Communication with the Acumulus webservice using the curl library.
16
+ * - Good error handling during communication.
17
+ */
18
+ class Communicator
19
+ {
20
+
21
+ /** @var \Siel\Acumulus\Web\ConfigInterface */
22
+ protected $config;
23
+
24
+ /** @var array */
25
+ protected $warnings;
26
+
27
+ /** @var array */
28
+ protected $errors;
29
+
30
+ public function __construct(ConfigInterface $config)
31
+ {
32
+ $this->config = $config;
33
+ }
34
+
35
+ /**
36
+ * Checks and, if necessary, corrects the status.
37
+ *
38
+ * If local errors or warnings were added, the status may be incorrectly
39
+ * indicating success. This method checks for this and corrects the status.
40
+ *
41
+ * @param array $response
42
+ * A response structure with, at least, fields 'errors', 'warnings' and
43
+ * 'status'.
44
+ */
45
+ public function checkStatus(array &$response)
46
+ {
47
+ // - Check if status is consistent (local errors and warnings should alter
48
+ // the status as well.
49
+ if (!empty($response['errors'])) {
50
+ if ($response['status'] != ConfigInterface::Status_Exception) {
51
+ $response['status'] = ConfigInterface::Status_Errors;
52
+ }
53
+ } else if (!empty($response['warnings'])) {
54
+ if ($response['status'] != ConfigInterface::Status_Exception && $response['status'] != ConfigInterface::Status_Errors) {
55
+ $response['status'] = ConfigInterface::Status_Warnings;
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Sends a message to the given API function and returns the results.
62
+ *
63
+ * For debugging purposes the return array also includes a key 'trace',
64
+ * containing an array with 2 keys, request and response, with the actual
65
+ * strings as were sent.
66
+ *
67
+ * @param string $apiFunction
68
+ * The API function to invoke.
69
+ * @param array $message
70
+ * The values to submit.
71
+ *
72
+ * @return array
73
+ * An array with the results including any warning and/or error messages.
74
+ */
75
+ public function callApiFunction($apiFunction, array $message)
76
+ {
77
+ // Reset warnings and errors.
78
+ $this->warnings = array();
79
+ $this->errors = array();
80
+
81
+ try {
82
+ // Compose URI.
83
+ $uri = $this->config->getBaseUri() . '/' . $this->config->getApiVersion() . '/' . $apiFunction . '.php';
84
+
85
+ // Complete message with values common to all API calls:
86
+ // - contract part
87
+ // - format part
88
+ // - environment part
89
+ $env = $this->config->getEnvironment();
90
+ $message = array_merge(array(
91
+ 'contract' => $this->config->getCredentials(),
92
+ 'format' => $this->config->getOutputFormat(),
93
+ 'testmode' => $this->config->getDebug() === ConfigInterface::Debug_TestMode ? ConfigInterface::TestMode_Test : ConfigInterface::TestMode_Normal,
94
+ 'connector' => array(
95
+ 'application' => "{$env['shopName']} {$env['shopVersion']}",
96
+ 'webkoppel' => "Acumulus {$env['moduleVersion']}",
97
+ 'development' => 'SIEL - Buro RaDer',
98
+ 'remark' => "Library {$env['libraryVersion']} - PHP {$env['phpVersion']}",
99
+ 'sourceuri' => 'https://www.siel.nl/',
100
+ ),
101
+ ), $message);
102
+
103
+ // Send message, receive response.
104
+ $response = $this->sendApiMessage($uri, $message);
105
+ } catch (Exception $e) {
106
+ $this->errors[] = array(
107
+ 'code' => $e->getCode(),
108
+ 'codetag' => "File: {$e->getFile()}, Line: {$e->getLine()}",
109
+ 'message' => $e->getMessage(),
110
+ );
111
+ $response = array();
112
+ }
113
+
114
+ // Process response.
115
+ // - Simplify errors and warnings parts: remove indirection and count.
116
+ if (!empty($response['errors']['error'])) {
117
+ $response['errors'] = $response['errors']['error'];
118
+ // If there was exactly 1 error, it wasn't put in an array of errors.
119
+ if (!is_array(reset($response['errors']))) {
120
+ $response['errors'] = array($response['errors']);
121
+ }
122
+ } else if (!isset($response['errors'])) {
123
+ $response['errors'] = array();
124
+ } else {
125
+ unset($response['errors']['count_errors']);
126
+ }
127
+
128
+ if (!empty($response['warnings']['warning'])) {
129
+ $response['warnings'] = $response['warnings']['warning'];
130
+ // If there was exactly 1 warning, it wasn't put in an array of warnings.
131
+ if (!is_array(reset($response['warnings']))) {
132
+ $response['warnings'] = array($response['warnings']);
133
+ }
134
+ } else if (!isset($response['warnings'])) {
135
+ $response['warnings'] = array();
136
+ } else {
137
+ unset($response['warnings']['count_warnings']);
138
+ }
139
+
140
+ // - Add local errors and warnings.
141
+ if (!empty($this->errors)) {
142
+ // Internal error(s), return those as well.
143
+ $response['errors'] = array_merge($this->errors, $response['errors']);
144
+ }
145
+ if (!empty($this->warnings)) {
146
+ // Internal warning(s), return those as well.
147
+ $response['warnings'] = array_merge($this->warnings, $response['warnings']);
148
+ }
149
+
150
+ // - Add status if not set. if no status is present the call failed, so we
151
+ // set the status to 1.
152
+ if (!isset($response['status'])) {
153
+ $response['status'] = ConfigInterface::Status_Errors;
154
+ }
155
+
156
+ $this->checkStatus($response);
157
+
158
+ return $response;
159
+ }
160
+
161
+ /**
162
+ * Sends a message to the Acumulus API and returns the answer.
163
+ *
164
+ * Any errors during:
165
+ * - conversion of the message to xml,
166
+ * - communication with the Acumulus WebAPI service
167
+ * - converting the answer to an array
168
+ * are returned as an 'error' in the 'errors' part of the return value.
169
+ *
170
+ *
171
+ * @param string $uri
172
+ * The URI of the Acumulus WebAPI call to send the message to.
173
+ * @param array $message
174
+ * The message to send to the Acumulus WebAPI.
175
+ *
176
+ * @return array
177
+ * The response as specified on
178
+ * https://apidoc.sielsystems.nl/content/warning-error-and-status-response-section-most-api-calls.
179
+ */
180
+ protected function sendApiMessage($uri, array $message)
181
+ {
182
+ $resultBase = array();
183
+
184
+ // Convert message to XML. XML requires 1 top level tag, so add one.
185
+ // The tagname is ignored by the Acumulus WebAPI.
186
+ $message = $this->convertToXml(array('myxml' => $message));
187
+ // Keep track of communication for debugging/logging at higher levels.
188
+ $resultBase['trace']['request'] = preg_replace('|<password>.*</password>|', '<password>REMOVED FOR SECURITY</password>', $message);
189
+
190
+ $response = $this->sendHttpPost($uri, array('xmlstring' => $message));
191
+
192
+ if ($response) {
193
+ $resultBase['trace']['response'] = $response;
194
+ $this->config->getLog()->debug('sendApiMessage(uri="%s", message="%s"), response="%s"',
195
+ $uri, $resultBase['trace']['request'], $resultBase['trace']['response']);
196
+
197
+ $result = false;
198
+ // When the API is gone we might receive an error message in an html page.
199
+ if ($this->isHtmlResponse($response)) {
200
+ $this->setHtmlReceivedError($response);
201
+ } else {
202
+ $alsoTryAsXml = false;
203
+ if ($this->config->getOutputFormat() === 'json') {
204
+ $result = json_decode($response, true);
205
+ if ($result === null) {
206
+ $this->setJsonError();
207
+ // Even if we pass <format>json</format> we might receive an XML
208
+ // response in case the XML was rejected before or during parsing.
209
+ $alsoTryAsXml = true;
210
+ }
211
+ }
212
+ if ($this->config->getOutputFormat() === 'xml' || $alsoTryAsXml) {
213
+ $result = $this->convertToArray($response);
214
+ }
215
+ }
216
+
217
+ if (is_array($result)) {
218
+ $resultBase += $result;
219
+ }
220
+ } else {
221
+ $this->config->getLog()->debug('sendApiMessage(uri="%s", message="%s"): failure',
222
+ $uri, $resultBase['trace']['request']);
223
+ }
224
+
225
+ return $resultBase;
226
+ }
227
+
228
+ /**
229
+ * @param string $uri
230
+ * The uri to send the HTTP request to.
231
+ * @param array|string $post
232
+ * An array of values to be placed in the POST body or an url-encoded string
233
+ * that contains all the POST values
234
+ *
235
+ * @return string|false
236
+ * The response body from the HTTP response or false in case of errors.
237
+ */
238
+ protected function sendHttpPost($uri, $post)
239
+ {
240
+ $response = false;
241
+
242
+ // Open a curl connection.
243
+ $ch = curl_init();
244
+ if (!$ch) {
245
+ $this->setCurlError($ch, 'curl_init()');
246
+ return $response;
247
+ }
248
+
249
+ // Configure the curl connection.
250
+ $options = array(
251
+ CURLOPT_URL => $uri,
252
+ CURLOPT_RETURNTRANSFER => true,
253
+ CURLOPT_SSL_VERIFYPEER => false,
254
+ CURLOPT_POST => true,
255
+ CURLOPT_POSTFIELDS => $post,
256
+ //CURLOPT_PROXY => '127.0.0.1:8888', // Uncomment to debug with Fiddler.
257
+ );
258
+ if (!curl_setopt_array($ch, $options)) {
259
+ $this->setCurlError($ch, 'curl_setopt_array()');
260
+ return $response;
261
+ }
262
+
263
+ // Send and receive over the curl connection.
264
+ $response = curl_exec($ch);
265
+ if (!$response) {
266
+ $this->setCurlError($ch, 'curl_exec()');
267
+ } else {
268
+ // Close the connection (this operation cannot fail).
269
+ curl_close($ch);
270
+ }
271
+
272
+ return $response;
273
+ }
274
+
275
+ /**
276
+ * @param string $response
277
+ *
278
+ * @return bool
279
+ * True if the response is html, false otherwise.
280
+ */
281
+ protected function isHtmlResponse($response)
282
+ {
283
+ return strtolower(substr($response, 0, strlen('<!doctype html'))) === '<!doctype html'
284
+ || strtolower(substr($response, 0, strlen('<html'))) === '<html'
285
+ || strtolower(substr($response, 0, strlen('<body'))) === '<body';
286
+ }
287
+
288
+ /**
289
+ * Converts an XML string to an array.
290
+ *
291
+ * @param string $xml
292
+ * A string containing XML.
293
+ *
294
+ * @return array|false
295
+ * An array representation of the XML string or false on errors.
296
+ */
297
+ protected function convertToArray($xml)
298
+ {
299
+ // Convert the response to an array via a 3-way conversion:
300
+ // - create a simplexml object
301
+ // - convert that to json
302
+ // - convert json to array
303
+ libxml_use_internal_errors(true);
304
+ if (!($result = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA))) {
305
+ $this->setLibxmlErrors(libxml_get_errors());
306
+ return false;
307
+ }
308
+
309
+ if (!($result = json_encode($result))) {
310
+ $this->setJsonError();
311
+ return false;
312
+ }
313
+ if (($result = json_decode($result, true)) === null) {
314
+ $this->setJsonError();
315
+ return false;
316
+ }
317
+
318
+ return $result;
319
+ }
320
+
321
+ /**
322
+ * Converts a keyed, optionally multi-level, array to XML.
323
+ *
324
+ * Each key is converted to a tag, no attributes are used. Numeric sub-arrays
325
+ * are repeated using the same key.
326
+ *
327
+ * @param array $values
328
+ * The array to convert to XML.
329
+ *
330
+ * @return string
331
+ * The XML string
332
+ */
333
+ protected function convertToXml(array $values)
334
+ {
335
+ $dom = new DOMDocument('1.0', 'utf-8');
336
+ $dom->xmlStandalone = true;
337
+ $dom->formatOutput = true;
338
+
339
+ $dom = $this->convertToDom($values, $dom);
340
+ $dom->normalizeDocument();
341
+ $result = $dom->saveXML();
342
+
343
+ return $result;
344
+ }
345
+
346
+ /**
347
+ * Recursively converts a value to a DOMDocument|DOMElement.
348
+ *
349
+ * @param mixed $values
350
+ * A keyed array, an numerically indexed array, or a scalar type.
351
+ * @param DOMDocument|DOMElement $element
352
+ * The element to append the values to.
353
+ *
354
+ * @return DOMDocument|DOMElement
355
+ */
356
+ protected function convertToDom($values, $element)
357
+ {
358
+ /** @var DOMDocument $document */
359
+ static $document = null;
360
+ $isFirstElement = true;
361
+
362
+ if ($element instanceof DOMDocument) {
363
+ $document = $element;
364
+ }
365
+ if (is_array($values)) {
366
+ foreach ($values as $key => $value) {
367
+ if (is_int($key)) {
368
+ if ($isFirstElement) {
369
+ $node = $element;
370
+ $isFirstElement = false;
371
+ } else {
372
+ $node = $document->createElement($element->tagName);
373
+ $element->parentNode->appendChild($node);
374
+ }
375
+ } else {
376
+ $node = $document->createElement($key);
377
+ $element->appendChild($node);
378
+ }
379
+ $this->convertToDom($value, $node);
380
+ }
381
+ } else {
382
+ $element->appendChild($document->createTextNode(is_bool($values) ? ($values ? 'true' : 'false') : $values));
383
+ }
384
+
385
+ return $element;
386
+ }
387
+
388
+ /**
389
+ * Adds a curl error message to the result.
390
+ *
391
+ * @param resource|bool $ch
392
+ * @param string $function
393
+ */
394
+ protected function setCurlError($ch, $function)
395
+ {
396
+ $env = $this->config->getEnvironment();
397
+ $this->errors[] = array(
398
+ 'code' => $ch ? curl_errno($ch) : 0,
399
+ 'codetag' => "$function (Curl: {$env['curlVersion']})",
400
+ 'message' => $ch ? curl_error($ch) : '',
401
+ );
402
+ curl_close($ch);
403
+ }
404
+
405
+ /**
406
+ * Adds a libxml error messages to the result.
407
+ *
408
+ * @param LibXMLError[] $errors
409
+ */
410
+ protected function setLibxmlErrors(array $errors)
411
+ {
412
+ foreach ($errors as $error) {
413
+ $message = array(
414
+ 'code' => $error->code,
415
+ 'codetag' => "Line: {$error->line}, Column: {$error->column}",
416
+ 'message' => trim($error->message),
417
+ );
418
+ if ($error->level === LIBXML_ERR_WARNING) {
419
+ $this->warnings[] = $message;
420
+ } else {
421
+ $this->errors[] = $message;
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Adds a json error message to the result.
428
+ */
429
+ protected function setJsonError()
430
+ {
431
+ $code = json_last_error();
432
+ switch ($code) {
433
+ case JSON_ERROR_NONE:
434
+ $message = 'No error';
435
+ break;
436
+ case JSON_ERROR_DEPTH:
437
+ $message = 'Maximum stack depth exceeded';
438
+ break;
439
+ case JSON_ERROR_STATE_MISMATCH:
440
+ $message = 'Underflow or the modes mismatch';
441
+ break;
442
+ case JSON_ERROR_CTRL_CHAR:
443
+ $message = 'Unexpected control character found';
444
+ break;
445
+ case JSON_ERROR_SYNTAX:
446
+ $message = 'Syntax error, malformed JSON';
447
+ break;
448
+ case JSON_ERROR_UTF8:
449
+ $message = 'Malformed UTF-8 characters, possibly incorrectly encoded';
450
+ break;
451
+ default:
452
+ $message = 'Unknown error';
453
+ break;
454
+ }
455
+ $env = $this->config->getEnvironment();
456
+ $this->errors[] = array(
457
+ 'code' => $code,
458
+ 'codetag' => "(json: {$env['jsonVersion']})",
459
+ 'message' => $message,
460
+ );
461
+ }
462
+
463
+ /**
464
+ * Adds an error message to the result.
465
+ *
466
+ * @param string $response
467
+ * String containing an html document.
468
+ */
469
+ protected function setHtmlReceivedError($response)
470
+ {
471
+ libxml_use_internal_errors(true);
472
+ $doc = new DOMDocument('1.0', 'utf-8');
473
+ $doc->loadHTML($response);
474
+ $body = $doc->getElementsByTagName('body');
475
+ if ($body->length > 0) {
476
+ $body = $body->item(0)->textContent;
477
+ } else {
478
+ $body = '';
479
+ }
480
+ $this->errors[] = array(
481
+ 'code' => 'HTML response received',
482
+ 'codetag' => '',
483
+ 'message' => $body,
484
+ );
485
+ }
486
+
487
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/CommunicatorLocal.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Web;
3
+
4
+ /**
5
+ * CommunicationLocal is a class derived from Communication that can be used for
6
+ * testing purposes. It does not actually send the message to Acumulus and fakes
7
+ * a response.
8
+ */
9
+ class CommunicatorLocal extends Communicator
10
+ {
11
+ /**
12
+ * @inheritdoc
13
+ */
14
+ protected function sendHttpPost($uri, $post)
15
+ {
16
+ if ($this->config->getOutputFormat() === 'json') {
17
+ $response = '{
18
+ "errors": {
19
+ "count_errors": "0"
20
+ },
21
+ "warnings": {
22
+ "warning": [ {
23
+ "code": "599",
24
+ "codetag": "LOCAL",
25
+ "message": "Warning - The message has not been sent. The communication layer operates in local debug mode."
26
+ } ],
27
+ "count_warnings": "1"
28
+ },
29
+ "status": "0"
30
+ }';
31
+ } else {
32
+ /** @noinspection HtmlUnknownTag */
33
+ $response = '<myxml>
34
+ <errors>
35
+ <count_errors>0</count_errors>
36
+ </errors>
37
+ <warnings>
38
+ <warning>
39
+ <code>599</code>
40
+ <codetag>LOCAL</codetag>
41
+ <message>Warning - The message has not been sent. The communication layer operates in local debug mode.</message>
42
+ </warning>
43
+ </warnings>
44
+ </myxml>';
45
+ }
46
+ return str_replace(array("\r", "\n", "\t"), '', $response);
47
+ }
48
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/ConfigInterface.php ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Web;
3
+
4
+ /**
5
+ * CommunicationConfigInterface defines an interface to store and retrieve
6
+ * communication specific configuration values.
7
+ *
8
+ * Configuration is stored in the host environment (normally a web shop), this
9
+ * interface abstracts from how a specific web shop does so.
10
+ */
11
+ interface ConfigInterface
12
+ {
13
+ const libraryVersion = '4.3.0';
14
+
15
+ // Web service configuration related constants.
16
+ const Status_NotSent = -1;
17
+ const Status_Success = 0;
18
+ const Status_Errors = 1;
19
+ const Status_Warnings = 2;
20
+ const Status_Exception = 3;
21
+ const Status_SendingPrevented_InvoiceCreated = 4;
22
+ const Status_SendingPrevented_InvoiceCompleted = 5;
23
+
24
+ const Debug_None = 1;
25
+ const Debug_SendAndLog = 2;
26
+ const Debug_TestMode = 4;
27
+ const Debug_StayLocal = 3;
28
+
29
+ // Web service API constants.
30
+ const TestMode_Normal = 0;
31
+ const TestMode_Test = 1;
32
+
33
+ // Web service related defaults.
34
+ const baseUri = 'https://api.sielsystems.nl/acumulus';
35
+ //const baseUri = 'https://ng1.sielsystems.nl';
36
+ const apiVersion = 'stable';
37
+ const outputFormat = 'json';
38
+
39
+ /**
40
+ * Returns the URI of the Acumulus API to connect with.
41
+ *
42
+ * This method returns the base URI, without version indicator and API call.
43
+ *
44
+ * @return string
45
+ * The URI of the Acumulus API.
46
+ */
47
+ public function getBaseUri();
48
+
49
+ /**
50
+ * Returns the version of the Acumulus API to use.
51
+ *
52
+ * A version number may be part of the URI, so this value implicitly also
53
+ * defines the API version to communicate with.
54
+ *
55
+ * @return string
56
+ * The version of the Acumulus API to use.
57
+ */
58
+ public function getApiVersion();
59
+
60
+ /**
61
+ * Indicates the debug mode of the web services communicator.
62
+ *
63
+ * @return int
64
+ * One of the ConfigInterface::Debug_... constants.
65
+ */
66
+ public function getDebug();
67
+
68
+ /**
69
+ * Returns the current log level for log messages from this module.
70
+ *
71
+ * @return int
72
+ * One of the Log::... constants.
73
+ */
74
+ public function getLogLevel();
75
+
76
+ /**
77
+ * Returns the format the output from the Acumulus API should be in.
78
+ *
79
+ * @return string
80
+ * xml or json.
81
+ */
82
+ public function getOutputFormat();
83
+
84
+ /**
85
+ * Returns the contract credentials to authenticate with the Acumulus API.
86
+ *
87
+ * @return array
88
+ * A keyed array with the keys:
89
+ * - contractcode
90
+ * - username
91
+ * - password
92
+ * - emailonerror
93
+ * - emailonwarning
94
+ */
95
+ public function getCredentials();
96
+
97
+ /**
98
+ * Returns information about the environment of this library.
99
+ *
100
+ * @return array
101
+ * A keyed array with information about the environment of this library:
102
+ * - libraryVersion
103
+ * - moduleVersion
104
+ * - shopName
105
+ * - shopVersion
106
+ * - phpVersion
107
+ * - os
108
+ * - curlVersion
109
+ * - jsonVersion
110
+ */
111
+ public function getEnvironment();
112
+
113
+ /**
114
+ * @return \Siel\Acumulus\Helpers\Log
115
+ */
116
+ public function getLog();
117
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Service.php ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Web;
3
+
4
+ use Siel\Acumulus\Helpers\TranslatorInterface;
5
+
6
+ /**
7
+ * Provides an easy interface towards the different API calls of the Acumulus
8
+ * web API.
9
+ *
10
+ * This class simplifies the communication so that the different web shop
11
+ * specific interfaces can be more rapidly developed.
12
+ *
13
+ * More info:
14
+ * - https://apidoc.sielsystems.nl/
15
+ * - http://www.siel.nl/webkoppelingen/
16
+ *
17
+ * The Web API call wrappers return their information as a keyed array, which is
18
+ * a simplified version of the call specific response structure and the exit
19
+ * structure as described on
20
+ * https://apidoc.sielsystems.nl/content/warning-error-and-status-response-section-most-api-calls.
21
+ *
22
+ * The general part is represented by the following keys in the result:
23
+ * - status: int; 0 = success; 1 = Failed, Errors found; 2 = Success with
24
+ * warnings; 3 = Exception, Please contact Acumulus technical support.
25
+ * - errors: an array of errors, an error is an array with the following keys:
26
+ * - code: int, see https://apidoc.sielsystems.nl/content/exit-and-warning-codes
27
+ * - codetag: string, a special code tag. Use this as a reference when
28
+ * communicating with Acumulus technical support.
29
+ * - message: string, a message describing the warning or error.
30
+ * - warnings: an array of warning arrays, these have the same keys as an error.
31
+ */
32
+ class Service
33
+ {
34
+ /** @var \Siel\Acumulus\Web\ConfigInterface */
35
+ protected $config;
36
+
37
+ /** @var \Siel\Acumulus\Helpers\TranslatorInterface */
38
+ protected $translator;
39
+
40
+ /** @var \Siel\Acumulus\Web\Communicator */
41
+ protected $communicator;
42
+
43
+ /**
44
+ * Constructor.
45
+ *
46
+ * @param \Siel\Acumulus\Web\ConfigInterface $config
47
+ * @param \Siel\Acumulus\Helpers\TranslatorInterface $translator
48
+ */
49
+ public function __construct(ConfigInterface $config, TranslatorInterface $translator)
50
+ {
51
+ $this->config = $config;
52
+ $this->communicator = null;
53
+
54
+ $this->translator = $translator;
55
+ $webServiceTranslations = new Translations();
56
+ $this->translator->add($webServiceTranslations);
57
+ }
58
+
59
+ /**
60
+ * Helper method to translate strings.
61
+ *
62
+ * @param string $key
63
+ * The key to get a translation for.
64
+ *
65
+ * @return string
66
+ * The translation for the given key or the key itself if no translation
67
+ * could be found.
68
+ */
69
+ protected function t($key)
70
+ {
71
+ return $this->translator->get($key);
72
+ }
73
+
74
+ /**
75
+ * Lazy loads the communicator.
76
+ *
77
+ * @return Communicator
78
+ */
79
+ protected function getCommunicator()
80
+ {
81
+ if ($this->communicator === null) {
82
+ if ($this->config->getDebug() == ConfigInterface::Debug_StayLocal) {
83
+ $this->communicator = new CommunicatorLocal($this->config);
84
+ } else {
85
+ $this->communicator = new Communicator($this->config);
86
+ }
87
+ }
88
+ return $this->communicator;
89
+ }
90
+
91
+ /**
92
+ * If the result contains any errors or warnings, a list of verbose messages
93
+ * is returned.
94
+ *
95
+ * @param array $result
96
+ * A keyed array that contains the results, but also any messages in the
97
+ * keys 'errors and 'warnings'.
98
+ * @param bool $addTraceMessages
99
+ * $result may contain the actual request and response as sent in the key
100
+ * 'trace'. If this parameter is true the trace should be added to the
101
+ * messages to be returned as well.
102
+ *
103
+ * @return string[]
104
+ * An array with textual messages that can be used to inform the user.
105
+ */
106
+ public function resultToMessages(array $result, $addTraceMessages = true)
107
+ {
108
+ $messages = array();
109
+ foreach ($result['errors'] as $error) {
110
+ $message = "{$error['code']}: ";
111
+ $message .= $this->t($error['message']);
112
+ if ($error['codetag']) {
113
+ $message .= " ({$error['codetag']})";
114
+ }
115
+ $messages[] = $this->t('message_error') . ' ' . $message;
116
+ }
117
+ foreach ($result['warnings'] as $warning) {
118
+ $message = "{$warning['code']}: ";
119
+ $message .= $this->t($warning['message']);
120
+ if ($warning['codetag']) {
121
+ $message .= " ({$warning['codetag']})";
122
+ }
123
+ $messages[] = $this->t('message_warning') . ' ' . $message;
124
+ }
125
+
126
+ if ($addTraceMessages && (!empty($messages) || $this->config->getDebug() != ConfigInterface::Debug_None)) {
127
+ if (isset($result['trace'])) {
128
+ $messages[] = $this->t('message_info_for_user');
129
+ if (isset($result['trace']['request'])) {
130
+ $messages[] = $this->t('message_sent') . ":\n" . $result['trace']['request'];
131
+ }
132
+ if (isset($result['trace']['response'])) {
133
+ $messages[] = $this->t('message_received') . ":\n" . $result['trace']['response'];
134
+ }
135
+ }
136
+ }
137
+
138
+ return $messages;
139
+ }
140
+
141
+ /**
142
+ * Converts an array of messages to a string that can be used in a text mail.
143
+ *
144
+ * @param string[] $messages
145
+ *
146
+ * @return string
147
+ */
148
+ public function messagesToText(array $messages)
149
+ {
150
+ return '* ' . implode("\n\n* ", $messages) . "\n\n";
151
+ }
152
+
153
+ /**
154
+ * Converts an array of messages to a string that can be used in an html mail.
155
+ *
156
+ * @param string[] $messages
157
+ *
158
+ * @return string
159
+ */
160
+ public function messagesToHtml(array $messages)
161
+ {
162
+ $messages_html = array();
163
+ foreach ($messages as $message) {
164
+ $messages_html[] = nl2br(htmlspecialchars($message, ENT_NOQUOTES));
165
+ }
166
+ return '<ul><li>' . implode("</li><li>", $messages_html) . '</li></ul>';
167
+ }
168
+
169
+ /**
170
+ * @param int $status
171
+ *
172
+ * @return string
173
+ */
174
+ public function getStatusText($status)
175
+ {
176
+ switch ($status) {
177
+ case ConfigInterface::Status_Success:
178
+ return $this->t('message_response_0');
179
+ case ConfigInterface::Status_Errors:
180
+ return $this->t('message_response_1');
181
+ case ConfigInterface::Status_Warnings:
182
+ return $this->t('message_response_2');
183
+ case ConfigInterface::Status_Exception:
184
+ return $this->t('message_response_3');
185
+ default:
186
+ return $this->t('message_response_x') . $status;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Retrieves a list of accounts.
192
+ *
193
+ * @return array
194
+ * Besides the general response structure, the actual result of this call is
195
+ * returned under the key 'accounts' and consists of an array of 'accounts',
196
+ * each 'account' being a keyed array with keys:
197
+ * - accountid
198
+ * - accountnumber
199
+ * - accountdescription
200
+ *
201
+ * See https://apidoc.sielsystems.nl/content/picklist-accounts-bankrekeningen.
202
+ */
203
+ public function getPicklistAccounts()
204
+ {
205
+ return $this->getPicklist('account');
206
+ }
207
+
208
+ /**
209
+ * Retrieves a list of contact types.
210
+ *
211
+ * @return array
212
+ * Besides the general response structure, the actual result of this call is
213
+ * returned under the key 'contacttypes' and consists of an array of
214
+ * 'contacttypes', each 'contacttype' being a keyed array with keys:
215
+ * - contacttypeid
216
+ * - contacttypename
217
+ *
218
+ * See https://apidoc.sielsystems.nl/content/picklist-contacttypes-contactsoorten.
219
+ */
220
+ public function getPicklistContactTypes()
221
+ {
222
+ return $this->getPicklist('contacttype');
223
+ }
224
+
225
+ /**
226
+ * Retrieves a list of cost centers.
227
+ *
228
+ * @return array
229
+ * Besides the general response structure, the actual result of this call is
230
+ * returned under the key 'costcenters' and consists of an array of
231
+ * 'costcenters', each 'costcenter' being a keyed array with keys:
232
+ * - costcenterid
233
+ * - costcentername
234
+ *
235
+ * See https://apidoc.sielsystems.nl/content/picklist-costcenters-kostenplaatsen.
236
+ */
237
+ public function getPicklistCostCenters()
238
+ {
239
+ return $this->getPicklist('costcenter');
240
+ }
241
+
242
+ /**
243
+ * Retrieves a list of cost headings.
244
+ *
245
+ * @return array
246
+ * Besides the general response structure, the actual result of this call is
247
+ * returned under the key 'costheadings' and consists of an array of
248
+ * 'costheadings', each 'costheading' being a keyed array with keys:
249
+ * - costheadingid
250
+ * - costheadingname
251
+ *
252
+ * See https://apidoc.sielsystems.nl/content/picklist-costheadings-kostensoorten.
253
+ */
254
+ public function getPicklistCostHeadings()
255
+ {
256
+ return $this->getPicklist('costheading');
257
+ }
258
+
259
+ /**
260
+ * Retrieves a list of cost types.
261
+ *
262
+ * @return array
263
+ * Besides the general response structure, the actual result of this call is
264
+ * returned under the key 'invoicetemplates' and consists of an array of
265
+ * 'invoicetemplates', each 'invoicetemplate' being a keyed array with keys:
266
+ * - invoicetemplateid
267
+ * - invoicetemplatename
268
+ *
269
+ * See https://apidoc.sielsystems.nl/content/picklist-invoice-templates-factuursjablonen.
270
+ */
271
+ public function getPicklistInvoiceTemplates()
272
+ {
273
+ return $this->getPicklist('invoicetemplate');
274
+ }
275
+
276
+ /**
277
+ * Retrieves a list of VAT types.
278
+ *
279
+ * @return array
280
+ * Besides the general response structure, the actual result of this call is
281
+ * returned under the key 'vattypes' and consists of an array of 'vattypes',
282
+ * each 'vattype' being a keyed array with keys:
283
+ * - 'vattypeid'
284
+ * - 'vattypename'
285
+ *
286
+ * See https://apidoc.sielsystems.nl/content/picklist-vattypes-btw-groepen.
287
+ */
288
+ public function getPicklistVatTypes()
289
+ {
290
+ return $this->getPicklist('vattype');
291
+ }
292
+
293
+ /**
294
+ * A helper method to retrieve a given picklist.
295
+ *
296
+ * The Acumulus API for picklists is so well standardized, that it is possible
297
+ * to use 1 general picklist retrieval function that can process all picklist
298
+ * types.
299
+ *
300
+ * @param string $picklist
301
+ * The picklist to retrieve, specify in singular form: account, contacttype,
302
+ * costcenter, etc.
303
+ *
304
+ * @return array
305
+ * Besides the general response structure, the actual result of this call is
306
+ * returned under the key $picklist in plural format (with an 's' attached)
307
+ * and consists of an array of keyed arrays, each keyed array being 1 result
308
+ * of the requested picklist.
309
+ */
310
+ protected function getPicklist($picklist)
311
+ {
312
+ $plural = $picklist . 's';
313
+ $response = $this->getCommunicator()->callApiFunction("picklists/picklist_$plural", array());
314
+ // Simplify result: remove indirection.
315
+ if (!empty($response[$plural][$picklist])) {
316
+ $response[$plural] = $response[$plural][$picklist];
317
+ // If there was only 1 result, it wasn't put in an array.
318
+ if (!is_array(reset($response[$plural]))) {
319
+ $response[$plural] = array($response[$plural]);
320
+ }
321
+ } else {
322
+ $response[$plural] = array();
323
+ }
324
+ return $response;
325
+ }
326
+
327
+ /**
328
+ * Retrieves a list of VAT rates for the given country at the given date.
329
+ *
330
+ * @param string $countryCode
331
+ * Country code of the country to retrieve the VAT info for.
332
+ * @param string $date
333
+ * ISO date string (yyyy-mm-dd) for the date to retrieve the VAT info for.
334
+ *
335
+ * @return array
336
+ * Besides the general response structure, the actual result of this call is
337
+ * returned under the key 'vatinfo' and consists of an array of "vatinfo's",
338
+ * each 'vatinfo' being a keyed array with keys:
339
+ * - vattype
340
+ * - vatrate
341
+ *
342
+ * See https://apidoc.sielsystems.nl/content/lookup-vatinfo-btw-informatie.
343
+ */
344
+ public function getVatInfo($countryCode, $date = '')
345
+ {
346
+ if (empty($date)) {
347
+ $date = date('Y-m-d');
348
+ }
349
+ $message = array(
350
+ 'vatdate' => $date,
351
+ 'vatcountry' => $countryCode,
352
+ );
353
+ $response = $this->getCommunicator()->callApiFunction("lookups/lookup_vatinfo", $message);
354
+ // Simplify result: remove indirection.
355
+ if (!empty($response['vatinfo']['vat'])) {
356
+ $response['vatinfo'] = $response['vatinfo']['vat'];
357
+ // If there was only 1 result, it wasn't put in an array.
358
+ if (!is_array(reset($response['vatinfo']))) {
359
+ $response['vatinfo'] = array($response['vatinfo']);
360
+ }
361
+ } else {
362
+ $response['vatinfo'] = array();
363
+ }
364
+ return $response;
365
+ }
366
+
367
+ /**
368
+ * Sends an invoice to Acumulus.
369
+ *
370
+ * @param array $invoice
371
+ * The invoice to send.
372
+ *
373
+ * @return array
374
+ * Besides the general response structure, the actual result of this call is
375
+ * returned under the following key:
376
+ * - invoice: an array of information about the created invoice, being an
377
+ * array with keys:
378
+ * - invoicenumber
379
+ * - token
380
+ * - entryid
381
+ * If the key invoice is present, it indicates success.
382
+ *
383
+ * See https://apidoc.sielsystems.nl/content/invoice-add.
384
+ * See https://apidoc.sielsystems.nl/content/warning-error-and-status-response-section-most-api-calls
385
+ * for more information on the contents of the returned array.
386
+ */
387
+ public function invoiceAdd(array $invoice)
388
+ {
389
+ return $this->getCommunicator()->callApiFunction("invoices/invoice_add", $invoice);
390
+ }
391
+
392
+ /**
393
+ * Merges any local messages into the result structure and adapts the status
394
+ * to correctly reflect local warnings and errors as well.
395
+ *
396
+ * @param array $result
397
+ * @param array $localMessages
398
+ *
399
+ * @return array
400
+ */
401
+ public function mergeLocalMessages(array $result, array $localMessages)
402
+ {
403
+ if (!empty($localMessages['errors'])) {
404
+ $result['errors'] = array_merge($result['errors'], $localMessages['errors']);
405
+ }
406
+ if (!empty($localMessages['warnings'])) {
407
+ $result['warnings'] = array_merge($result['warnings'], $localMessages['warnings']);
408
+ }
409
+ $this->getCommunicator()->checkStatus($result);
410
+ return $result;
411
+ }
412
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/Web/Translations.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\Web;
3
+
4
+ use Siel\Acumulus\Helpers\TranslationCollection;
5
+
6
+ /**
7
+ * Contains translations for classes in the \Siel\Acumulus\Web namespace.
8
+ */
9
+ class Translations extends TranslationCollection
10
+ {
11
+ protected $nl = array(
12
+ 'message_error' => 'Fout',
13
+ 'message_warning' => 'Waarschuwing',
14
+ 'message_info_for_user' => 'De informatie hieronder wordt alleen getoond om eventuele support te vergemakkelijken. U kunt deze informatie negeren.',
15
+ 'message_sent' => 'Verzonden bericht',
16
+ 'message_received' => 'Ontvangen bericht',
17
+ 'message_response_0' => 'Succes. Zonder waarschuwingen',
18
+ 'message_response_1' => 'Mislukt. Fouten gevonden',
19
+ 'message_response_2' => 'Succes. Met waarschuwingen',
20
+ 'message_response_3' => 'Fout. Neem contact op met Acumulus',
21
+ 'message_response_x' => 'Onbekende status code',
22
+ );
23
+
24
+ protected $en = array(
25
+ 'message_error' => 'Error',
26
+ 'message_warning' => 'Warning',
27
+ 'message_info_for_user' => 'The information below is only shown to facilitate support. You may ignore these messages.',
28
+ 'message_sent' => 'Message sent',
29
+ 'message_received' => 'Message received',
30
+ 'message_response_0' => 'Success. Without warnings',
31
+ 'message_response_1' => 'Failed. Errors found',
32
+ 'message_response_2' => 'Success. With any warnings',
33
+ 'message_response_3' => 'Exception. Please contact Acumulus technical support',
34
+ 'message_response_x' => 'Unknown status code',
35
+ );
36
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/FormMapper.php ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Form;
5
+
6
+ /**
7
+ * FormMapper maps an Acumulus form definition to a WooCommerce form definition.
8
+ */
9
+ class FormMapper
10
+ {
11
+ const required = '<span class="required">*</span>';
12
+
13
+ /** @var string */
14
+ protected $page;
15
+
16
+ /** @var FormRenderer */
17
+ protected $formRenderer;
18
+
19
+ /**
20
+ * Maps a set of field definitions.
21
+ *
22
+ * @param Form $form
23
+ * @param string $page
24
+ *
25
+ * @return \Siel\Acumulus\WooCommerce\Helpers\FormRenderer
26
+ */
27
+ public function map(Form $form, $page)
28
+ {
29
+ $this->formRenderer = new FormRenderer();
30
+ $this->page = $page;
31
+ $form->addValues();
32
+ $this->fields($form->getFields(), '');
33
+ return $this->formRenderer;
34
+ }
35
+
36
+ /**
37
+ * Maps a set of field definitions.
38
+ *
39
+ * @param array[] $fields
40
+ * @param string $section
41
+ * The page section to add the fields to.
42
+ */
43
+ protected function fields(array $fields, $section)
44
+ {
45
+ foreach ($fields as $id => $field) {
46
+ $field['id'] = $id;
47
+ if (!isset($field['name'])) {
48
+ $field['name'] = $id;
49
+ }
50
+ if (!isset($field['attributes'])) {
51
+ $field['attributes'] = array();
52
+ }
53
+ if (!isset($field['label'])) {
54
+ $field['label'] = '';
55
+ }
56
+ $this->field($field, $section);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Maps a single field definition.
62
+ *
63
+ * @param array $field
64
+ * Field(set) definition.
65
+ * @param string $section
66
+ * The section this item (if it is a field) should be added to.
67
+ */
68
+ protected function field(array $field, $section)
69
+ {
70
+ if ($field['type'] === 'fieldset') {
71
+ $renderer = $this->formRenderer;
72
+ add_settings_section($field['id'], $field['legend'], function () use ($renderer, $field) {
73
+ $renderer->field($field);
74
+ }, $this->page);
75
+ $this->fields($field['fields'], $field['id']);
76
+ } else {
77
+ $required = !empty($field['attributes']['required']) ? static::required : '';
78
+ add_settings_field($field['id'], $field['label'] . $required, array($this->formRenderer, 'field'), $this->page, $section, $field);
79
+ }
80
+ }
81
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/FormRenderer.php ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Form;
5
+ use Siel\Acumulus\Helpers\FormRenderer as BaseFormRenderer;
6
+
7
+ /**
8
+ * Class FormRenderer renders a form in the WordPress settings pages standard.
9
+ */
10
+ class FormRenderer extends BaseFormRenderer
11
+ {
12
+ /**
13
+ * Constructor.
14
+ */
15
+ public function __construct()
16
+ {
17
+ $this->radioWrapperTag = 'ul';
18
+ $this->radioWrapperClass = '';
19
+ $this->radio1WrapperTag = 'li';
20
+ $this->radio1WrapperClass = '';
21
+ $this->checkboxWrapperTag = 'ul';
22
+ $this->checkboxWrapperClass = '';
23
+ $this->checkbox1WrapperTag = 'li';
24
+ $this->checkbox1WrapperClass = '';
25
+ }
26
+
27
+ /**
28
+ * @inheritdoc
29
+ *
30
+ * This override does only set the form, as we need it later on to retrieve
31
+ * additional information about field sets.
32
+ */
33
+ public function render(Form $form)
34
+ {
35
+ $this->form = $form;
36
+ return '';
37
+ }
38
+
39
+ /**
40
+ * @inheritdoc
41
+ *
42
+ * This override makes access public and echo's the output besides returning
43
+ * it as WordPress is in field by field outputting mode when this method gets
44
+ * called.
45
+ */
46
+ public function field(array $field)
47
+ {
48
+ $output = parent::field($field);
49
+ echo $output;
50
+ return $output;
51
+ }
52
+
53
+ /**
54
+ * @inheritdoc
55
+ *
56
+ * This override only renders the description as WordPress already renders the
57
+ * fieldset title and the fields.
58
+ */
59
+ protected function renderFieldset(array $field)
60
+ {
61
+ $output = '';
62
+ if (!empty($field['description'])) {
63
+ $output .= $this->renderDescription($field['description']);
64
+ }
65
+ return $output;
66
+ }
67
+
68
+ /**
69
+ * {@inheritdoc}
70
+ *
71
+ * This override:
72
+ * - Skips the rendering of the wrapper and label as WordPress does that
73
+ * itself.
74
+ * - Echo's the output as WordPress is in outputting mode here.
75
+ */
76
+ protected function renderField(array $field)
77
+ {
78
+ $type = $field['type'];
79
+ $id = $field['id'];
80
+ $name = $field['name'];
81
+ $value = isset($field['value']) ? $field['value'] : '';
82
+ $attributes = $field['attributes'];
83
+ $description = isset($field['description']) ? $field['description'] : '';
84
+ $options = isset($field['options']) ? $field['options'] : array();
85
+
86
+ $output = '';
87
+
88
+ $output .= $this->renderElement($type, $id, $name, $value, $attributes, $options);
89
+ if ($type !== 'hidden') {
90
+ $output .= $this->renderDescription($description);
91
+ }
92
+ return $output;
93
+ }
94
+
95
+ /**
96
+ * Recursively searches a field by its id.
97
+ *
98
+ * @param array[] $fields
99
+ * @param string $id
100
+ *
101
+ * @return array|null
102
+ */
103
+ protected function getFieldById($fields, $id)
104
+ {
105
+ if (array_key_exists($id, $fields)) {
106
+ return $fields[$id];
107
+ }
108
+ foreach ($fields as $id => $field) {
109
+ if (isset($field['fields'])) {
110
+ $result = $this->getFieldById($field['fields'], $id);
111
+ if ($result !== null) {
112
+ return $result;
113
+ }
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/Log.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Log as BaseLog;
5
+ use WC_Logger;
6
+
7
+ /**
8
+ * Extends the base log class to log any library logging to the WP log.
9
+ */
10
+ class Log extends BaseLog
11
+ {
12
+ /**
13
+ * {@inheritdoc}
14
+ *
15
+ * This override checks for WP_DEBUG first.
16
+ */
17
+ protected function write($message, $severity)
18
+ {
19
+ $logger = new WC_Logger();
20
+ $message = sprintf('%s - %s', $this->getSeverityString($severity), $message);
21
+ $logger->add('acumulus', $message);
22
+ }
23
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Helpers/Mailer.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Helpers;
3
+
4
+ use Siel\Acumulus\Helpers\Mailer as BaseMailer;
5
+
6
+ /**
7
+ * Extends the base mailer class to send a mail using the WP mail features.
8
+ */
9
+ class Mailer extends BaseMailer
10
+ {
11
+ /**
12
+ * {@inheritdoc}
13
+ */
14
+ public function sendInvoiceAddMailResult(array $result, array $messages, $invoiceSourceType, $invoiceSourceReference)
15
+ {
16
+ $to = $this->getToAddress();
17
+
18
+ $subject = $this->getSubject($result);
19
+
20
+ $fromEmail = get_bloginfo('admin_email');
21
+ $fromName = get_bloginfo('name');
22
+ $headers = array(
23
+ "from: $fromName <$fromEmail>",
24
+ 'Content-Type: text/html; charset=UTF-8',
25
+ );
26
+
27
+ $body = $this->getBody($result, $messages, $invoiceSourceType, $invoiceSourceReference);
28
+ $html = $body['html'];
29
+
30
+ return wp_mail($to, $subject, $html, $headers);
31
+ }
32
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Invoice/Creator.php ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Invoice;
3
+
4
+ use Siel\Acumulus\Helpers\Number;
5
+ use Siel\Acumulus\Invoice\ConfigInterface;
6
+ use Siel\Acumulus\Invoice\Creator as BaseCreator;
7
+ use WC_Abstract_Order;
8
+ use WC_Coupon;
9
+ use WC_Order;
10
+ use WC_Product;
11
+
12
+ /**
13
+ * Allows to create arrays in the Acumulus invoice structure from a WordPress
14
+ * order or order refund.
15
+ */
16
+ class Creator extends BaseCreator
17
+ {
18
+ // More specifically typed property.
19
+ /** @var Source */
20
+ protected $invoiceSource;
21
+
22
+ /** @var WC_Abstract_Order The order or refund that is sent to Acumulus. */
23
+ protected $shopSource;
24
+
25
+ /** @var WC_Order The order self or the order that got refunded. */
26
+ protected $order;
27
+
28
+ /** @var bool Whether the order has (non empty) item lines. */
29
+ protected $hasItemLines;
30
+
31
+ /**
32
+ * {@inheritdoc}
33
+ *
34
+ * This override also initializes WooCommerce specific properties related to
35
+ * the source.
36
+ */
37
+ protected function setInvoiceSource($invoiceSource)
38
+ {
39
+ parent::setInvoiceSource($invoiceSource);
40
+ switch ($this->invoiceSource->getType()) {
41
+ case Source::Order:
42
+ $this->shopSource = $this->invoiceSource->getSource();
43
+ $this->order = $this->shopSource;
44
+ break;
45
+ case Source::CreditNote:
46
+ $this->shopSource = $this->invoiceSource->getSource();
47
+ $this->order = new WC_Order($this->shopSource->post->post_parent);
48
+ break;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * {@inheritdoc}
54
+ */
55
+ protected function getCustomer()
56
+ {
57
+ $result = array();
58
+
59
+ $order = $this->order;
60
+
61
+ $this->addIfNotEmpty($result, 'contactyourid', $order->customer_user);
62
+ $this->addEmpty($result, 'companyname1', $order->billing_company);
63
+ $result['fullname'] = $order->billing_first_name . ' ' . $order->billing_last_name;
64
+ $this->addEmpty($result, 'address1', $order->billing_address_1);
65
+ $this->addEmpty($result, 'address2', $order->billing_address_2);
66
+ $this->addEmpty($result, 'postalcode', $order->billing_postcode);
67
+ $this->addEmpty($result, 'city', $order->billing_city);
68
+ if (isset($order->billing_country)) {
69
+ $result['countrycode'] = $order->billing_country;
70
+ }
71
+ // The EU VAT Number plugin allows customers to indicate their VAT number as
72
+ // to apply for the reversed VAT scheme. The vat number is stored under the
73
+ // '_vat_number' meta key, though older versions did so under the
74
+ // 'VAT Number' key.
75
+ // See http://docs.woothemes.com/document/eu-vat-number-2/
76
+ $this->addIfNotEmpty($result, 'vatnumber', get_post_meta($order->id, 'VAT Number', true));
77
+ $this->addIfNotEmpty($result, 'vatnumber', get_post_meta($order->id, 'vat_number', true));
78
+ $this->addIfNotEmpty($result, 'telephone', $order->billing_phone);
79
+ $result['email'] = $order->billing_email;
80
+
81
+ return $result;
82
+ }
83
+
84
+ /**
85
+ * {@inheritdoc}
86
+ *
87
+ * For refunds, this override also searches in the order that gets refunded.
88
+ */
89
+ protected function searchProperty($property)
90
+ {
91
+ $value = parent::searchProperty($property);
92
+ if (empty($value) && $this->invoiceSource->getType() === Source::CreditNote) {
93
+ // Also try the order that gets refunded.
94
+ $value = $this->getProperty($property, $this->order);
95
+ }
96
+ return $value;
97
+ }
98
+
99
+ /**
100
+ * {@inheritdoc}
101
+ */
102
+ protected function getInvoiceNumber($invoiceNumberSource)
103
+ {
104
+ return $this->invoiceSource->getReference();
105
+ }
106
+
107
+ /**
108
+ * Returns the date to use as invoice date for the order.
109
+ *
110
+ * param int $dateToUse
111
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_InvoiceCreate or
112
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_OrderCreate
113
+ *
114
+ * @return string
115
+ * Date to send to Acumulus as the invoice date: yyyy-mm-dd.
116
+ */
117
+ protected function getInvoiceDateOrder(/*$dateToUse*/)
118
+ {
119
+ // createdAt returns yyyy-mm-dd hh:mm:ss, take date part.
120
+ return substr($this->shopSource->order_date, 0, strlen('2000-01-01'));
121
+ }
122
+
123
+ /**
124
+ * Returns the date to use as invoice date for the order refund.
125
+ *
126
+ * param int $dateToUse
127
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_InvoiceCreate or
128
+ * \Siel\Acumulus\Invoice\ConfigInterface\InvoiceDate_OrderCreate
129
+ *
130
+ * @return string
131
+ * Date to send to Acumulus as the invoice date: yyyy-mm-dd.
132
+ */
133
+ protected function getInvoiceDateCreditNote(/*$dateToUse*/)
134
+ {
135
+ return substr($this->shopSource->post->post_date, 0, strlen('2000-01-01'));
136
+ }
137
+
138
+ /**
139
+ * Returns whether the order has been paid or not.
140
+ *
141
+ * @return int
142
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Paid or
143
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Due
144
+ */
145
+ protected function getPaymentStateOrder()
146
+ {
147
+ return $this->shopSource->needs_payment() ? ConfigInterface::PaymentStatus_Due : ConfigInterface::PaymentStatus_Paid;
148
+ }
149
+
150
+ /**
151
+ * Returns whether the order refund has been paid or not.
152
+ *
153
+ * For now we assume that a refund is paid back on creation.
154
+ *
155
+ * @return int
156
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Paid or
157
+ * \Siel\Acumulus\Invoice\ConfigInterface::PaymentStatus_Due
158
+ */
159
+ protected function getPaymentStateCreditNote()
160
+ {
161
+ return ConfigInterface::PaymentStatus_Paid;
162
+ }
163
+
164
+ /**
165
+ * Returns the payment date of the order.
166
+ *
167
+ * @return string
168
+ * The payment date of the order (yyyy-mm-dd).
169
+ */
170
+ protected function getPaymentDateOrder()
171
+ {
172
+ return substr($this->shopSource->paid_date, 0, strlen('2000-01-01'));
173
+ }
174
+
175
+ /**
176
+ * Returns the payment date of the order refund.
177
+ *
178
+ * We take the last modified date as pay date.
179
+ *
180
+ * @return string
181
+ * The payment date of the order refund (yyyy-mm-dd).
182
+ */
183
+ protected function getPaymentDateCreditNote()
184
+ {
185
+ return substr($this->shopSource->post->post_modified, 0, strlen('2000-01-01'));
186
+ }
187
+
188
+ /**
189
+ * {@inheritdoc}
190
+ *
191
+ * This override provides the values meta-invoice-amountinc and
192
+ * meta-invoice-vatamount.
193
+ */
194
+ protected function getInvoiceTotals()
195
+ {
196
+ return array(
197
+ 'meta-invoice-amountinc' => $this->shopSource->get_total(),
198
+ 'meta-invoice-vatamount' => $this->shopSource->get_total_tax(),
199
+ );
200
+ }
201
+
202
+ /**
203
+ * {@inheritdoc}
204
+ */
205
+ protected function getItemLines()
206
+ {
207
+ $result = array();
208
+ $lines = $this->shopSource->get_items(apply_filters('woocommerce_admin_order_item_types', 'line_item'));
209
+ foreach ($lines as $order_item_id => $line) {
210
+ $line['order_item_id'] = $order_item_id;
211
+ $itemLine = $this->getItemLine($line);
212
+ if ($itemLine) {
213
+ $result[] = $itemLine;
214
+ }
215
+ }
216
+
217
+ $this->hasItemLines = count($result) > 0;
218
+ return $result;
219
+ }
220
+
221
+ /**
222
+ * Returns 1 item line.
223
+ *
224
+ * @param array $item
225
+ *
226
+ * @return array
227
+ * May be empty if the line should not be sent (e.g. qty = 0 on a refund).
228
+ */
229
+ protected function getItemLine(array $item)
230
+ {
231
+ $result = array();
232
+
233
+ // Qty = 0 can happen on refunds: products that are not returned are still
234
+ // listed but have qty = 0.
235
+ if (Number::isZero($item['qty'])) {
236
+ return $result;
237
+ }
238
+
239
+ // get_item_total() returns cost per item after discount and ex vat (2nd
240
+ // param).
241
+ $productPriceEx = $this->shopSource->get_item_total($item, false, false);
242
+ $productPriceInc = $this->shopSource->get_item_total($item, true, false);
243
+ // get_item_tax returns tax per item after discount.
244
+ $productVat = $this->shopSource->get_item_tax($item, false);
245
+
246
+ $result['product'] = $item['name'];
247
+ $isVariation = !empty($item['variation_id']);
248
+ $product = $this->shopSource->get_product_from_item($item);
249
+ // $product can be NULL if the product has been deleted.
250
+ if ($product instanceof WC_Product) {
251
+ $this->addIfNotEmpty($result, 'itemnumber', $product->get_sku());
252
+ if ($isVariation) {
253
+ $variation = $this->getVariantDescription($item, $product);
254
+ $result['product'] .= " ($variation)";
255
+ }
256
+ }
257
+
258
+ // WooCommerce does not support the margin scheme. So in a standard install
259
+ // this method will always return false. But if this method happens to
260
+ // return true anyway (customisation, hook), the costprice tag will trigger
261
+ // vattype = 5 for Acumulus.
262
+ if ($this->allowMarginScheme() && !empty($item['cost_price'])) {
263
+ // Margin scheme:
264
+ // - Do not put VAT on invoice: send price incl VAT as unitprice.
265
+ // - But still send the VAT rate to Acumulus.
266
+ // Costprice > 0 is the trigger for Acumulus to use the margin scheme.
267
+ $result += array(
268
+ 'unitprice' => $productPriceInc,
269
+ 'costprice' => $item['cost_price'],
270
+ );
271
+ } else {
272
+ $result += array(
273
+ 'unitprice' => $productPriceEx,
274
+ 'unitpriceinc' => $productPriceInc,
275
+ 'vatamount' => $productVat,
276
+ );
277
+ }
278
+
279
+ $result['quantity'] = $item['qty'];
280
+ // Precision: one of the prices is entered by the administrator and thus can
281
+ // be considered exact. The computed one is not rounded, so we can assume a
282
+ // very high precision for all values here.
283
+ $result += $this->getVatRangeTags($productVat, $productPriceEx, 0.001, 0.001);
284
+
285
+ return $result;
286
+ }
287
+
288
+ /**
289
+ * Returns a description for this variant.
290
+ *
291
+ * @param array $item
292
+ * @param \WC_Product $product
293
+ *
294
+ * @return string
295
+ * The description for this variant, a string in the form:
296
+ * (variant: value, variant: value, ...)
297
+ */
298
+ protected function getVariantDescription(array $item, \WC_Product $product)
299
+ {
300
+ $result = array();
301
+
302
+ if ($metadata = $this->shopSource->has_meta($item['order_item_id'])) {
303
+ // Define hidden core fields.
304
+ $hiddenOrderItemMeta = apply_filters('woocommerce_hidden_order_itemmeta', array(
305
+ '_qty',
306
+ '_tax_class',
307
+ '_product_id',
308
+ '_variation_id',
309
+ '_line_subtotal',
310
+ '_line_subtotal_tax',
311
+ '_line_total',
312
+ '_line_tax',
313
+ ));
314
+ foreach ($metadata as $meta) {
315
+ // Skip hidden core fields and serialized data (also hidden core fields).
316
+ if (in_array($meta['meta_key'], $hiddenOrderItemMeta) || is_serialized($meta['meta_value'])) {
317
+ continue;
318
+ }
319
+
320
+ // Get attribute data.
321
+ if (taxonomy_exists(wc_sanitize_taxonomy_name($meta['meta_key']))) {
322
+ $term = get_term_by('slug', $meta['meta_value'], wc_sanitize_taxonomy_name($meta['meta_key']));
323
+ $meta['meta_key'] = wc_attribute_label(wc_sanitize_taxonomy_name($meta['meta_key']));
324
+ $meta['meta_value'] = isset($term->name) ? $term->name : $meta['meta_value'];
325
+ } else {
326
+ $meta['meta_key'] = apply_filters('woocommerce_attribute_label', wc_attribute_label($meta['meta_key'], $product), $meta['meta_key']);
327
+ }
328
+
329
+ $result[] = $meta['meta_key'] . ': ' . rawurldecode($meta['meta_value']);
330
+ }
331
+ }
332
+
333
+ return implode(', ', $result);
334
+ }
335
+
336
+ /**
337
+ * @inheritdoc
338
+ *
339
+ * WooCommerce has general fee lines, so we have to override this method to
340
+ * add these general fees (type unknown to us)
341
+ */
342
+ protected function getFeeLines()
343
+ {
344
+ $result = parent::getFeeLines();
345
+
346
+ // So far, all amounts found on refunds are negative, so we probably don't
347
+ // need to correct the sign on these lines either: but this has not been
348
+ // tested yet!.
349
+ foreach ($this->shopSource->get_fees() as $feeLine) {
350
+ $line = $this->getFeeLine($feeLine);
351
+ $line['meta-line-type'] = static::LineType_Other;
352
+ $result[] = $line;
353
+ }
354
+ return $result;
355
+ }
356
+
357
+ /**
358
+ * @param array $line
359
+ *
360
+ * @return array
361
+ */
362
+ protected function getFeeLine(array $line)
363
+ {
364
+ $feeEx = $line['line_total'];
365
+ $feeVat = $line['line_tax'];
366
+
367
+ $result = array(
368
+ 'product' => $this->t($line['name']),
369
+ 'unitprice' => $feeEx,
370
+ 'quantity' => 1,
371
+ 'vatamount' => $feeVat,
372
+ ) + $this->getVatRangeTags($feeVat, $feeEx);
373
+
374
+ return $result;
375
+ }
376
+
377
+ /**
378
+ * {@inheritdoc}
379
+ */
380
+ protected function getShippingLine()
381
+ {
382
+ // Precision: shipping costs are entered ex VAT, so that may be rounded to
383
+ // the cent by the administrator. The computed costs inc VAT is rounded to
384
+ // the cent as well, so both are to be considered precise to the cent.
385
+ $shippingEx = $this->shopSource->get_total_shipping();
386
+ $shippingVat = $this->shopSource->get_shipping_tax();
387
+ if (!Number::isZero($shippingEx)) {
388
+ $description = $this->t('shipping_costs');
389
+ } else {
390
+ // We do not need to indicate that free shipping is not refunded on an
391
+ // order refund.
392
+ if ($this->invoiceSource->getType() == Source::CreditNote) {
393
+ return array();
394
+ }
395
+ $description = $this->t('free_shipping');
396
+ }
397
+
398
+ $result = array(
399
+ 'product' => $description,
400
+ 'unitprice' => $shippingEx,
401
+ 'quantity' => 1,
402
+ 'vatamount' => $shippingVat,
403
+ ) + $this->getVatRangeTags($shippingVat, $shippingEx);
404
+
405
+ return $result;
406
+ }
407
+
408
+ /**
409
+ * {@inheritdoc}
410
+ */
411
+ protected function getDiscountLines()
412
+ {
413
+ $result = array();
414
+
415
+ // For refunds without any articles (probably just a manual refund) we don't
416
+ // need to know what discounts were applied on the original order. So skip
417
+ // get_used_coupons() on refunds without articles.
418
+ if ($this->invoiceSource->getType() !== Source::CreditNote || $this->hasItemLines) {
419
+ // Add a line for all coupons applied. Coupons are only stored on the order,
420
+ // not on refunds, so use the order property.
421
+ $usedCoupons = $this->order->get_used_coupons();
422
+ foreach ($usedCoupons as $code) {
423
+ $coupon = new WC_Coupon($code);
424
+ $result[] = $this->getDiscountLine($coupon);
425
+ }
426
+ }
427
+ return $result;
428
+ }
429
+
430
+ /**
431
+ * Returns 1 order discount line for 1 coupon usage.
432
+ *
433
+ * In woocommerce, discounts are implemented with coupons. Multiple coupons
434
+ * can be used per order. Coupons can:
435
+ * - have a fixed amount or a percentage.
436
+ * - be applied to the whole cart or only be used for a set of products.
437
+ *
438
+ * Hooray:
439
+ * As of WooCommerce 2.3, coupons can no longer be set as "apply after tax":
440
+ * https://woocommerce.wordpress.com/2014/12/12/upcoming-coupon-changes-in-woocommerce-2-3/
441
+ * WC_Coupon::apply_before_tax() now always returns true (and thus might be
442
+ * deprecated and removed in the future): do no longer use.
443
+ *
444
+ * @param WC_Coupon $coupon
445
+ *
446
+ * @return array
447
+ */
448
+ protected function getDiscountLine(WC_Coupon $coupon)
449
+ {
450
+ // Get a description for the value of this coupon.
451
+ // Entered discount amounts follow the wc_prices_include_tax() setting. Use
452
+ // that info in the description.
453
+ $description = sprintf('%s %s: ', $this->t('discount_code'), $coupon->code);
454
+ if (in_array($coupon->discount_type, array('fixed_product', 'fixed_cart'))) {
455
+ $amount = $this->getSign() * $coupon->coupon_amount;
456
+ if (!Number::isZero($amount)) {
457
+ $description .= sprintf('€%.2f (%s)', $amount, wc_prices_include_tax() ? $this->t('inc_vat') : $this->t('ex_vat'));
458
+ }
459
+ if ($coupon->enable_free_shipping()) {
460
+ if (!Number::isZero($amount)) {
461
+ $description .= ' + ';
462
+ }
463
+ $description .= $this->t('free_shipping');
464
+ }
465
+ } else {
466
+ $description .= $coupon->coupon_amount . '%';
467
+ if ($coupon->enable_free_shipping()) {
468
+ $description .= ' + ' . $this->t('free_shipping');
469
+ }
470
+ }
471
+
472
+ // Discounts are already applied, add a descriptive line with 0 amount.
473
+ // The VAT rate to categorize this line under should be determined by the
474
+ // completor.
475
+ return array(
476
+ 'itemnumber' => $coupon->code,
477
+ 'product' => $description,
478
+ 'unitprice' => 0,
479
+ 'unitpriceinc' => 0,
480
+ 'quantity' => 1,
481
+ 'vatrate' => null,
482
+ 'vatamount' => 0,
483
+ 'meta-vatrate-source' => static::VatRateSource_Completor,
484
+ );
485
+ }
486
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Invoice/Source.php ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Invoice;
3
+
4
+ use Siel\Acumulus\Invoice\Source as BaseSource;
5
+
6
+ /**
7
+ * Wraps a WooCommerce order in an invoice source object.
8
+ *
9
+ * Since WC 2.2.0 multiple order types can be defined, @see
10
+ * wc_register_order_type() and wc_get_order_types(). WooCommerce itself defines
11
+ * 'shop_order' and 'shop_order_refund'. The base class for all these types of
12
+ * orders is WC_Abstract_Order
13
+ */
14
+ class Source extends BaseSource
15
+ {
16
+ // More specifically typed properties.
17
+ /** @var \WC_Abstract_Order */
18
+ protected $source;
19
+
20
+ /**
21
+ * Loads an Order or refund source for the set id.
22
+ */
23
+ protected function setSource()
24
+ {
25
+ $this->source = WC()->order_factory->get_order($this->id);
26
+ }
27
+
28
+ /**
29
+ * Sets the id based on the loaded Order or Order refund.
30
+ */
31
+ protected function setId()
32
+ {
33
+ $this->id = $this->source->id;
34
+ }
35
+
36
+ /**
37
+ * {@inheritdoc}
38
+ */
39
+ public function getReference()
40
+ {
41
+ // Method get_order_number() is used for when other plugins are installed
42
+ // that add an order number that differs from the ID. Known plugins that do
43
+ // so: woocommerce-sequential-order-numbers(-pro) and
44
+ // wc-sequential-order-numbers.
45
+ return $this->source->get_order_number();
46
+ }
47
+
48
+ /**
49
+ * {@inheritDoc}
50
+ *
51
+ * @return string
52
+ */
53
+ public function getStatus()
54
+ {
55
+ return $this->source->get_status();
56
+ }
57
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/AcumulusEntryModel.php ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Shop;
3
+
4
+ use Siel\Acumulus\Shop\AcumulusEntryModel as BaseAcumulusEntryModel;
5
+ use Siel\Acumulus\WooCommerce\Helpers\Log;
6
+ use Siel\Acumulus\WooCommerce\Invoice\Source;
7
+
8
+ /**
9
+ * Implements the WooCommerce/WordPress specific acumulus entry model class.
10
+ *
11
+ * In WordPress this data is stored as metadata. As such, the "records" returned
12
+ * here are an array of all metadata values, thus not filtered by Acumulus keys.
13
+ */
14
+ class AcumulusEntryModel extends BaseAcumulusEntryModel
15
+ {
16
+ const KEY_ENTRY_ID = '_acumulus_entry_id';
17
+ const KEY_TOKEN = '_acumulus_token';
18
+ // Note this meta key is not actually stored as the post_type can give us the
19
+ // same information.
20
+ const KEY_TYPE = '_acumulus_type';
21
+ const KEY_CREATED = '_acumulus_created';
22
+ const KEY_UPDATED = '_acumulus_updated';
23
+
24
+ /**
25
+ * Helper method that converts a WP/WC post type to a source type constant.
26
+ *
27
+ * @param string $shopType
28
+ *
29
+ * @return string
30
+ */
31
+ protected function shopTypeToSourceType($shopType)
32
+ {
33
+ switch ($shopType) {
34
+ case 'shop_order':
35
+ return Source::Order;
36
+ case 'shop_order_refund':
37
+ return Source::CreditNote;
38
+ default:
39
+ Log::getInstance()->error('InvoiceManager::shopTypeToSourceType(%s): unknown', $shopType);
40
+ return '';
41
+ }
42
+ }
43
+
44
+ /**
45
+ * {@inheritdoc}
46
+ */
47
+ public function getByEntryId($entryId)
48
+ {
49
+ $result = false;
50
+ $metaQuery = array(
51
+ 'posts_per_page' => 1,
52
+ 'meta_key' => static::KEY_ENTRY_ID,
53
+ 'meta_value' => $entryId,
54
+ 'meta_compare' => '=',
55
+ );
56
+ $posts = query_posts($metaQuery);
57
+ if (!empty($posts)) {
58
+ $post = reset($posts);
59
+ $result = get_post_meta($post->id);
60
+ $result[static::KEY_TYPE] = $this->shopTypeToSourceType($post->post_type);
61
+ }
62
+ return $result !== false ? $result : null;
63
+ }
64
+
65
+ /**
66
+ * {@inheritdoc}
67
+ */
68
+ public function getByInvoiceSourceId($invoiceSourceType, $invoiceSourceId)
69
+ {
70
+ $result = null;
71
+ $post = get_post($invoiceSourceId);
72
+ if (!empty($post->post_type) && $this->shopTypeToSourceType($post->post_type) === $invoiceSourceType) {
73
+ $result = get_post_meta($invoiceSourceId);
74
+ if (isset($result[static::KEY_ENTRY_ID])) {
75
+ // Acumulus meta data found: add invoice type as that is not stored in
76
+ // the meta data.
77
+ $result[static::KEY_TYPE] = $invoiceSourceType;
78
+ } else {
79
+ $result = null;
80
+ }
81
+ }
82
+ return $result;
83
+ }
84
+
85
+ /**
86
+ * {@inheritdoc}
87
+ *
88
+ * This override uses the WordPress meta data API to store the acumulus entry
89
+ * data with the order.
90
+ */
91
+ public function save($invoiceSource, $entryId, $token)
92
+ {
93
+ $now = $this->sqlNow();
94
+ $orderId = $invoiceSource->getId();
95
+ add_post_meta($orderId, static::KEY_CREATED, $now, true);
96
+ //$exists = add_post_meta($orderId, '_acumulus_created', $now, true) === FALSE;
97
+ return update_post_meta($orderId, static::KEY_ENTRY_ID, $entryId) !== false
98
+ && update_post_meta($orderId, static::KEY_TOKEN, $token) !== false
99
+ && update_post_meta($orderId, static::KEY_UPDATED, $now) !== false;
100
+ }
101
+
102
+ /**
103
+ * {@inheritdoc}
104
+ */
105
+ protected function insert($invoiceSource, $entryId, $token, $created)
106
+ {
107
+ throw new \BadMethodCallException(__METHOD__ . ' not implemented');
108
+ }
109
+
110
+ /**
111
+ * {@inheritdoc}
112
+ */
113
+ protected function update($record, $entryId, $token, $updated)
114
+ {
115
+ throw new \BadMethodCallException(__METHOD__ . ' not implemented');
116
+ }
117
+
118
+ /**
119
+ * {@inheritdoc}
120
+ */
121
+ protected function sqlNow()
122
+ {
123
+ return current_time('timestamp', true);
124
+ }
125
+
126
+ /**
127
+ * {@inheritdoc}
128
+ *
129
+ * We use the WordPress metadata API which is readily available, so nothing
130
+ * has to be done here.
131
+ */
132
+ public function install()
133
+ {
134
+ return true;
135
+ }
136
+
137
+ /**
138
+ * {@inheritdoc}
139
+ *
140
+ * We use the WordPress metadata API which is readily available, so nothing
141
+ * has to be done here.
142
+ */
143
+ public function uninstall()
144
+ {
145
+ // We do not delete all Acumulus metadata, not even via a confirmation page.
146
+ // If we would want to do so, we can use this code:
147
+ // delete_post_meta_by_key('_acumulus_entry_id'); // for other keys as well.
148
+ return true;
149
+ }
150
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/ConfigForm.php ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Shop;
3
+
4
+ use Siel\Acumulus\Invoice\ConfigInterface as InvoiceConfigInterface;
5
+ use Siel\Acumulus\Shop\ConfigForm as BaseConfigForm;
6
+ use Siel\Acumulus\Shop\ConfigInterface;
7
+
8
+ /**
9
+ * Class ConfigForm processes and builds the settings form page for the
10
+ * WooCommerce Acumulus module.
11
+ */
12
+ class ConfigForm extends BaseConfigForm
13
+ {
14
+ /**
15
+ * {@inheritdoc}
16
+ */
17
+ protected function getShopOrderStatuses()
18
+ {
19
+ $result = array();
20
+
21
+ $orderStatuses = wc_get_order_statuses();
22
+ foreach ($orderStatuses as $key => $label) {
23
+ $result[substr($key, strlen('wc-'))] = $label;
24
+ }
25
+
26
+ return $result;
27
+ }
28
+
29
+ /**
30
+ * {@inheritdoc}
31
+ *
32
+ * This override removes the 'Use invoice #' option as WC does not have
33
+ * separate invoices.
34
+ */
35
+ protected function getInvoiceNrSourceOptions()
36
+ {
37
+ $result = parent::getInvoiceNrSourceOptions();
38
+ unset($result[InvoiceConfigInterface::InvoiceNrSource_ShopInvoice]);
39
+ return $result;
40
+ }
41
+
42
+ /**
43
+ * {@inheritdoc}
44
+ *
45
+ * This override removes the 'Use invoice date' option as WC does not have
46
+ * separate invoices.
47
+ */
48
+ protected function getDateToUseOptions()
49
+ {
50
+ $result = parent::getDateToUseOptions();
51
+ unset($result[InvoiceConfigInterface::InvoiceDate_InvoiceCreate]);
52
+ return $result;
53
+ }
54
+
55
+ /**
56
+ * {@inheritdoc}
57
+ *
58
+ * This override removes the 'Use invoice sent' option as WC does not have
59
+ * separate invoices, let alone an event on sending it.
60
+ */
61
+ protected function getTriggerInvoiceSendEventOptions()
62
+ {
63
+ $result = parent::getTriggerInvoiceSendEventOptions();
64
+ unset($result[ConfigInterface::TriggerInvoiceSendEvent_InvoiceCreate]);
65
+ return $result;
66
+ }
67
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/ConfigStore.php ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Shop;
3
+
4
+ use Siel\Acumulus\Shop\ConfigStore as BaseConfigStore;
5
+
6
+ /**
7
+ * Implements the connection to the WordPress config component.
8
+ */
9
+ class ConfigStore extends BaSeConfigStore
10
+ {
11
+ /**
12
+ * {@inheritdoc}
13
+ */
14
+ public function getShopEnvironment()
15
+ {
16
+ global $wp_version, $woocommerce;
17
+ $environment = array(
18
+ // Lazy load is no longer needed (as in L3) as this method will only be
19
+ // called when the config gets actually queried.
20
+ 'moduleVersion' => \Acumulus::create()->getVersionNumber(),
21
+ 'shopName' => $this->shopName,
22
+ 'shopVersion' => (isset($woocommerce) ? $woocommerce->version : 'unknown') . ' (WordPress: ' . $wp_version . ')',
23
+ );
24
+ return $environment;
25
+ }
26
+
27
+ /**
28
+ * {@inheritdoc}
29
+ */
30
+ public function load(array $keys)
31
+ {
32
+ $result = array();
33
+ // Load the values from the web shop specific configuration.
34
+ $configurationValues = get_option('acumulus');
35
+ if (is_array($configurationValues)) {
36
+ foreach ($keys as $key) {
37
+ // Do not overwrite defaults if no value is set.
38
+ if (isset($configurationValues[$key])) {
39
+ $result[$key] = $configurationValues[$key];
40
+ }
41
+ }
42
+ }
43
+ return $result;
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ */
49
+ public function save(array $values)
50
+ {
51
+ $values = $this->saveCommon($values);
52
+
53
+ $configurationValues = array();
54
+ foreach ($values as $key => $value) {
55
+ if ($value !== null) {
56
+ $configurationValues[$key] = $value;
57
+ }
58
+ }
59
+ return update_option('acumulus', $configurationValues);
60
+ }
61
+ }
app/code/community/Siel/Acumulus/libraries/Siel/Acumulus/WooCommerce/Shop/InvoiceManager.php ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace Siel\Acumulus\WooCommerce\Shop;
3
+
4
+ use DateTime;
5
+ use Siel\Acumulus\Invoice\Source as BaseSource;
6
+ use Siel\Acumulus\Shop\InvoiceManager as BaseInvoiceManager;
7
+ use Siel\Acumulus\WooCommerce\Invoice\Source;
8
+ use WP_Query;
9
+
10
+ class InvoiceManager extends BaseInvoiceManager
11
+ {
12
+ /**
13
+ * Helper method that converts our source type constants to a WP/WC post type.
14
+ *
15
+ * @param string $invoiceSourceType
16
+ *
17
+ * @return string
18
+ */
19
+ protected function sourceTypeToShopType($invoiceSourceType)
20
+ {
21
+ switch ($invoiceSourceType) {
22
+ case Source::Order:
23
+ return 'shop_order';
24
+ case Source::CreditNote:
25
+ return 'shop_order_refund';
26
+ default:
27
+ $this->config->getLog()->error('InvoiceManager::sourceTypeToShopType(%s): unknown', $invoiceSourceType);
28
+ return '';
29
+ }
30
+ }
31
+
32
+ /**
33
+ * {@inheritdoc}
34
+ */
35
+ public function getInvoiceSourcesByIdRange($invoiceSourceType, $InvoiceSourceIdFrom, $InvoiceSourceIdTo)
36
+ {
37
+ // We use our own query here as defining a range of pots ids based on a
38
+ // between does not seem to be possible with the query syntax.
39
+ global $wpdb;
40
+ $key = 'ID';
41
+ $invoiceSourceIds = $wpdb->get_col($wpdb->prepare("SELECT `$key` FROM `{$wpdb->posts}` WHERE `$key` BETWEEN %u AND %u AND `post_type` = %s",
42
+ $InvoiceSourceIdFrom, $InvoiceSourceIdTo, $this->sourceTypeToShopType($invoiceSourceType)));
43
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $invoiceSourceIds);
44
+ }
45
+
46
+ /**
47
+ * {@inheritdoc}
48
+ *
49
+ * We support:
50
+ * - "WooCommerce Sequential Order Numbers (Pro)", see
51
+ * https://wordpress.org/plugins/woocommerce-sequential-order-numbers/ and
52
+ * http://docs.woothemes.com/document/sequential-order-numbers/.
53
+ * - "WC Sequential Order Numbers", see
54
+ * https://wordpress.org/plugins/wc-sequential-order-numbers/ and
55
+ * http://plugins.dualcube.com/product/wc-sequential-order-numbers/.
56
+ *
57
+ * If you know of other plugins, please let us know.
58
+ */
59
+ public function getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo)
60
+ {
61
+ // To be able to define the query we need to know under which meta key the
62
+ // order number/reference is stored.
63
+ // - WooCommerce Sequential Order Numbers (Pro) uses the key order_number.
64
+ // - WC Sequential Order Numbers uses the keys order_number_formatted and
65
+ // order_number.
66
+ // Both only work with orders, not refunds.
67
+ if ((is_plugin_active('woocommerce-sequential-order-numbers/woocommerce-sequential-order-numbers.php') || is_plugin_active('woocommerce-sequential-order-numbers-pro/woocommerce-sequential-order-numbers-pro.php')) && $invoiceSourceType === Source::Order) {
68
+ // Search for the order by the order number as assigned by the plugin.
69
+ $args = array(
70
+ 'meta_query' => array(
71
+ array(
72
+ 'key' => '_order_number',
73
+ 'value' => array($InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo),
74
+ 'compare' => 'BETWEEN',
75
+ 'type' => 'NUMERIC',
76
+ ),
77
+ ),
78
+ );
79
+ return $this->query2Sources($args, $invoiceSourceType);
80
+ } else if (is_plugin_active('wc-sequential-order-numbers/Sequential_Order_Numbers.php') && $invoiceSourceType === Source::Order) {
81
+ // This plugin has not been tested yet. It will probably give problems as
82
+ // on installing it does not add the used meta keys retrospectively. So
83
+ // I think this will only work for orders added after installation of this
84
+ // plugin. But then, that is a bit the nature of the idea of sequential
85
+ // order numbers: it should be used as of the beginning...
86
+ $args = array(
87
+ 'meta_query' => array(
88
+ array(
89
+ 'key' => '_order_number_formatted',
90
+ 'value' => array($InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo),
91
+ 'compare' => 'BETWEEN',
92
+ ),
93
+ ),
94
+ );
95
+ return $this->query2Sources($args, $invoiceSourceType);
96
+ }
97
+ return parent::getInvoiceSourcesByReferenceRange($invoiceSourceType, $InvoiceSourceReferenceFrom, $InvoiceSourceReferenceTo);
98
+ }
99
+
100
+ /**
101
+ * {@inheritdoc}
102
+ */
103
+ public function getInvoiceSourcesByDateRange($invoiceSourceType, DateTime $dateFrom, DateTime $dateTo)
104
+ {
105
+ $args = array(
106
+ 'date_query' => array(
107
+ array(
108
+ 'column' => 'post_modified',
109
+ 'after' => array(
110
+ 'year' => $dateFrom->format('Y'),
111
+ 'month' => $dateFrom->format('m'),
112
+ 'day' => $dateFrom->format('d'),
113
+ ),
114
+ 'before' => array(
115
+ 'year' => $dateTo->format('Y'),
116
+ 'month' => $dateTo->format('m'),
117
+ 'day' => $dateTo->format('d'),
118
+ ),
119
+ 'inclusive' => true,
120
+ ),
121
+ ),
122
+ );
123
+ return $this->query2Sources($args, $invoiceSourceType);
124
+ }
125
+
126
+ /**
127
+ * {@inheritdoc}
128
+ *
129
+ * This WooCommerce override applies the 'acumulus_invoice_created' filter.
130
+ */
131
+ protected function triggerInvoiceCreated(array &$invoice, BaseSource $invoiceSource)
132
+ {
133
+ $invoice = apply_filters('acumulus_invoice_created', $invoice, $invoiceSource);
134
+ }
135
+
136
+ /**
137
+ * {@inheritdoc}
138
+ *
139
+ * This WooCommerce override applies the 'acumulus_invoice_completed' filter.
140
+ */
141
+ protected function triggerInvoiceCompleted(array &$invoice, BaseSource $invoiceSource)
142
+ {
143
+ $invoice = apply_filters('acumulus_invoice_completed', $invoice, $invoiceSource);
144
+ }
145
+
146
+ /**
147
+ * {@inheritdoc}
148
+ *
149
+ * This WooCommerce override executes the 'acumulus_invoice_sent' action.
150
+ */
151
+ protected function triggerInvoiceSent(array $invoice, BaseSource $invoiceSource, array $result)
152
+ {
153
+ do_action('acumulus_invoice_sent', $invoice, $invoiceSource, $result);
154
+ }
155
+
156
+ /**
157
+ * Helper method to get a list of Source given a set of query arguments.
158
+ *
159
+ * @param array $args
160
+ * @param string $invoiceSourceType
161
+ * @param bool $sort
162
+ *
163
+ * @return Source[]
164
+ */
165
+ protected function query2Sources(array $args, $invoiceSourceType, $sort = true)
166
+ {
167
+ // Add default arguments.
168
+ $args = $args + array(
169
+ 'fields' => 'ids',
170
+ 'numberposts' => -1,
171
+ 'post_type' => $this->sourceTypeToShopType($invoiceSourceType),
172
+ 'post_status' => array_keys(wc_get_order_statuses()),
173
+ );
174
+ $query = new WP_Query($args);
175
+ $ids = $query->get_posts();
176
+ if ($sort) {
177
+ sort($ids);
178
+ }
179
+ return $this->getSourcesByIdsOrSources($invoiceSourceType, $ids);
180
+ }
181
+ }
app/code/community/Siel/Acumulus/libraries/Siel/leesmij.txt ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Acumulus web service API library
2
+ ===============================
3
+
4
+ @author: Buro Rader (http://www.burorader.com/).
5
+ @support: TODO. zie het Acumulus forum.
6
+
7
+ Inleiding
8
+ ---------
9
+ Deze Acumulus web service API library is bedoeld om het ontwikkelen van client
10
+ side code die communiceert met de Acumulus API te vereenvoudigen. De library is
11
+ in eerste instantie ontwikkelt voor gebruik door de koppelingen voor de webshop
12
+ software van Magento, PrestaShop, OpenCart, VirtueMart en WooCommerce, maar is
13
+ dusdanig opgezet dat ook andere koppelingen er hun voordeel mee kunnen doen.
14
+
15
+ Dit is dus een cross-CMS/webshop library. Daarom kan de code in deze library
16
+ niet voldoen aan specifieke richtlijnen voor een specifieke webshop of CMS. Denk
17
+ hierbij aan:
18
+ - Coding standards.
19
+ - Organisatie en naamgeving.
20
+ - Vertaling door gebruikmaking van webshop specifieke vertaalfunctionaliteit.
21
+ - Webshop specifieke "Best practises".
22
+
23
+ Licentie
24
+ --------
25
+ Als deze library u apart ter beschikking is gesteld, is dit onder de GNU GENERAL
26
+ PUBLIC LICENSE versie 3. De Engelstalige en enige officiële tekst hiervan kunt u
27
+ vinden op http://www.gnu.org/licenses/gpl.html. Een niet bindende
28
+ Nederlandstalige vertaling hiervan vindt u op
29
+ http://bartbeuving.files.wordpress.com/2008/07/gpl-v3-nl-101.pdf. Beide teksten
30
+ zijn ook meegeleverd met deze module.
31
+
32
+ Als deze library u als onderdeel van een koppeling ter beschikking is gesteld,
33
+ geldt de licentie van de koppeling ook voor deze library.
34
+
35
+ Ontwikkeling
36
+ ------------
37
+ Deze library is nog (volop) in ontwikkeling en zal:
38
+ - waar nodig aangepast worden om met nog meer webshops om te kunnen gaan
39
+ - uitgebreid worden met nieuwe wensen.
40
+ - waar mogelijk en zinvol nog verder geabstraheerd worden.
41
+
42
+ Backwards compatibility is daarbij een zorg maar geen absolute vereiste. Omdat
43
+ de webshopkoppelingen met library uitgeleverd worden is dit voor gebruikers geen
44
+ probleem. Voor ontwikkelaars die zelfstandig met deze library aan de slag zijn
45
+ gegaan kan dit wel voor problemen zorgen.
46
+
47
+ todo's:
48
+ - Class "webshop capabilities" die een aantal methods die een webshop-specifieke
49
+ lijst terugkeren centraliseert (is er een credit note, is er een invoice)
50
+ - Class "Shop invoice" (wrapper zoals "Invoice Source").
51
+ - Margin scheme zal niet samen gaan met gebruik van getVatRangeTags(). Refactor
52
+ dit.
53
+ - Versturen credit notes via opties op config formulier regelen: (wel/niet
54
+ automatisch + moment van versturen),
app/code/community/Siel/Acumulus/libraries/Siel/psr4.php ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @file This file registers an autoloader for the Siel namespace library.
4
+ *
5
+ * As not all web shops support auto-loading based on namespaces or have other
6
+ * glitches, eg. expecting lower cased file names, we define our own autoloader.
7
+ * If the module cannot use the autoloader of the web shop, this file should be
8
+ * loaded during bootstrapping of the module.
9
+ *
10
+ * Thanks to https://gist.github.com/mageekguy/8300961
11
+ */
12
+ namespace Siel;
13
+
14
+ // Prepend this autoloader: it will not throw, nor warn, while the shop specific
15
+ // autoloader might do so.
16
+ spl_autoload_register(
17
+ function ($class) {
18
+ if (strpos($class, __NAMESPACE__ . '\\') === 0) {
19
+ $fileName = __DIR__ . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen(__NAMESPACE__))) . '.php';
20
+ // Checking if the file exists prevent warnings in OpenCart1 where
21
+ // using just @include(...) did not help prevent them.
22
+ if (is_readable($fileName)) {
23
+ include($fileName);
24
+ }
25
+ }
26
+ },
27
+ false,
28
+ true);
app/code/community/Siel/Acumulus/license.txt ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ them if you wish), that you receive source code or can get it if you
26
+ want it, that you can change the software or use pieces of it in new
27
+ free programs, and that you know you can do these things.
28
+
29
+ To protect your rights, we need to prevent others from denying you
30
+ these rights or asking you to surrender the rights. Therefore, you have
31
+ certain responsibilities if you distribute copies of the software, or if
32
+ you modify it: responsibilities to respect the freedom of others.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must pass on to the recipients the same
36
+ freedoms that you received. You must make sure that they, too, receive
37
+ or can get the source code. And you must show them these terms so they
38
+ know their rights.
39
+
40
+ Developers that use the GNU GPL protect your rights with two steps:
41
+ (1) assert copyright on the software, and (2) offer you this License
42
+ giving you legal permission to copy, distribute and/or modify it.
43
+
44
+ For the developers' and authors' protection, the GPL clearly explains
45
+ that there is no warranty for this free software. For both users' and
46
+ authors' sake, the GPL requires that modified versions be marked as
47
+ changed, so that their problems will not be attributed erroneously to
48
+ authors of previous versions.
49
+
50
+ Some devices are designed to deny users access to install or run
51
+ modified versions of the software inside them, although the manufacturer
52
+ can do so. This is fundamentally incompatible with the aim of
53
+ protecting users' freedom to change the software. The systematic
54
+ pattern of such abuse occurs in the area of products for individuals to
55
+ use, which is precisely where it is most unacceptable. Therefore, we
56
+ have designed this version of the GPL to prohibit the practice for those
57
+ products. If such problems arise substantially in other domains, we
58
+ stand ready to extend this provision to those domains in future versions
59
+ of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents.
62
+ States should not allow patents to restrict development and use of
63
+ software on general-purpose computers, but in those that do, we wish to
64
+ avoid the special danger that patents applied to a free program could
65
+ make it effectively proprietary. To prevent this, the GPL assures that
66
+ patents cannot be used to render the program non-free.
67
+
68
+ The precise terms and conditions for copying, distribution and
69
+ modification follow.
70
+
71
+ TERMS AND CONDITIONS
72
+
73
+ 0. Definitions.
74
+
75
+ "This License" refers to version 3 of the GNU General Public License.
76
+
77
+ "Copyright" also means copyright-like laws that apply to other kinds of
78
+ works, such as semiconductor masks.
79
+
80
+ "The Program" refers to any copyrightable work licensed under this
81
+ License. Each licensee is addressed as "you". "Licensees" and
82
+ "recipients" may be individuals or organizations.
83
+
84
+ To "modify" a work means to copy from or adapt all or part of the work
85
+ in a fashion requiring copyright permission, other than the making of an
86
+ exact copy. The resulting work is called a "modified version" of the
87
+ earlier work or a work "based on" the earlier work.
88
+
89
+ A "covered work" means either the unmodified Program or a work based
90
+ on the Program.
91
+
92
+ To "propagate" a work means to do anything with it that, without
93
+ permission, would make you directly or secondarily liable for
94
+ infringement under applicable copyright law, except executing it on a
95
+ computer or modifying a private copy. Propagation includes copying,
96
+ distribution (with or without modification), making available to the
97
+ public, and in some countries other activities as well.
98
+
99
+ To "convey" a work means any kind of propagation that enables other
100
+ parties to make or receive copies. Mere interaction with a user through
101
+ a computer network, with no transfer of a copy, is not conveying.
102
+
103
+ An interactive user interface displays "Appropriate Legal Notices"
104
+ to the extent that it includes a convenient and prominently visible
105
+ feature that (1) displays an appropriate copyright notice, and (2)
106
+ tells the user that there is no warranty for the work (except to the
107
+ extent that warranties are provided), that licensees may convey the
108
+ work under this License, and how to view a copy of this License. If
109
+ the interface presents a list of user commands or options, such as a
110
+ menu, a prominent item in the list meets this criterion.
111
+
112
+ 1. Source Code.
113
+
114
+ The "source code" for a work means the preferred form of the work
115
+ for making modifications to it. "Object code" means any non-source
116
+ form of a work.
117
+
118
+ A "Standard Interface" means an interface that either is an official
119
+ standard defined by a recognized standards body, or, in the case of
120
+ interfaces specified for a particular programming language, one that
121
+ is widely used among developers working in that language.
122
+
123
+ The "System Libraries" of an executable work include anything, other
124
+ than the work as a whole, that (a) is included in the normal form of
125
+ packaging a Major Component, but which is not part of that Major
126
+ Component, and (b) serves only to enable use of the work with that
127
+ Major Component, or to implement a Standard Interface for which an
128
+ implementation is available to the public in source code form. A
129
+ "Major Component", in this context, means a major essential component
130
+ (kernel, window system, and so on) of the specific operating system
131
+ (if any) on which the executable work runs, or a compiler used to
132
+ produce the work, or an object code interpreter used to run it.
133
+
134
+ The "Corresponding Source" for a work in object code form means all
135
+ the source code needed to generate, install, and (for an executable
136
+ work) run the object code and to modify the work, including scripts to
137
+ control those activities. However, it does not include the work's
138
+ System Libraries, or general-purpose tools or generally available free
139
+ programs which are used unmodified in performing those activities but
140
+ which are not part of the work. For example, Corresponding Source
141
+ includes interface definition files associated with source files for
142
+ the work, and the source code for shared libraries and dynamically
143
+ linked subprograms that the work is specifically designed to require,
144
+ such as by intimate data communication or control flow between those
145
+ subprograms and other parts of the work.
146
+
147
+ The Corresponding Source need not include anything that users
148
+ can regenerate automatically from other parts of the Corresponding
149
+ Source.
150
+
151
+ The Corresponding Source for a work in source code form is that
152
+ same work.
153
+
154
+ 2. Basic Permissions.
155
+
156
+ All rights granted under this License are granted for the term of
157
+ copyright on the Program, and are irrevocable provided the stated
158
+ conditions are met. This License explicitly affirms your unlimited
159
+ permission to run the unmodified Program. The output from running a
160
+ covered work is covered by this License only if the output, given its
161
+ content, constitutes a covered work. This License acknowledges your
162
+ rights of fair use or other equivalent, as provided by copyright law.
163
+
164
+ You may make, run and propagate covered works that you do not
165
+ convey, without conditions so long as your license otherwise remains
166
+ in force. You may convey covered works to others for the sole purpose
167
+ of having them make modifications exclusively for you, or provide you
168
+ with facilities for running those works, provided that you comply with
169
+ the terms of this License in conveying all material for which you do
170
+ not control copyright. Those thus making or running the covered works
171
+ for you must do so exclusively on your behalf, under your direction
172
+ and control, on terms that prohibit them from making any copies of
173
+ your copyrighted material outside their relationship with you.
174
+
175
+ Conveying under any other circumstances is permitted solely under
176
+ the conditions stated below. Sublicensing is not allowed; section 10
177
+ makes it unnecessary.
178
+
179
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
+
181
+ No covered work shall be deemed part of an effective technological
182
+ measure under any applicable law fulfilling obligations under article
183
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184
+ similar laws prohibiting or restricting circumvention of such
185
+ measures.
186
+
187
+ When you convey a covered work, you waive any legal power to forbid
188
+ circumvention of technological measures to the extent such circumvention
189
+ is effected by exercising rights under this License with respect to
190
+ the covered work, and you disclaim any intention to limit operation or
191
+ modification of the work as a means of enforcing, against the work's
192
+ users, your or third parties' legal rights to forbid circumvention of
193
+ technological measures.
194
+
195
+ 4. Conveying Verbatim Copies.
196
+
197
+ You may convey verbatim copies of the Program's source code as you
198
+ receive it, in any medium, provided that you conspicuously and
199
+ appropriately publish on each copy an appropriate copyright notice;
200
+ keep intact all notices stating that this License and any
201
+ non-permissive terms added in accord with section 7 apply to the code;
202
+ keep intact all notices of the absence of any warranty; and give all
203
+ recipients a copy of this License along with the Program.
204
+
205
+ You may charge any price or no price for each copy that you convey,
206
+ and you may offer support or warranty protection for a fee.
207
+
208
+ 5. Conveying Modified Source Versions.
209
+
210
+ You may convey a work based on the Program, or the modifications to
211
+ produce it from the Program, in the form of source code under the
212
+ terms of section 4, provided that you also meet all of these conditions:
213
+
214
+ a) The work must carry prominent notices stating that you modified
215
+ it, and giving a relevant date.
216
+
217
+ b) The work must carry prominent notices stating that it is
218
+ released under this License and any conditions added under section
219
+ 7. This requirement modifies the requirement in section 4 to
220
+ "keep intact all notices".
221
+
222
+ c) You must license the entire work, as a whole, under this
223
+ License to anyone who comes into possession of a copy. This
224
+ License will therefore apply, along with any applicable section 7
225
+ additional terms, to the whole of the work, and all its parts,
226
+ regardless of how they are packaged. This License gives no
227
+ permission to license the work in any other way, but it does not
228
+ invalidate such permission if you have separately received it.
229
+
230
+ d) If the work has interactive user interfaces, each must display
231
+ Appropriate Legal Notices; however, if the Program has interactive
232
+ interfaces that do not display Appropriate Legal Notices, your
233
+ work need not make them do so.
234
+
235
+ A compilation of a covered work with other separate and independent
236
+ works, which are not by their nature extensions of the covered work,
237
+ and which are not combined with it such as to form a larger program,
238
+ in or on a volume of a storage or distribution medium, is called an
239
+ "aggregate" if the compilation and its resulting copyright are not
240
+ used to limit the access or legal rights of the compilation's users
241
+ beyond what the individual works permit. Inclusion of a covered work
242
+ in an aggregate does not cause this License to apply to the other
243
+ parts of the aggregate.
244
+
245
+ 6. Conveying Non-Source Forms.
246
+
247
+ You may convey a covered work in object code form under the terms
248
+ of sections 4 and 5, provided that you also convey the
249
+ machine-readable Corresponding Source under the terms of this License,
250
+ in one of these ways:
251
+
252
+ a) Convey the object code in, or embodied in, a physical product
253
+ (including a physical distribution medium), accompanied by the
254
+ Corresponding Source fixed on a durable physical medium
255
+ customarily used for software interchange.
256
+
257
+ b) Convey the object code in, or embodied in, a physical product
258
+ (including a physical distribution medium), accompanied by a
259
+ written offer, valid for at least three years and valid for as
260
+ long as you offer spare parts or customer support for that product
261
+ model, to give anyone who possesses the object code either (1) a
262
+ copy of the Corresponding Source for all the software in the
263
+ product that is covered by this License, on a durable physical
264
+ medium customarily used for software interchange, for a price no
265
+ more than your reasonable cost of physically performing this
266
+ conveying of source, or (2) access to copy the
267
+ Corresponding Source from a network server at no charge.
268
+
269
+ c) Convey individual copies of the object code with a copy of the
270
+ written offer to provide the Corresponding Source. This
271
+ alternative is allowed only occasionally and noncommercially, and
272
+ only if you received the object code with such an offer, in accord
273
+ with subsection 6b.
274
+
275
+ d) Convey the object code by offering access from a designated
276
+ place (gratis or for a charge), and offer equivalent access to the
277
+ Corresponding Source in the same way through the same place at no
278
+ further charge. You need not require recipients to copy the
279
+ Corresponding Source along with the object code. If the place to
280
+ copy the object code is a network server, the Corresponding Source
281
+ may be on a different server (operated by you or a third party)
282
+ that supports equivalent copying facilities, provided you maintain
283
+ clear directions next to the object code saying where to find the
284
+ Corresponding Source. Regardless of what server hosts the
285
+ Corresponding Source, you remain obligated to ensure that it is
286
+ available for as long as needed to satisfy these requirements.
287
+
288
+ e) Convey the object code using peer-to-peer transmission, provided
289
+ you inform other peers where the object code and Corresponding
290
+ Source of the work are being offered to the general public at no
291
+ charge under subsection 6d.
292
+
293
+ A separable portion of the object code, whose source code is excluded
294
+ from the Corresponding Source as a System Library, need not be
295
+ included in conveying the object code work.
296
+
297
+ A "User Product" is either (1) a "consumer product", which means any
298
+ tangible personal property which is normally used for personal, family,
299
+ or household purposes, or (2) anything designed or sold for incorporation
300
+ into a dwelling. In determining whether a product is a consumer product,
301
+ doubtful cases shall be resolved in favor of coverage. For a particular
302
+ product received by a particular user, "normally used" refers to a
303
+ typical or common use of that class of product, regardless of the status
304
+ of the particular user or of the way in which the particular user
305
+ actually uses, or expects or is expected to use, the product. A product
306
+ is a consumer product regardless of whether the product has substantial
307
+ commercial, industrial or non-consumer uses, unless such uses represent
308
+ the only significant mode of use of the product.
309
+
310
+ "Installation Information" for a User Product means any methods,
311
+ procedures, authorization keys, or other information required to install
312
+ and execute modified versions of a covered work in that User Product from
313
+ a modified version of its Corresponding Source. The information must
314
+ suffice to ensure that the continued functioning of the modified object
315
+ code is in no case prevented or interfered with solely because
316
+ modification has been made.
317
+
318
+ If you convey an object code work under this section in, or with, or
319
+ specifically for use in, a User Product, and the conveying occurs as
320
+ part of a transaction in which the right of possession and use of the
321
+ User Product is transferred to the recipient in perpetuity or for a
322
+ fixed term (regardless of how the transaction is characterized), the
323
+ Corresponding Source conveyed under this section must be accompanied
324
+ by the Installation Information. But this requirement does not apply
325
+ if neither you nor any third party retains the ability to install
326
+ modified object code on the User Product (for example, the work has
327
+ been installed in ROM).
328
+
329
+ The requirement to provide Installation Information does not include a
330
+ requirement to continue to provide support service, warranty, or updates
331
+ for a work that has been modified or installed by the recipient, or for
332
+ the User Product in which it has been modified or installed. Access to a
333
+ network may be denied when the modification itself materially and
334
+ adversely affects the operation of the network or violates the rules and
335
+ protocols for communication across the network.
336
+
337
+ Corresponding Source conveyed, and Installation Information provided,
338
+ in accord with this section must be in a format that is publicly
339
+ documented (and with an implementation available to the public in
340
+ source code form), and must require no special password or key for
341
+ unpacking, reading or copying.
342
+
343
+ 7. Additional Terms.
344
+
345
+ "Additional permissions" are terms that supplement the terms of this
346
+ License by making exceptions from one or more of its conditions.
347
+ Additional permissions that are applicable to the entire Program shall
348
+ be treated as though they were included in this License, to the extent
349
+ that they are valid under applicable law. If additional permissions
350
+ apply only to part of the Program, that part may be used separately
351
+ under those permissions, but the entire Program remains governed by
352
+ this License without regard to the additional permissions.
353
+
354
+ When you convey a copy of a covered work, you may at your option
355
+ remove any additional permissions from that copy, or from any part of
356
+ it. (Additional permissions may be written to require their own
357
+ removal in certain cases when you modify the work.) You may place
358
+ additional permissions on material, added by you to a covered work,
359
+ for which you have or can give appropriate copyright permission.
360
+
361
+ Notwithstanding any other provision of this License, for material you
362
+ add to a covered work, you may (if authorized by the copyright holders of
363
+ that material) supplement the terms of this License with terms:
364
+
365
+ a) Disclaiming warranty or limiting liability differently from the
366
+ terms of sections 15 and 16 of this License; or
367
+
368
+ b) Requiring preservation of specified reasonable legal notices or
369
+ author attributions in that material or in the Appropriate Legal
370
+ Notices displayed by works containing it; or
371
+
372
+ c) Prohibiting misrepresentation of the origin of that material, or
373
+ requiring that modified versions of such material be marked in
374
+ reasonable ways as different from the original version; or
375
+
376
+ d) Limiting the use for publicity purposes of names of licensors or
377
+ authors of the material; or
378
+
379
+ e) Declining to grant rights under trademark law for use of some
380
+ trade names, trademarks, or service marks; or
381
+
382
+ f) Requiring indemnification of licensors and authors of that
383
+ material by anyone who conveys the material (or modified versions of
384
+ it) with contractual assumptions of liability to the recipient, for
385
+ any liability that these contractual assumptions directly impose on
386
+ those licensors and authors.
387
+
388
+ All other non-permissive additional terms are considered "further
389
+ restrictions" within the meaning of section 10. If the Program as you
390
+ received it, or any part of it, contains a notice stating that it is
391
+ governed by this License along with a term that is a further
392
+ restriction, you may remove that term. If a license document contains
393
+ a further restriction but permits relicensing or conveying under this
394
+ License, you may add to a covered work material governed by the terms
395
+ of that license document, provided that the further restriction does
396
+ not survive such relicensing or conveying.
397
+
398
+ If you add terms to a covered work in accord with this section, you
399
+ must place, in the relevant source files, a statement of the
400
+ additional terms that apply to those files, or a notice indicating
401
+ where to find the applicable terms.
402
+
403
+ Additional terms, permissive or non-permissive, may be stated in the
404
+ form of a separately written license, or stated as exceptions;
405
+ the above requirements apply either way.
406
+
407
+ 8. Termination.
408
+
409
+ You may not propagate or modify a covered work except as expressly
410
+ provided under this License. Any attempt otherwise to propagate or
411
+ modify it is void, and will automatically terminate your rights under
412
+ this License (including any patent licenses granted under the third
413
+ paragraph of section 11).
414
+
415
+ However, if you cease all violation of this License, then your
416
+ license from a particular copyright holder is reinstated (a)
417
+ provisionally, unless and until the copyright holder explicitly and
418
+ finally terminates your license, and (b) permanently, if the copyright
419
+ holder fails to notify you of the violation by some reasonable means
420
+ prior to 60 days after the cessation.
421
+
422
+ Moreover, your license from a particular copyright holder is
423
+ reinstated permanently if the copyright holder notifies you of the
424
+ violation by some reasonable means, this is the first time you have
425
+ received notice of violation of this License (for any work) from that
426
+ copyright holder, and you cure the violation prior to 30 days after
427
+ your receipt of the notice.
428
+
429
+ Termination of your rights under this section does not terminate the
430
+ licenses of parties who have received copies or rights from you under
431
+ this License. If your rights have been terminated and not permanently
432
+ reinstated, you do not qualify to receive new licenses for the same
433
+ material under section 10.
434
+
435
+ 9. Acceptance Not Required for Having Copies.
436
+
437
+ You are not required to accept this License in order to receive or
438
+ run a copy of the Program. Ancillary propagation of a covered work
439
+ occurring solely as a consequence of using peer-to-peer transmission
440
+ to receive a copy likewise does not require acceptance. However,
441
+ nothing other than this License grants you permission to propagate or
442
+ modify any covered work. These actions infringe copyright if you do
443
+ not accept this License. Therefore, by modifying or propagating a
444
+ covered work, you indicate your acceptance of this License to do so.
445
+
446
+ 10. Automatic Licensing of Downstream Recipients.
447
+
448
+ Each time you convey a covered work, the recipient automatically
449
+ receives a license from the original licensors, to run, modify and
450
+ propagate that work, subject to this License. You are not responsible
451
+ for enforcing compliance by third parties with this License.
452
+
453
+ An "entity transaction" is a transaction transferring control of an
454
+ organization, or substantially all assets of one, or subdividing an
455
+ organization, or merging organizations. If propagation of a covered
456
+ work results from an entity transaction, each party to that
457
+ transaction who receives a copy of the work also receives whatever
458
+ licenses to the work the party's predecessor in interest had or could
459
+ give under the previous paragraph, plus a right to possession of the
460
+ Corresponding Source of the work from the predecessor in interest, if
461
+ the predecessor has it or can get it with reasonable efforts.
462
+
463
+ You may not impose any further restrictions on the exercise of the
464
+ rights granted or affirmed under this License. For example, you may
465
+ not impose a license fee, royalty, or other charge for exercise of
466
+ rights granted under this License, and you may not initiate litigation
467
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
468
+ any patent claim is infringed by making, using, selling, offering for
469
+ sale, or importing the Program or any portion of it.
470
+
471
+ 11. Patents.
472
+
473
+ A "contributor" is a copyright holder who authorizes use under this
474
+ License of the Program or a work on which the Program is based. The
475
+ work thus licensed is called the contributor's "contributor version".
476
+
477
+ A contributor's "essential patent claims" are all patent claims
478
+ owned or controlled by the contributor, whether already acquired or
479
+ hereafter acquired, that would be infringed by some manner, permitted
480
+ by this License, of making, using, or selling its contributor version,
481
+ but do not include claims that would be infringed only as a
482
+ consequence of further modification of the contributor version. For
483
+ purposes of this definition, "control" includes the right to grant
484
+ patent sublicenses in a manner consistent with the requirements of
485
+ this License.
486
+
487
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
488
+ patent license under the contributor's essential patent claims, to
489
+ make, use, sell, offer for sale, import and otherwise run, modify and
490
+ propagate the contents of its contributor version.
491
+
492
+ In the following three paragraphs, a "patent license" is any express
493
+ agreement or commitment, however denominated, not to enforce a patent
494
+ (such as an express permission to practice a patent or covenant not to
495
+ sue for patent infringement). To "grant" such a patent license to a
496
+ party means to make such an agreement or commitment not to enforce a
497
+ patent against the party.
498
+
499
+ If you convey a covered work, knowingly relying on a patent license,
500
+ and the Corresponding Source of the work is not available for anyone
501
+ to copy, free of charge and under the terms of this License, through a
502
+ publicly available network server or other readily accessible means,
503
+ then you must either (1) cause the Corresponding Source to be so
504
+ available, or (2) arrange to deprive yourself of the benefit of the
505
+ patent license for this particular work, or (3) arrange, in a manner
506
+ consistent with the requirements of this License, to extend the patent
507
+ license to downstream recipients. "Knowingly relying" means you have
508
+ actual knowledge that, but for the patent license, your conveying the
509
+ covered work in a country, or your recipient's use of the covered work
510
+ in a country, would infringe one or more identifiable patents in that
511
+ country that you have reason to believe are valid.
512
+
513
+ If, pursuant to or in connection with a single transaction or
514
+ arrangement, you convey, or propagate by procuring conveyance of, a
515
+ covered work, and grant a patent license to some of the parties
516
+ receiving the covered work authorizing them to use, propagate, modify
517
+ or convey a specific copy of the covered work, then the patent license
518
+ you grant is automatically extended to all recipients of the covered
519
+ work and works based on it.
520
+
521
+ A patent license is "discriminatory" if it does not include within
522
+ the scope of its coverage, prohibits the exercise of, or is
523
+ conditioned on the non-exercise of one or more of the rights that are
524
+ specifically granted under this License. You may not convey a covered
525
+ work if you are a party to an arrangement with a third party that is
526
+ in the business of distributing software, under which you make payment
527
+ to the third party based on the extent of your activity of conveying
528
+ the work, and under which the third party grants, to any of the
529
+ parties who would receive the covered work from you, a discriminatory
530
+ patent license (a) in connection with copies of the covered work
531
+ conveyed by you (or copies made from those copies), or (b) primarily
532
+ for and in connection with specific products or compilations that
533
+ contain the covered work, unless you entered into that arrangement,
534
+ or that patent license was granted, prior to 28 March 2007.
535
+
536
+ Nothing in this License shall be construed as excluding or limiting
537
+ any implied license or other defenses to infringement that may
538
+ otherwise be available to you under applicable patent law.
539
+
540
+ 12. No Surrender of Others' Freedom.
541
+
542
+ If conditions are imposed on you (whether by court order, agreement or
543
+ otherwise) that contradict the conditions of this License, they do not
544
+ excuse you from the conditions of this License. If you cannot convey a
545
+ covered work so as to satisfy simultaneously your obligations under this
546
+ License and any other pertinent obligations, then as a consequence you may
547
+ not convey it at all. For example, if you agree to terms that obligate you
548
+ to collect a royalty for further conveying from those to whom you convey
549
+ the Program, the only way you could satisfy both those terms and this
550
+ License would be to refrain entirely from conveying the Program.
551
+
552
+ 13. Use with the GNU Affero General Public License.
553
+
554
+ Notwithstanding any other provision of this License, you have
555
+ permission to link or combine any covered work with a work licensed
556
+ under version 3 of the GNU Affero General Public License into a single
557
+ combined work, and to convey the resulting work. The terms of this
558
+ License will continue to apply to the part which is the covered work,
559
+ but the special requirements of the GNU Affero General Public License,
560
+ section 13, concerning interaction through a network will apply to the
561
+ combination as such.
562
+
563
+ 14. Revised Versions of this License.
564
+
565
+ The Free Software Foundation may publish revised and/or new versions of
566
+ the GNU General Public License from time to time. Such new versions will
567
+ be similar in spirit to the present version, but may differ in detail to
568
+ address new problems or concerns.
569
+
570
+ Each version is given a distinguishing version number. If the
571
+ Program specifies that a certain numbered version of the GNU General
572
+ Public License "or any later version" applies to it, you have the
573
+ option of following the terms and conditions either of that numbered
574
+ version or of any later version published by the Free Software
575
+ Foundation. If the Program does not specify a version number of the
576
+ GNU General Public License, you may choose any version ever published
577
+ by the Free Software Foundation.
578
+
579
+ If the Program specifies that a proxy can decide which future
580
+ versions of the GNU General Public License can be used, that proxy's
581
+ public statement of acceptance of a version permanently authorizes you
582
+ to choose that version for the Program.
583
+
584
+ Later license versions may give you additional or different
585
+ permissions. However, no additional obligations are imposed on any
586
+ author or copyright holder as a result of your choosing to follow a
587
+ later version.
588
+
589
+ 15. Disclaimer of Warranty.
590
+
591
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
+
600
+ 16. Limitation of Liability.
601
+
602
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610
+ SUCH DAMAGES.
611
+
612
+ 17. Interpretation of Sections 15 and 16.
613
+
614
+ If the disclaimer of warranty and limitation of liability provided
615
+ above cannot be given local legal effect according to their terms,
616
+ reviewing courts shall apply local law that most closely approximates
617
+ an absolute waiver of all civil liability in connection with the
618
+ Program, unless a warranty or assumption of liability accompanies a
619
+ copy of the Program in return for a fee.
620
+
621
+ END OF TERMS AND CONDITIONS
622
+
623
+ How to Apply These Terms to Your New Programs
624
+
625
+ If you develop a new program, and you want it to be of the greatest
626
+ possible use to the public, the best way to achieve this is to make it
627
+ free software which everyone can redistribute and change under these terms.
628
+
629
+ To do so, attach the following notices to the program. It is safest
630
+ to attach them to the start of each source file to most effectively
631
+ state the exclusion of warranty; and each file should have at least
632
+ the "copyright" line and a pointer to where the full notice is found.
633
+
634
+ <one line to give the program's name and a brief idea of what it does.>
635
+ Copyright (C) <year> <name of author>
636
+
637
+ This program is free software: you can redistribute it and/or modify
638
+ it under the terms of the GNU General Public License as published by
639
+ the Free Software Foundation, either version 3 of the License, or
640
+ (at your option) any later version.
641
+
642
+ This program is distributed in the hope that it will be useful,
643
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
644
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645
+ GNU General Public License for more details.
646
+
647
+ You should have received a copy of the GNU General Public License
648
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
649
+
650
+ Also add information on how to contact you by electronic and paper mail.
651
+
652
+ If the program does terminal interaction, make it output a short
653
+ notice like this when it starts in an interactive mode:
654
+
655
+ <program> Copyright (C) <year> <name of author>
656
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
+ This is free software, and you are welcome to redistribute it
658
+ under certain conditions; type `show c' for details.
659
+
660
+ The hypothetical commands `show w' and `show c' should show the appropriate
661
+ parts of the General Public License. Of course, your program's commands
662
+ might be different; for a GUI interface, you would use an "about box".
663
+
664
+ You should also get your employer (if you work as a programmer) or school,
665
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
666
+ For more information on this, and how to apply and follow the GNU GPL, see
667
+ <http://www.gnu.org/licenses/>.
668
+
669
+ The GNU General Public License does not permit incorporating your program
670
+ into proprietary programs. If your program is a subroutine library, you
671
+ may consider it more useful to permit linking proprietary applications with
672
+ the library. If this is what you want to do, use the GNU Lesser General
673
+ Public License instead of this License. But first, please read
674
+ <http://www.gnu.org/philosophy/why-not-lgpl.html>.
app/code/community/Siel/Acumulus/licentie-nl.pdf ADDED
Binary file
app/code/community/Siel/Acumulus/sql/acumulus_setup/install-4.0.0.php ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /** @var $this Siel_Acumulus_Model_Resource_Setup */
4
+ $installer = $this;
5
+ $installer->startSetup();
6
+ $table = $installer->getTableDefinition();
7
+ $installer->getConnection()->createTable($table);
8
+ $installer->endSetup();
app/code/community/Siel/Acumulus/sql/acumulus_setup/upgrade-3.4.4-4.0.0.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /** @var Mage_Core_Model_Resource $resource */
4
+ $resource = Mage::getSingleton('core/resource');
5
+ /** @var $this Siel_Acumulus_Model_Resource_Setup */
6
+ $installer = $this;
7
+ $installer->startSetup();
8
+
9
+ $connection = $installer->getConnection();
10
+
11
+ $tableName = $installer->getTable('acumulus/entry');
12
+ $oldTableName = $tableName . '_old';
13
+
14
+ // Rename current table.
15
+ $connection->renameTablesBatch(array(array('oldName' => $tableName, 'newName' => $oldTableName)));
16
+
17
+ // Create new table.
18
+ $table = $installer->getTableDefinition();
19
+ $connection->createTable($table);
20
+
21
+ // Copy data from old to new table.
22
+ // - Orders:
23
+ $insertOrders = <<<SQL
24
+ insert into $tableName
25
+ (entry_id, token, source_type, source_id, created, updated)
26
+ select entry_id, token, 'Order' as source_type, order_id as source_id, created, updated
27
+ from $oldTableName
28
+ where creditmemo_id is null;
29
+ SQL;
30
+ $connection->query($insertOrders);
31
+
32
+ // - Credit memos:
33
+ $insertCreditNotes = <<<SQL
34
+ insert into $tableName
35
+ (entry_id, token, source_type, source_id, created, updated)
36
+ select entry_id, token, 'CreditNote' as source_type, creditmemo_id as source_id, created, updated
37
+ from $oldTableName
38
+ where creditmemo_id is not null;
39
+ SQL;
40
+ $connection->query($insertCreditNotes);
41
+
42
+ // Delete old table.
43
+ $connection->dropTable($oldTableName);
44
+
45
+ $installer->endSetup();
app/etc/modules/Siel_Acumulus.xml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Siel_Acumulus>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ </Siel_Acumulus>
8
+ </modules>
9
+ </config>
package.xml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>Siel_Acumulus</name>
4
+ <version>4.3.0</version>
5
+ <stability>stable</stability>
6
+ <license uri="http://www.gnu.org/licenses/gpl-3.0.en.html">GNU v3</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>This addon connects your Magento store to the Dutch SIEL Acumulus online financial administration application.</summary>
10
+ <description>The Acumulus addon connects your Magento store to the Dutch SIEL Acumulus online financial administration application. It can add your invoices automatically or via a batch send form to your administration, saving you a lot of manual, error prone work.&#xD;
11
+ &#xD;
12
+ So, the Acumulus addon assumes that:&#xD;
13
+ &lt;ul&gt;&#xD;
14
+ &lt;li&gt;You have an account with SIEL Acumulus (see https://www.siel.nl/acumulus/ or https://www.siel.nl/acumulus/koppelingen/webwinkels/Magento/).&lt;/li&gt;&#xD;
15
+ &lt;/ul&gt;&#xD;
16
+ If not, this addon is useless and will not do anything.&#xD;
17
+ &#xD;
18
+ The Acumulus addon:&#xD;
19
+ &lt;ul&gt;&#xD;
20
+ &lt;li&gt;Reacts to order status changes via events.&lt;/li&gt;&#xD;
21
+ &lt;li&gt;Does have 2 admin screens: a settings and a batch send screen.&lt;/li&gt;&#xD;
22
+ &lt;li&gt;Does not in any way interfere with the front-end UI.&lt;/li&gt;&#xD;
23
+ &lt;/ul&gt;&#xD;
24
+ </description>
25
+ <notes>See changelog.txt</notes>
26
+ <authors><author><name>SIEL Acumulus</name><user>MAG002648740</user><email>erwin@burorader.com</email></author></authors>
27
+ <date>2016-03-09</date>
28
+ <time>11:54:17</time>
29
+ <contents><target name="magecommunity"><dir name="Siel"><dir name="Acumulus"><dir name="Block"><dir name="Adminhtml"><dir name="Form"><file name="Form.php" hash="c32631f5ff4d1ec72630a896a14cefe1"/></dir><file name="Form.php" hash="3447b44cb78547aa3ccaf90be8913c71"/></dir></dir><dir name="Helper"><file name="CompiledMagentoAutoLoader.php" hash="624856afa5ffe2b86918fddab1276ac7"/><file name="Data.php" hash="e282053f1509f5729b8313bff4afa04f"/></dir><dir name="Model"><file name="Entry.php" hash="ecbee1f8734af44b0af5ab1096a7610c"/><dir name="Order"><file name="Observer.php" hash="db7a0075c168e2305b5daa0ec586f12f"/></dir><dir name="Resource"><dir name="Entry"><file name="Collection.php" hash="9ce6644ce43795f616d64fde860da859"/></dir><file name="Entry.php" hash="2cfcf403ae511d0a6855a0c4d21cd494"/><file name="Setup.php" hash="0963aabe90a080b59f2493ff80e4545e"/></dir></dir><file name="changelog.txt" hash="e64229f676be66ebf14469f88fa94d42"/><dir name="controllers"><dir name="Adminhtml"><file name="AcumulusController.php" hash="6da0889cd922b109f3fc84bb2662f580"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="fb59e7cfc09fc06e5fba3288f1b518d6"/><file name="config.xml" hash="53f5d4abf723209ac6131808bf435a05"/></dir><file name="events.txt" hash="63aae80ed8a7672a47f3b07742117afc"/><file name="leesmij.txt" hash="ac1c4c7f2877db37d171b1656c79b497"/><dir name="libraries"><dir name="Siel"><dir name="Acumulus"><dir name="Helpers"><file name="Countries.php" hash="1a628daefc8e80557952aa231e3fa668"/><file name="Form.php" hash="418e59a0db0844480a9cc41e9c757aa9"/><file name="FormRenderer.php" hash="70d9d54039220bf5f354a50037a653ee"/><file name="Log.php" hash="02c5333d527d27d719fc71085c84a2fa"/><file name="MailTranslations.php" hash="6e0a782bc1fc0f0b586eaaa16054b241"/><file name="Mailer.php" hash="56886e4151bfa6d8039dbd48d8af9504"/><file name="Number.php" hash="fac66430236c1e8418304e0f6b6e17f9"/><file name="Requirements.php" hash="d201fa16a5b9737f94eeca5408fb2240"/><file name="TranslationCollection.php" hash="53b41c859c88211beebbb2b07aeee244"/><file name="Translator.php" hash="5b6a88b816e8c2eb43e91f94a0205803"/><file name="TranslatorInterface.php" hash="80ef668eff7043dfa84ce7aadb662957"/></dir><dir name="Invoice"><file name="Completor.php" hash="555a6af4e4b1ec8adc5e0f47b9c15a7c"/><file name="CompletorInvoiceLines.php" hash="4874fb9642f0f335a417e594e2d00f2c"/><dir name="CompletorStrategy"><file name="ApplySameVatRate.php" hash="96c3686b9108e5235c850b6b3f9650c5"/><file name="Fail.php" hash="2305fca1967bb77f8e160999872c3fd2"/><file name="SplitKnownDiscountLine.php" hash="b5dae2d3fc008a462f20591bf976563d"/><file name="SplitLine.php" hash="690d0e402e11456472cadc94c9c973c1"/><file name="SplitNonMatchingLine.php" hash="b18b4673a0de5a51e0d50c2f9d7c946b"/><file name="TryAllVatRatePermutations.php" hash="9bcd8ac1b92c8e7980e4e298a9d966ae"/></dir><file name="CompletorStrategyBase.php" hash="6f8d30ce3c9f0f8a4aea412dcf3f0ee4"/><file name="CompletorStrategyLines.php" hash="8ccdc9cd8659b1a920789938f11d31fc"/><file name="ConfigInterface.php" hash="d2a123c1c3c6422f229a06af6b3c2c7e"/><file name="Creator.php" hash="253965039fa20fc233df96b4f20f6f41"/><file name="Source.php" hash="c391c921b901e5e73ce1fc281c66e2e7"/><file name="Translations.php" hash="9e15b2027260c4ba4be1a94958a43653"/></dir><dir name="Joomla"><dir name="Helpers"><file name="FormRenderer.php" hash="c16967c0ec8df7f58da4404bba717700"/><file name="Log.php" hash="e96e7c54ad8ef6c17d9f83540e3fbcef"/><file name="Mailer.php" hash="a89cf15036c8bac4c06beb6ed1b823dd"/></dir><dir name="HikaShop"><dir name="Invoice"><file name="Creator.php" hash="16b90dc15febc660a9c39c93ea0a78b5"/><file name="Source.php" hash="0dc47cf33c0797028844a61107ae7315"/></dir><dir name="Shop"><file name="ConfigForm.php" hash="fbf71cdcfdce070f1242cf196ceaf276"/><file name="InvoiceManager.php" hash="54dbeef00c8f5af79e8594ab6f3684f0"/></dir></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="cdaa80c74132cd6b2513cf8ed7d86aba"/><file name="BatchForm.php" hash="908f7a909a6c7b4ac548fd8cadcae65c"/><file name="ConfigForm.php" hash="8c2d4a210dfc4ebdf0d5d10eafcf1e34"/><file name="ConfigStore.php" hash="78a755e624249e56f4ff8409524c51f4"/><file name="InvoiceManager.php" hash="976eab3a9248be0ab4e7c57bd2e3bc8c"/></dir><dir name="VirtueMart"><dir name="Invoice"><file name="Creator.php" hash="5f6c8dda2a65a453b5bb31d29564cfdd"/><file name="Source.php" hash="245cb6f0fee76f458e6fc5b46d78b260"/></dir><dir name="Shop"><file name="ConfigForm.php" hash="0f132abe45dbd3eedf3d8209796e8e18"/><file name="InvoiceManager.php" hash="6cbbcf81daf69e730f2971f7b6b40ad0"/></dir></dir></dir><dir name="Magento"><dir name="Helpers"><file name="FormMapper.php" hash="93b91014706253a6359534f2153c4d4f"/><file name="Log.php" hash="68869b7309c9b6c47baf55cbdac8f8ad"/><file name="Mailer.php" hash="edd028edf6493e83f7a524f3aeebe73f"/></dir><dir name="Invoice"><file name="Completor.php" hash="e89536c6267a92c82291d396f8fdc22e"/><file name="Creator.php" hash="b9480ac0b43e5d6b8489fbf82fd63c1e"/><file name="Source.php" hash="74e763d74e8200325f2dd48e0c643fa3"/></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="f523ed9a0882c3bf059859db983efc18"/><file name="BatchForm.php" hash="fd3464cbbf7b9ec43616f7e56c4dcca8"/><file name="ConfigForm.php" hash="a8360d982c1782bdb02beabf28b95c60"/><file name="ConfigStore.php" hash="291955dd1b79c49c49a768d4a0e9f831"/><file name="InvoiceManager.php" hash="fffa04ae2f7e7837b830f8c7f58be348"/></dir></dir><dir name="OpenCart"><dir name="Helpers"><file name="Log.php" hash="a2f540ce3f0204a390af036b85d2acc6"/><file name="Mailer.php" hash="461938b3047e3eaf475c803cd833b42d"/><file name="OcHelper.php" hash="a0495d4b96b1fa5ea7ef978913344fad"/><file name="Registry.php" hash="f5a8dd23e676db98764dc655a78a4e2b"/></dir><dir name="Invoice"><file name="Creator.php" hash="ba901e1c41a574465f13ccda4ab6ae59"/><file name="Source.php" hash="480af34a70dfe6464c3b5b2db3f7ca7a"/></dir><dir name="OpenCart1"><dir name="Helpers"><file name="FormRenderer.php" hash="6f25870323a83c63680e1d93a7810d08"/><file name="OcHelper.php" hash="83cf3d748f464b604779a01e9a560757"/></dir><dir name="Shop"><file name="InvoiceManager.php" hash="54084e3864a8429e6091da4b4613dde9"/></dir></dir><dir name="OpenCart2"><dir name="Helpers"><file name="FormRenderer.php" hash="ca896df3d4a50a0106d6584a59688ae8"/><file name="OcHelper.php" hash="b247c71a0ea4ce11a8734a871b643ea0"/></dir><dir name="Shop"><file name="InvoiceManager.php" hash="a709321e4e27f6e543f9cb7cc1659cfd"/></dir></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="c4c4af48b934400c5f66b953d1fa7f81"/><file name="BatchForm.php" hash="cec6d8648a979781f93ba4fe0ecc79ce"/><file name="ConfigForm.php" hash="e4d13008c8a90505fbb7672ce0787ef5"/><file name="ConfigStore.php" hash="ad7b986b89ae919246dccf5d9785945e"/><file name="InvoiceManager.php" hash="82552ce4cca460d0247e5a673d729067"/></dir></dir><dir name="PrestaShop"><dir name="Helpers"><file name="FormMapper.php" hash="223658340e4ca00f23447a97f38f1820"/><file name="Log.php" hash="ccf831f432ba4b04f444dccf2b59c4a8"/><file name="Mailer.php" hash="ed6ff4f7cca198f5e5f021846c297899"/><dir name="mails"><dir name="en"><file name="message.html" hash="d41d8cd98f00b204e9800998ecf8427e"/><file name="message.txt" hash="d41d8cd98f00b204e9800998ecf8427e"/></dir><dir name="nl"><file name="message.html" hash="d41d8cd98f00b204e9800998ecf8427e"/><file name="message.txt" hash="d41d8cd98f00b204e9800998ecf8427e"/></dir><file name=".htaccess" hash="dd31bb1af314c32c34de8e7aff34b429"/></dir></dir><dir name="Invoice"><file name="Creator.php" hash="909a5aad31aa79b8682bd1bbdbd8729e"/><file name="Source.php" hash="0dfec460efd64c7ec38921e883a35de7"/></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="a82393e075ce69fe8abb0ffa04859d6b"/><file name="BatchForm.php" hash="a43bf0208d854f4c8bc14209c7ec2b02"/><file name="ConfigForm.php" hash="655381f1efee167c5c1ca48e1f3b78bb"/><file name="ConfigStore.php" hash="13bbe959be7f3b0be5d785238773e5a7"/><file name="InvoiceManager.php" hash="391ee86c5ac3493e99323e28cd106b3e"/></dir></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="014fea20e7c734fb09ea18a65fcf1bd6"/><file name="BatchForm.php" hash="b6ad9775a7d259c295904b3ff1d24f0d"/><file name="BatchFormTranslations.php" hash="eb8743fee58e6eac1222acf455f558eb"/><file name="BatchTranslations.php" hash="b2de3631e360132c5d4588edb6cd3364"/><file name="Config.php" hash="cd7882bb87cb92354ce95b90a4c4b116"/><file name="ConfigForm.php" hash="934aea51f141aafb13c0424a30e84b04"/><file name="ConfigFormTranslations.php" hash="776eb4af036a7eb710b327c8342848c5"/><file name="ConfigInterface.php" hash="7b30cccd5b91638c68bc9a7dba2d145e"/><file name="ConfigStore.php" hash="564843ddf549fc61634d4d204b9097e9"/><file name="ConfigStoreInterface.php" hash="ce6236dd8c1946e5ffb297093d97cfee"/><file name="ConfirmUninstallForm.php" hash="d8ae9595709065611fa60d52b97ae2bc"/><file name="ConfirmUninstallFormTranslations.php" hash="4004dfe1556b16e762764eb428d6868e"/><file name="InjectorInterface.php" hash="9cd5c574245a42a3a673e27b3f8d7aff"/><file name="InvoiceManager.php" hash="c3d79c08df8808a3da4ffb6632405521"/><file name="ModuleTranslations.php" hash="716fcb217c79abd10f83b6ebfe792486"/></dir><dir name="Web"><file name="Communicator.php" hash="16554201ae92f77f5381688c31e86081"/><file name="CommunicatorLocal.php" hash="3533284d6cbfb7c3faa461a85b4a9950"/><file name="ConfigInterface.php" hash="0ab7e4a24453e76e7101349296ef6773"/><file name="Service.php" hash="7a90175d5e66cbf9af4f6acf89854669"/><file name="Translations.php" hash="62c8f3bca470df9f16ee5f4b74780b80"/></dir><dir name="WooCommerce"><dir name="Helpers"><file name="FormMapper.php" hash="cdcac8bd8d44ce98d79dc519d3cfc4df"/><file name="FormRenderer.php" hash="38e84aa367100f9f8b2a7aede99fcd48"/><file name="Log.php" hash="3e7ca3fa5058c2566864702d0f4e9ea5"/><file name="Mailer.php" hash="8cf8c987fbdcc4bef6fec18f3adc348d"/></dir><dir name="Invoice"><file name="Creator.php" hash="a8a100aaa00a350d979bcd40f7252a73"/><file name="Source.php" hash="fcb716ad46e89c1607c6d2cdf37ead80"/></dir><dir name="Shop"><file name="AcumulusEntryModel.php" hash="051668471829b3c9ad45968325687a1c"/><file name="ConfigForm.php" hash="d678232c11285f291102913580939d27"/><file name="ConfigStore.php" hash="ea8df8c02347c512ea816b1d440950c7"/><file name="InvoiceManager.php" hash="dd4062e568b1c8765f5ff5f736ed332f"/></dir></dir></dir><file name="leesmij.txt" hash="f88ca59b1a830d1982c775ced8c07abf"/><file name="psr4.php" hash="a2e43be4d375ec396fc574bf13c19458"/></dir></dir><file name="license.txt" hash="d32239bcb673463ab874e80d47fae504"/><file name="licentie-nl.pdf" hash="1af9588b31002745c526e46feb911c6d"/><dir name="sql"><dir name="acumulus_setup"><file name="install-4.0.0.php" hash="06c933104dbd0f0d7fd6370506c046f2"/><file name="upgrade-3.4.4-4.0.0.php" hash="7a0d1a018ba31770657554876982783d"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Siel_Acumulus.xml" hash="8b4935569d0eae94852fa9f3af8ed166"/></dir></target></contents>
30
+ <compatible/>
31
+ <dependencies><required><php><min>5.3.0</min><max>5.7.0</max></php><extension><name>SimpleXML</name><min/><max/></extension><extension><name>curl</name><min/><max/></extension><extension><name>dom</name><min/><max/></extension></required></dependencies>
32
+ </package>