From 68c3752313c52c9796d8006b165c4d217d7c2e1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Thu, 9 Jul 2015 16:23:37 +0200
Subject: [PATCH] accounting: prevent user from creating balance transaction
 while previous one is still beeing reindexed

---
 ...ountingPeriod_createBalanceTransaction.xml | 386 +++++++++---------
 .../scripts/checkTransactionsState.xml        |   4 +
 product/ERP5/tests/testAccounting.py          |  34 ++
 3 files changed, 227 insertions(+), 197 deletions(-)

diff --git a/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingPeriod_createBalanceTransaction.xml b/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingPeriod_createBalanceTransaction.xml
index 20b166eac5..ac6ced327c 100644
--- a/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingPeriod_createBalanceTransaction.xml
+++ b/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingPeriod_createBalanceTransaction.xml
@@ -67,8 +67,8 @@ def roundCurrency(value, resource_relative_url):\n
   qty_precision = precision_cache[resource_relative_url]\n
   return round(value, qty_precision)\n
 \n
+# This tag is checked in accounting period workflow\n
 activity_tag = \'BalanceTransactionCreation\'\n
-activate_kw=dict(tag=activity_tag)\n
 \n
 at_date = context.getStopDate()\n
 assert at_date\n
@@ -109,7 +109,7 @@ def getDependantSectionList(group, main_section):\n
       section_list.extend(getDependantSectionList(subgroup, main_section))\n
 \n
   return section_list\n
-      \n
+\n
 group_value = section.getGroupValue()\n
 section_list = [section]\n
 if group_value is not None:\n
@@ -117,7 +117,6 @@ if group_value is not None:\n
 \n
 def createBalanceTransaction(section):\n
   return portal.accounting_module.newContent(\n
-                          activate_kw=activate_kw,\n
                           portal_type=\'Balance Transaction\',\n
                           start_date=(at_date + 1).earliestTime(),\n
                           title=context.getTitle() or Base_translateString(\'Balance Transaction\'),\n
@@ -125,221 +124,214 @@ def createBalanceTransaction(section):\n
                           resource=section_currency,\n
                           causality_value=context)\n
 \n
-for section in section_list:\n
-  section_uid = section.getUid()\n
-  balance_transaction = None\n
-\n
-  group_by_node_node_category_list = []\n
-  group_by_mirror_section_node_category_list = []\n
-  group_by_payment_node_category_list = []\n
-  profit_and_loss_node_category_list = []\n
-\n
-  node_category_list = portal.portal_categories\\\n
-              .account_type.getCategoryChildValueList()\n
-  for node_category in node_category_list:\n
-    node_category_url = node_category.getRelativeUrl()\n
-    if node_category_url in (\n
-        \'account_type/asset/cash/bank\',):\n
-      group_by_payment_node_category_list.append(node_category_url)\n
-    elif node_category_url in (\n
-        \'account_type/asset/receivable\',\n
-        \'account_type/liability/payable\'):\n
-      group_by_mirror_section_node_category_list.append(node_category_url)\n
-    elif node_category.isMemberOf(\'account_type/income\') or \\\n
-         node_category.isMemberOf(\'account_type/expense\'):\n
-      profit_and_loss_node_category_list.append(node_category_url)\n
-    else:\n
-      group_by_node_node_category_list.append(node_category_url)\n
-\n
-  getInventoryList = portal.portal_simulation.getInventoryList\n
-\n
-  inventory_param_dict = dict(section_uid=section_uid,\n
-                              simulation_state=(\'delivered\',),\n
-                              precision=section_currency_precision,\n
-                              portal_type=portal.getPortalAccountingMovementTypeList(),\n
-                              at_date=at_date.latestTime(),)\n
-  \n
-  # Calculate the sum of profit and loss accounts balances for that period.\n
-  # This must match the difference between assets, liability and equity accounts.\n
-  profit_and_loss_accounts_balance = portal.portal_simulation.getInventoryAssetPrice(\n
-    from_date=context.getStartDate(),\n
-    node_category_strict_membership=profit_and_loss_node_category_list,\n
-    **inventory_param_dict)\n
-  selected_profit_and_loss_account_balance = portal.portal_simulation.getInventoryAssetPrice(\n
-    node=profit_and_loss_account,\n
-    resource=section_currency,\n
-    **inventory_param_dict)\n
+with context.defaultActivateParameterDict({\'tag\': activity_tag}, placeless=True):\n
+  for section in section_list:\n
+    section_uid = section.getUid()\n
+    balance_transaction = None\n
+\n
+    group_by_node_node_category_list = []\n
+    group_by_mirror_section_node_category_list = []\n
+    group_by_payment_node_category_list = []\n
+    profit_and_loss_node_category_list = []\n
+\n
+    node_category_list = portal.portal_categories\\\n
+                .account_type.getCategoryChildValueList()\n
+    for node_category in node_category_list:\n
+      node_category_url = node_category.getRelativeUrl()\n
+      if node_category_url in (\n
+          \'account_type/asset/cash/bank\',):\n
+        group_by_payment_node_category_list.append(node_category_url)\n
+      elif node_category_url in (\n
+          \'account_type/asset/receivable\',\n
+          \'account_type/liability/payable\'):\n
+        group_by_mirror_section_node_category_list.append(node_category_url)\n
+      elif node_category.isMemberOf(\'account_type/income\') or \\\n
+           node_category.isMemberOf(\'account_type/expense\'):\n
+        profit_and_loss_node_category_list.append(node_category_url)\n
+      else:\n
+        group_by_node_node_category_list.append(node_category_url)\n
+\n
+    getInventoryList = portal.portal_simulation.getInventoryList\n
+\n
+    inventory_param_dict = dict(section_uid=section_uid,\n
+                                simulation_state=(\'delivered\',),\n
+                                precision=section_currency_precision,\n
+                                portal_type=portal.getPortalAccountingMovementTypeList(),\n
+                                at_date=at_date.latestTime(),)\n
   \n
-  section_currency_uid = context.getParentValue().getPriceCurrencyUid()\n
-\n
-  profit_and_loss_quantity = 0\n
-  line_count = 0\n
-\n
-  for inventory in getInventoryList(\n
-          node_category_strict_membership=group_by_node_node_category_list,\n
-          group_by_node=1,\n
-          group_by_resource=1,\n
-          **inventory_param_dict):\n
-    \n
-    total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
-    quantity = roundCurrency(inventory.total_quantity or 0,\n
-                             inventory.resource_relative_url)\n
-    \n
-    if not total_price and not quantity:\n
-      continue\n
-    \n
-    line_count += 1\n
-    if inventory.resource_uid != section_currency_uid:\n
+    # Calculate the sum of profit and loss accounts balances for that period.\n
+    # This must match the difference between assets, liability and equity accounts.\n
+    profit_and_loss_accounts_balance = portal.portal_simulation.getInventoryAssetPrice(\n
+      from_date=context.getStartDate(),\n
+      node_category_strict_membership=profit_and_loss_node_category_list,\n
+      **inventory_param_dict)\n
+    selected_profit_and_loss_account_balance = portal.portal_simulation.getInventoryAssetPrice(\n
+      node=profit_and_loss_account,\n
+      resource=section_currency,\n
+      **inventory_param_dict)\n
+\n
+    section_currency_uid = context.getParentValue().getPriceCurrencyUid()\n
+\n
+    profit_and_loss_quantity = 0\n
+    line_count = 0\n
+\n
+    for inventory in getInventoryList(\n
+            node_category_strict_membership=group_by_node_node_category_list,\n
+            group_by_node=1,\n
+            group_by_resource=1,\n
+            **inventory_param_dict):\n
+\n
+      total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
+      quantity = roundCurrency(inventory.total_quantity or 0,\n
+                               inventory.resource_relative_url)\n
+\n
+      if not total_price and not quantity:\n
+        continue\n
+\n
+      line_count += 1\n
+      if inventory.resource_uid != section_currency_uid:\n
+        profit_and_loss_quantity += total_price\n
+\n
+        if balance_transaction is None:\n
+          balance_transaction = createBalanceTransaction(section)\n
+        balance_transaction.newContent(\n
+            id=\'%03d\' % line_count,\n
+            portal_type=\'Balance Transaction Line\',\n
+            destination=inventory.node_relative_url,\n
+            resource=inventory.resource_relative_url,\n
+            quantity=quantity,\n
+            destination_total_asset_price=total_price)\n
+      else:\n
+        if total_price != quantity:\n
+          # If this fail for you, your accounting doesn\'t use currencies with\n
+          # consistency\n
+          raise ValueError(\'Different price: %s != %s \' % (\n
+                            total_price, quantity))\n
+\n
+        if inventory.node_relative_url != profit_and_loss_account:\n
+          profit_and_loss_quantity += total_price\n
+          if balance_transaction is None:\n
+            balance_transaction = createBalanceTransaction(section)\n
+          balance_transaction.newContent(\n
+            id=\'%03d\' % line_count,\n
+            portal_type=\'Balance Transaction Line\',\n
+            destination=inventory.node_relative_url,\n
+            quantity=total_price)\n
+\n
+\n
+    for inventory in getInventoryList(\n
+            node_category_strict_membership=group_by_mirror_section_node_category_list,\n
+            group_by_node=1,\n
+            group_by_mirror_section=1,\n
+            group_by_resource=1,\n
+            **inventory_param_dict):\n
+\n
+      total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
+      quantity = roundCurrency(inventory.total_quantity or 0,\n
+                               inventory.resource_relative_url)\n
+\n
+      if not total_price and not quantity:\n
+        continue\n
       profit_and_loss_quantity += total_price\n
-      \n
-      if balance_transaction is None:\n
-        balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
+      line_count += 1\n
+\n
+      if inventory.resource_uid != section_currency_uid:\n
+        if balance_transaction is None:\n
+          balance_transaction = createBalanceTransaction(section)\n
+        balance_transaction.newContent(\n
           id=\'%03d\' % line_count,\n
           portal_type=\'Balance Transaction Line\',\n
-          activate_kw=activate_kw,\n
           destination=inventory.node_relative_url,\n
+          source_section_uid=inventory.mirror_section_uid,\n
           resource=inventory.resource_relative_url,\n
           quantity=quantity,\n
           destination_total_asset_price=total_price)\n
-    else:\n
-      if total_price != quantity:\n
-        # If this fail for you, your accounting doesn\'t use currencies with\n
-        # consistency\n
-        raise ValueError(\'Different price: %s != %s \' % (\n
-                          total_price, quantity))\n
-      \n
-      if inventory.node_relative_url != profit_and_loss_account:\n
-        profit_and_loss_quantity += total_price\n
+      else:\n
+        if total_price != quantity:\n
+          raise ValueError(\'Different price: %s != %s \' % (\n
+                            total_price, quantity))\n
         if balance_transaction is None:\n
           balance_transaction = createBalanceTransaction(section)\n
         balance_transaction.newContent(\n
           id=\'%03d\' % line_count,\n
           portal_type=\'Balance Transaction Line\',\n
-          activate_kw=activate_kw,\n
           destination=inventory.node_relative_url,\n
+          source_section_uid=inventory.mirror_section_uid,\n
           quantity=total_price)\n
 \n
-      \n
-  for inventory in getInventoryList(\n
-          node_category_strict_membership=group_by_mirror_section_node_category_list,\n
-          group_by_node=1,\n
-          group_by_mirror_section=1,\n
-          group_by_resource=1,\n
-          **inventory_param_dict):\n
-\n
-    total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
-    quantity = roundCurrency(inventory.total_quantity or 0,\n
-                             inventory.resource_relative_url)\n
-    \n
-    if not total_price and not quantity:\n
-      continue\n
-    profit_and_loss_quantity += total_price\n
-    line_count += 1\n
 \n
-    if inventory.resource_uid != section_currency_uid:\n
-      if balance_transaction is None:\n
-        balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
-        id=\'%03d\' % line_count,\n
-        portal_type=\'Balance Transaction Line\',\n
-        activate_kw=activate_kw,\n
-        destination=inventory.node_relative_url,\n
-        source_section_uid=inventory.mirror_section_uid,\n
-        resource=inventory.resource_relative_url,\n
-        quantity=quantity,\n
-        destination_total_asset_price=total_price)\n
-    else:\n
-      if total_price != quantity:\n
-        raise ValueError(\'Different price: %s != %s \' % (\n
-                          total_price, quantity))\n
-      if balance_transaction is None:\n
+    for inventory in getInventoryList(\n
+            node_category_strict_membership=group_by_payment_node_category_list,\n
+            group_by_node=1,\n
+            group_by_payment=1,\n
+            group_by_resource=1,\n
+            **inventory_param_dict):\n
+\n
+      total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
+      quantity = roundCurrency(inventory.total_quantity or 0,\n
+                               inventory.resource_relative_url)\n
+\n
+      if not total_price and not quantity:\n
+        continue\n
+      profit_and_loss_quantity += total_price\n
+\n
+      line_count += 1\n
+\n
+      if inventory.resource_uid != section_currency_uid:\n
+        if balance_transaction is None:\n
+          balance_transaction = createBalanceTransaction(section)\n
+        balance_transaction.newContent(\n
+          id=\'%03d\' % line_count,\n
+          portal_type=\'Balance Transaction Line\',\n
+          destination=inventory.node_relative_url,\n
+          resource=inventory.resource_relative_url,\n
+          quantity=quantity,\n
+          destination_payment_uid=inventory.payment_uid,\n
+          destination_total_asset_price=total_price)\n
+      else:\n
+        if total_price != quantity:\n
+          raise ValueError(\'Different price: %s != %s \' % (\n
+                            total_price, quantity))\n
+        if balance_transaction is None:\n
+          balance_transaction = createBalanceTransaction(section)\n
+        balance_transaction.newContent(\n
+          id=\'%03d\' % line_count,\n
+          portal_type=\'Balance Transaction Line\',\n
+          destination=inventory.node_relative_url,\n
+          destination_payment_uid=inventory.payment_uid,\n
+          quantity=total_price)\n
+\n
+    if balance_transaction is None:\n
+      # we did not have any transaction for this section\n
+\n
+      # One possible corner case is that we have only transactions that brings\n
+      # the balance of all balance sheets accounts to 0. In this case we want to\n
+      # create a balance transaction that notes that the current balance of profit\n
+      # and loss account is 0, so that the delta gets indexed. \n
+      if profit_and_loss_accounts_balance:\n
         balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
-        id=\'%03d\' % line_count,\n
-        portal_type=\'Balance Transaction Line\',\n
-        activate_kw=activate_kw,\n
-        destination=inventory.node_relative_url,\n
-        source_section_uid=inventory.mirror_section_uid,\n
-        quantity=total_price)\n
-\n
-\n
-  for inventory in getInventoryList(\n
-          node_category_strict_membership=group_by_payment_node_category_list,\n
-          group_by_node=1,\n
-          group_by_payment=1,\n
-          group_by_resource=1,\n
-          **inventory_param_dict):\n
-\n
-    total_price = roundCurrency(inventory.total_price or 0, section_currency)\n
-    quantity = roundCurrency(inventory.total_quantity or 0,\n
-                             inventory.resource_relative_url)\n
-    \n
-    if not total_price and not quantity:\n
+        balance_transaction.newContent(\n
+              portal_type=\'Balance Transaction Line\',\n
+              destination=profit_and_loss_account,\n
+              quantity=0)\n
+        balance_transaction.stop()\n
+        balance_transaction.deliver()\n
       continue\n
-    profit_and_loss_quantity += total_price\n
-    \n
-    line_count += 1\n
 \n
-    if inventory.resource_uid != section_currency_uid:\n
-      if balance_transaction is None:\n
-        balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
-        id=\'%03d\' % line_count,\n
-        portal_type=\'Balance Transaction Line\',\n
-        activate_kw=activate_kw,\n
-        destination=inventory.node_relative_url,\n
-        resource=inventory.resource_relative_url,\n
-        quantity=quantity,\n
-        destination_payment_uid=inventory.payment_uid,\n
-        destination_total_asset_price=total_price)\n
-    else:\n
-      if total_price != quantity:\n
-        raise ValueError(\'Different price: %s != %s \' % (\n
-                          total_price, quantity))\n
-      if balance_transaction is None:\n
-        balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
-        id=\'%03d\' % line_count,\n
-        portal_type=\'Balance Transaction Line\',\n
-        activate_kw=activate_kw,\n
-        destination=inventory.node_relative_url,\n
-        destination_payment_uid=inventory.payment_uid,\n
-        quantity=total_price)\n
-\n
-  if balance_transaction is None:\n
-    # we did not have any transaction for this section\n
-    \n
-    # One possible corner case is that we have only transactions that brings\n
-    # the balance of all balance sheets accounts to 0. In this case we want to\n
-    # create a balance transaction that notes that the current balance of profit\n
-    # and loss account is 0, so that the delta gets indexed. \n
-    if profit_and_loss_accounts_balance:\n
-      balance_transaction = createBalanceTransaction(section)\n
-      balance_transaction.newContent(\n
-            activate_kw=activate_kw,\n
-            portal_type=\'Balance Transaction Line\',\n
-            destination=profit_and_loss_account,\n
-            quantity=0)\n
-      balance_transaction.stop()\n
-      balance_transaction.deliver()\n
-    continue\n
-\n
-  assert roundCurrency(profit_and_loss_accounts_balance, section_currency) == roundCurrency(\n
-       - roundCurrency(selected_profit_and_loss_account_balance, section_currency)\n
-       - roundCurrency(profit_and_loss_quantity, section_currency), section_currency)\n
-  \n
-  # add a final line for p&l\n
-  balance_transaction.newContent(\n
-            id=\'%03d\' % (line_count + 1),\n
-            activate_kw=activate_kw,\n
-            portal_type=\'Balance Transaction Line\',\n
-            destination=profit_and_loss_account,\n
-            quantity=-profit_and_loss_quantity)\n
+    assert roundCurrency(profit_and_loss_accounts_balance, section_currency) == roundCurrency(\n
+         - roundCurrency(selected_profit_and_loss_account_balance, section_currency)\n
+         - roundCurrency(profit_and_loss_quantity, section_currency), section_currency)\n
+\n
+    # add a final line for p&l\n
+    balance_transaction.newContent(\n
+              id=\'%03d\' % (line_count + 1),\n
+              portal_type=\'Balance Transaction Line\',\n
+              destination=profit_and_loss_account,\n
+              quantity=-profit_and_loss_quantity)\n
 \n
-  # and go to delivered state directly (the user is not supposed to edit this document)\n
-  balance_transaction.stop()\n
-  balance_transaction.deliver()\n
+    # and go to delivered state directly (the user is not supposed to edit this document)\n
+    balance_transaction.stop()\n
+    balance_transaction.deliver()\n
 \n
 # make sure this Accounting Period has an activity pending during the indexing\n
 # of the balance transaction.\n
diff --git a/bt5/erp5_accounting/WorkflowTemplateItem/portal_workflow/accounting_period_workflow/scripts/checkTransactionsState.xml b/bt5/erp5_accounting/WorkflowTemplateItem/portal_workflow/accounting_period_workflow/scripts/checkTransactionsState.xml
index 70a26de875..ff3a3a8f5f 100644
--- a/bt5/erp5_accounting/WorkflowTemplateItem/portal_workflow/accounting_period_workflow/scripts/checkTransactionsState.xml
+++ b/bt5/erp5_accounting/WorkflowTemplateItem/portal_workflow/accounting_period_workflow/scripts/checkTransactionsState.xml
@@ -60,6 +60,10 @@ portal = period.getPortalObject()\n
 \n
 period.Base_checkConsistency()\n
 \n
+# This tag is used in AccountingPeriod_createBalanceTransaction\n
+if portal.portal_activities.countMessageWithTag(\'BalanceTransactionCreation\'):\n
+  raise ValidationFailed(translateString("Balance transaction creation already in progress. Please try again later."))\n
+\n
 valid_simulation_state_list = [\'cancelled\', \'delivered\', \'deleted\', \'rejected\']\n
 all_state_list = [x[1] for x in\n
   portal.Base_getTranslatedWorkflowStateItemList(wf_id=\'accounting_workflow\')]\n
diff --git a/product/ERP5/tests/testAccounting.py b/product/ERP5/tests/testAccounting.py
index 19122c4a67..f94fb3c3f0 100644
--- a/product/ERP5/tests/testAccounting.py
+++ b/product/ERP5/tests/testAccounting.py
@@ -2499,6 +2499,40 @@ class TestClosingPeriod(AccountingTestCase):
                               section_uid=self.section.getUid(),
                               node_uid=self.account_module.receivable.getUid()))
 
+  def test_ParrallelClosingRefused(self):
+    organisation_module = self.organisation_module
+    stool = self.portal.portal_simulation
+    period = self.section.newContent(portal_type='Accounting Period')
+    period.setStartDate(DateTime(2006, 1, 1))
+    period.setStopDate(DateTime(2006, 12, 31))
+    period.start()
+    period2 = self.section.newContent(portal_type='Accounting Period')
+    period2.setStartDate(DateTime(2007, 1, 1))
+    period2.setStopDate(DateTime(2007, 12, 31))
+    period2.start()
+
+    pl = self.portal.account_module.newContent(
+              portal_type='Account',
+              account_type='equity')
+
+    transaction1 = self._makeOne(
+        start_date=DateTime(2006, 1, 1),
+        destination_section_value=organisation_module.client_1,
+        portal_type='Sale Invoice Transaction',
+        simulation_state='delivered',
+        lines=(dict(source_value=self.account_module.goods_sales,
+                    source_credit=100),
+               dict(source_value=self.account_module.receivable,
+                    source_debit=100)))
+
+    self.portal.portal_workflow.doActionFor(
+           period, 'stop_action',
+           profit_and_loss_account=pl.getRelativeUrl())
+
+    self.assertRaises(ValidationFailed,
+          self.getPortal().portal_workflow.doActionFor,
+          period2, 'stop_action' )
+
 
 
 class TestAccountingExport(AccountingTestCase):
-- 
2.30.9