From 0a7ad12fbdda821e4375a4f4c7e65909581d1f6b Mon Sep 17 00:00:00 2001
From: s j <sj@1729.be>
Date: Sun, 31 Jan 2021 20:22:43 +0100
Subject: [PATCH 1/2] i5528 add vote_weight property to share_type, export at
 date of total vote_weight per shareholder

---
 modules/sr_general_meeting/composer.json      |  13 ++
 .../sr_general_meeting.info.yml               |   5 +
 .../sr_general_meeting.links.menu.yml         |   6 +
 .../sr_general_meeting.module                 |  62 ++++++
 .../sr_general_meeting.routing.yml            |   7 +
 .../sr_general_meeting.services.yml           |   7 +
 .../src/Form/ExportGeneralMeetingVoteForm.php |  77 ++++++++
 .../src/GeneralMeetingService.php             | 187 ++++++++++++++++++
 .../src/GeneralMeetingServiceInterface.php    |  11 ++
 .../templates/sr-general-meeting.html.twig    |   1 +
 .../tests/src/Functional/LoadTest.php         |  46 +++++
 11 files changed, 422 insertions(+)
 create mode 100644 modules/sr_general_meeting/composer.json
 create mode 100644 modules/sr_general_meeting/sr_general_meeting.info.yml
 create mode 100644 modules/sr_general_meeting/sr_general_meeting.links.menu.yml
 create mode 100644 modules/sr_general_meeting/sr_general_meeting.module
 create mode 100644 modules/sr_general_meeting/sr_general_meeting.routing.yml
 create mode 100644 modules/sr_general_meeting/sr_general_meeting.services.yml
 create mode 100644 modules/sr_general_meeting/src/Form/ExportGeneralMeetingVoteForm.php
 create mode 100644 modules/sr_general_meeting/src/GeneralMeetingService.php
 create mode 100644 modules/sr_general_meeting/src/GeneralMeetingServiceInterface.php
 create mode 100644 modules/sr_general_meeting/templates/sr-general-meeting.html.twig
 create mode 100644 modules/sr_general_meeting/tests/src/Functional/LoadTest.php

diff --git a/modules/sr_general_meeting/composer.json b/modules/sr_general_meeting/composer.json
new file mode 100644
index 00000000..45685618
--- /dev/null
+++ b/modules/sr_general_meeting/composer.json
@@ -0,0 +1,13 @@
+{
+    "name": "sr_general_meeting",
+    "type": "drupal-module",
+    "description": "Shareholder Register General Meeting",
+    "keywords": [
+    ],
+    "homepage": "https://www.drupal.org/project/sr_general_meeting",
+    "minimum-stability": "dev",
+    "support": {
+        "issues": "https://www.drupal.org/project/issues/sr_general_meeting",
+        "source": "http://cgit.drupalcode.org/sr_general_meeting"
+    }
+}
diff --git a/modules/sr_general_meeting/sr_general_meeting.info.yml b/modules/sr_general_meeting/sr_general_meeting.info.yml
new file mode 100644
index 00000000..57b94023
--- /dev/null
+++ b/modules/sr_general_meeting/sr_general_meeting.info.yml
@@ -0,0 +1,5 @@
+name: 'Shareholder Register General Meeting'
+type: module
+description: 'Shareholder Register General Meeting'
+core: 8.x
+package: 'startx'
diff --git a/modules/sr_general_meeting/sr_general_meeting.links.menu.yml b/modules/sr_general_meeting/sr_general_meeting.links.menu.yml
new file mode 100644
index 00000000..2ee489ca
--- /dev/null
+++ b/modules/sr_general_meeting/sr_general_meeting.links.menu.yml
@@ -0,0 +1,6 @@
+
+sr_general_meeting_votes:
+  title: 'General Meeting votes'
+  description: 'Export voting weights for general meeting'
+  route_name: sr_general_meeting.export_general_meeting_vote_form
+  parent: system.admin.shareholder_register.tools
diff --git a/modules/sr_general_meeting/sr_general_meeting.module b/modules/sr_general_meeting/sr_general_meeting.module
new file mode 100644
index 00000000..64dbd8ff
--- /dev/null
+++ b/modules/sr_general_meeting/sr_general_meeting.module
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains sr_general_meeting.module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Implements hook_help().
+ */
+function sr_general_meeting_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the sr_general_meeting module.
+    case 'help.page.sr_general_meeting':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Shareholder Register General Meeting') . '</p>';
+      return $output;
+
+    default:
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function sr_general_meeting_theme() {
+  return [
+    'sr_general_meeting' => [
+      'render element' => 'children',
+    ],
+  ];
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function sr_general_meeting_entity_base_field_info(EntityTypeInterface $entity_type) {
+  if ($entity_type->id() == 'share_type') {
+    $fields = array();
+    $fields['vote_weight'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('Voting weight'))
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'number',
+        'weight' => 50,
+      ])
+      ->setDisplayOptions('form', [
+        'label' => 'above',
+        'type' => 'number',
+        'weight' => 50,
+      ])
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayConfigurable('form', TRUE);
+
+    return $fields;
+  }
+}
diff --git a/modules/sr_general_meeting/sr_general_meeting.routing.yml b/modules/sr_general_meeting/sr_general_meeting.routing.yml
new file mode 100644
index 00000000..2caf2334
--- /dev/null
+++ b/modules/sr_general_meeting/sr_general_meeting.routing.yml
@@ -0,0 +1,7 @@
+sr_general_meeting.export_general_meeting_vote_form:
+  path: '/admin/shareholder_register/tools/export_general_meeting_vote'
+  defaults:
+    _form: '\Drupal\sr_general_meeting\Form\ExportGeneralMeetingVoteForm'
+    _title: 'Export General Meeting Voting Weigth'
+  requirements:
+    _access: 'TRUE'
diff --git a/modules/sr_general_meeting/sr_general_meeting.services.yml b/modules/sr_general_meeting/sr_general_meeting.services.yml
new file mode 100644
index 00000000..7da359b3
--- /dev/null
+++ b/modules/sr_general_meeting/sr_general_meeting.services.yml
@@ -0,0 +1,7 @@
+services:
+  logger.channel.sr_general_meeting:
+    parent: logger.channel_base
+    arguments: ['sr_general_meeting']
+  sr_general_meeting.default:
+    class: Drupal\sr_general_meeting\GeneralMeetingService
+    arguments: []
diff --git a/modules/sr_general_meeting/src/Form/ExportGeneralMeetingVoteForm.php b/modules/sr_general_meeting/src/Form/ExportGeneralMeetingVoteForm.php
new file mode 100644
index 00000000..8502f321
--- /dev/null
+++ b/modules/sr_general_meeting/src/Form/ExportGeneralMeetingVoteForm.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\sr_general_meeting\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Class ExportGeneralMeetingVoteForm.
+ */
+class ExportGeneralMeetingVoteForm extends FormBase {
+
+  /**
+   * Drupal\sr_general_meeting\GeneralMeetingServiceInterface definition.
+   *
+   * @var \Drupal\sr_general_meeting\GeneralMeetingServiceInterface
+   */
+  protected $srGeneralMeetingDefault;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $instance = parent::create($container);
+    $instance->srGeneralMeetingDefault = $container->get('sr_general_meeting.default');
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'export_general_meeting_vote_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['date'] = [
+      '#type' => 'date',
+      '#title' => $this->t('Date'),
+      '#weight' => '0',
+      '#required' => TRUE,
+      '#default_value' => date('Y-m-d'),
+    ];
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Submit'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    foreach ($form_state->getValues() as $key => $value) {
+      // @TODO: Validate fields.
+    }
+    parent::validateForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = [
+      'date' => $form_state->getValue('date')
+    ];
+    $batch = $this->srGeneralMeetingDefault->getGeneralMeetingBatch($config);
+    batch_set($batch);
+  }
+
+}
diff --git a/modules/sr_general_meeting/src/GeneralMeetingService.php b/modules/sr_general_meeting/src/GeneralMeetingService.php
new file mode 100644
index 00000000..94c32e6e
--- /dev/null
+++ b/modules/sr_general_meeting/src/GeneralMeetingService.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\sr_general_meeting;
+
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+
+use Drupal\shareholder_register\Entity\Shareholder;
+
+/**
+ * Class GeneralMeetingService.
+ */
+class GeneralMeetingService implements GeneralMeetingServiceInterface {
+
+
+  /**
+   * Constructs a new GeneralMeetingService object.
+   */
+  public function __construct() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGeneralMeetingBatch($config) {
+    $batch = array(
+      'title' => t('Computing general meeting voting weight'),
+      'operations' => [],
+    );
+    $batch['operations'][] = [
+      '\Drupal\sr_general_meeting\GeneralMeetingService::initGeneralMeetingBatch',
+      [$config],
+    ];
+    $batch['operations'][] = [
+      '\Drupal\sr_general_meeting\GeneralMeetingService::computeGeneralMeetingBatch',
+      [$config],
+    ];
+    $batch['operations'][] = [
+      '\Drupal\sr_general_meeting\GeneralMeetingService::writeGeneralMeetingBatch',
+      [$config],
+    ];
+    $batch['finished'] = '\Drupal\sr_general_meeting\GeneralMeetingService::finishGeneralMeetingBatch';
+
+    return $batch;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function initGeneralMeetingBatch($general_meeting_config, &$context) {
+    $context['results']['details'] = [];
+    $context['results']['totals'] = [];
+    $context['results']['config'] = $general_meeting_config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function computeGeneralMeetingBatch($general_meeting_config, &$context) {
+    $formatter = \Drupal::service('shareholder_register.formatter');
+
+    if (!isset($context['sandbox']['shareholder_ids'])) {
+      $shareholder_ids = \Drupal::entityQuery('shareholder')
+        ->condition('state', 'valid')
+        ->execute();
+
+      $context['sandbox']['shareholder_ids'] = $shareholder_ids;
+      $context['sandbox']['shareholder_ids_count'] = count($context['sandbox']['shareholder_ids']);
+      $context['results']['totals'] = [];
+    }
+
+    for ($i = 0; $i < 100; $i++) {
+      if ($shareholder_id = array_pop($context['sandbox']['shareholder_ids'])) {
+        $shareholder = Shareholder::load($shareholder_id);
+
+        $shares = $shareholder->getSharesAtDate($general_meeting_config['date']);
+
+        if (!count($shares)) {
+          continue;
+        }
+        $weight = 0;
+        foreach ($shares as $share) {
+          $weight += $share->getShareType()->get('vote_weight')->value;
+        }
+
+        $context['results']['totals'][$shareholder->id()] = [
+          'shareholder_id' => $shareholder->id(),
+          'shares' => $formatter->sharesToRanges($shares),
+          'weight' => $weight,
+        ];
+
+      }
+    }
+
+    if ($context['sandbox']['shareholder_ids_count'] > 0) {
+      $context['finished'] = ($context['sandbox']['shareholder_ids_count'] - count($context['sandbox']['shareholder_ids'])) / $context['sandbox']['shareholder_ids_count'];
+    }
+    else {
+      $context['finished'] = 1;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function writeGeneralMeetingBatch($general_meeting_config, &$context) {
+    if (!isset($context['results']['output'])) {
+      $output = file_save_data('', 'private://general_meeting.xlsx');
+      $output->setTemporary();
+      $output->save();
+
+      $context['results']['output'] = $output;
+
+      $spreadsheet = new Spreadsheet();
+
+      // Set document properties.
+      $spreadsheet->getProperties()->setCreator('DSR')
+        ->setLastModifiedBy(t('DSR'))
+        ->setTitle(t('DSR General Meeting Voting Weight'))
+        ->setSubject(t('DSR General Meeting Voting Weight'))
+        ->setDescription(t('DSR General Meeting Voting Weight'))
+        ->setCategory(t('DSR General Meeting'));
+    }
+    else {
+      $reader = new Xlsx();
+      $reader->setReadDataOnly(TRUE);
+      $spreadsheet = $reader->load(drupal_realpath($context['results']['output']->getFileUri()));
+    }
+
+    if (!isset($context['sandbox']['to_write'])) {
+      $config = $context['results']['config'];
+      $sheet = $spreadsheet->setActiveSheetIndex(0);
+
+      $sheet->setCellValue("A1", t('General Meeting Voting Weight for date @date',
+      [
+        '@date' => $general_meeting_config['date'],
+      ]));
+      $sheet->setCellValue("A3", t('Shareholder Number'));
+      $sheet->setCellValue("B3", t('Shareholder Name'));
+      $sheet->setCellValue("C3", t('Shares'));
+      $sheet->setCellValue("D3", t('Voting weight'));
+
+      $context['sandbox']['row'] = 5;
+      $context['sandbox']['to_write'] = $context['results']['totals'];
+    }
+    else {
+      $sheet = $spreadsheet->getActiveSheet();
+    }
+
+    for ($i = 0; $i < 5000; $i++) {
+      if ($group = array_pop($context['sandbox']['to_write'])) {
+        $row = $context['sandbox']['row'];
+        $shareholder = Shareholder::load($group['shareholder_id']);
+
+        $sheet->setCellValue("A{$row}", $shareholder->getNumber());
+        $sheet->setCellValue("B{$row}", $shareholder->getName());
+        $sheet->setCellValue("C{$row}", $group['shares']);
+        $sheet->setCellValue("D{$row}", $group['weight']);
+
+        $context['sandbox']['row']++;
+      }
+    }
+
+    $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+    $writer->setPreCalculateFormulas(FALSE);
+    $writer->save(drupal_realpath($context['results']['output']->getFileUri()));
+    $context['results']['output']->save();
+    $context['finished'] = (count($context['results']['totals']) - count($context['sandbox']['to_write'])) / count($context['results']['totals']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function finishGeneralMeetingBatch($success, $results, $operations) {
+    drupal_set_message(t(
+        'Exported General Meeting Voting Weight  download: @url',
+        [
+          '@url' => Link::fromTextAndUrl('general_meeting.xlsx', Url::fromUri(file_create_url($results['output']->getFileUri())))->toString(),
+        ]
+    ));
+  }
+
+}
diff --git a/modules/sr_general_meeting/src/GeneralMeetingServiceInterface.php b/modules/sr_general_meeting/src/GeneralMeetingServiceInterface.php
new file mode 100644
index 00000000..d8aedefb
--- /dev/null
+++ b/modules/sr_general_meeting/src/GeneralMeetingServiceInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\sr_general_meeting;
+
+/**
+ * Interface GeneralMeetingServiceInterface.
+ */
+interface GeneralMeetingServiceInterface {
+
+
+}
diff --git a/modules/sr_general_meeting/templates/sr-general-meeting.html.twig b/modules/sr_general_meeting/templates/sr-general-meeting.html.twig
new file mode 100644
index 00000000..95722bd3
--- /dev/null
+++ b/modules/sr_general_meeting/templates/sr-general-meeting.html.twig
@@ -0,0 +1 @@
+<!-- Add you custom twig html here -->
diff --git a/modules/sr_general_meeting/tests/src/Functional/LoadTest.php b/modules/sr_general_meeting/tests/src/Functional/LoadTest.php
new file mode 100644
index 00000000..7a007819
--- /dev/null
+++ b/modules/sr_general_meeting/tests/src/Functional/LoadTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\sr_general_meeting\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Simple test to ensure that main page loads with module enabled.
+ *
+ * @group sr_general_meeting
+ */
+class LoadTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['sr_general_meeting'];
+
+  /**
+   * A user with permission to administer site configuration.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->user = $this->drupalCreateUser(['administer site configuration']);
+    $this->drupalLogin($this->user);
+  }
+
+  /**
+   * Tests that the home page loads with a 200 response.
+   */
+  public function testLoad() {
+    $this->drupalGet(Url::fromRoute('<front>'));
+    $this->assertSession()->statusCodeEquals(200);
+  }
+
+}
-- 
GitLab


From 26c0adacae539142cdfccabeba45f220017e665c Mon Sep 17 00:00:00 2001
From: s j <sj@1729.be>
Date: Mon, 1 Feb 2021 10:12:45 +0100
Subject: [PATCH 2/2] i5528 add details to export of voting weights

---
 .../src/GeneralMeetingService.php             | 109 ++++++++++++++++++
 1 file changed, 109 insertions(+)

diff --git a/modules/sr_general_meeting/src/GeneralMeetingService.php b/modules/sr_general_meeting/src/GeneralMeetingService.php
index 94c32e6e..8cb18db6 100644
--- a/modules/sr_general_meeting/src/GeneralMeetingService.php
+++ b/modules/sr_general_meeting/src/GeneralMeetingService.php
@@ -43,6 +43,10 @@ class GeneralMeetingService implements GeneralMeetingServiceInterface {
       '\Drupal\sr_general_meeting\GeneralMeetingService::writeGeneralMeetingBatch',
       [$config],
     ];
+    $batch['operations'][] = [
+      '\Drupal\sr_general_meeting\GeneralMeetingService::writeGeneralMeetingDetailsBatch',
+      [$config],
+    ];
     $batch['finished'] = '\Drupal\sr_general_meeting\GeneralMeetingService::finishGeneralMeetingBatch';
 
     return $batch;
@@ -57,6 +61,16 @@ class GeneralMeetingService implements GeneralMeetingServiceInterface {
     $context['results']['config'] = $general_meeting_config;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function getShareHash($share) {
+    return [
+      'hash' => $share->getShareType()->id(),
+      'label' => $share->getShareType()->label(),
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -71,6 +85,7 @@ class GeneralMeetingService implements GeneralMeetingServiceInterface {
       $context['sandbox']['shareholder_ids'] = $shareholder_ids;
       $context['sandbox']['shareholder_ids_count'] = count($context['sandbox']['shareholder_ids']);
       $context['results']['totals'] = [];
+      $context['results']['details'] = [];
     }
 
     for ($i = 0; $i < 100; $i++) {
@@ -83,8 +98,29 @@ class GeneralMeetingService implements GeneralMeetingServiceInterface {
           continue;
         }
         $weight = 0;
+        $shares_by_hash = [];
         foreach ($shares as $share) {
+          $hash_detail = static::getShareHash($share);
           $weight += $share->getShareType()->get('vote_weight')->value;
+
+          if (!isset($context['results']['details'][$shareholder->id()])) {
+            $context['results']['details'][$shareholder->id()] = [];
+          }
+          if (!isset($context['results']['details'][$shareholder->id()][$hash_detail['hash']])) {
+            $context['results']['details'][$shareholder->id()][$hash_detail['hash']] = [
+              'shareholder_id' => $shareholder->id(),
+              'share_numbers' => [],
+              'label' => $hash_detail['label'],
+              'weight' => 0,
+            ];
+            $shares_by_hash[$hash_detail['hash']] = [];
+          }
+          $context['results']['details'][$shareholder->id()][$hash_detail['hash']]['weight'] += $share->getShareType()->get('vote_weight')->value;
+          $shares_by_hash[$hash_detail['hash']][] = $share;
+        }
+
+        foreach ($shares_by_hash as $hash => $shares_for_hash) {
+          $context['results']['details'][$shareholder->id()][$hash]['shares'] = $formatter->sharesToRanges($shares_for_hash);
         }
 
         $context['results']['totals'][$shareholder->id()] = [
@@ -172,6 +208,79 @@ class GeneralMeetingService implements GeneralMeetingServiceInterface {
     $context['finished'] = (count($context['results']['totals']) - count($context['sandbox']['to_write'])) / count($context['results']['totals']);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function writeGeneralMeetingDetailsBatch($general_meeting_config, &$context) {
+    if (!isset($context['results']['output'])) {
+      $output = file_save_data('', 'private://general_meeting.xlsx');
+      $output->setTemporary();
+      $output->save();
+
+      $context['results']['output'] = $output;
+
+      $spreadsheet = new Spreadsheet();
+
+      // Set document properties.
+      $spreadsheet->getProperties()->setCreator('DSR')
+        ->setLastModifiedBy(t('DSR'))
+        ->setTitle(t('DSR General Meeting Voting Weight'))
+        ->setSubject(t('DSR General Meeting Voting Weight'))
+        ->setDescription(t('DSR General Meeting Voting Weight'))
+        ->setCategory(t('DSR General Meeting'));
+    }
+    else {
+      $reader = new Xlsx();
+      $reader->setReadDataOnly(TRUE);
+      $spreadsheet = $reader->load(drupal_realpath($context['results']['output']->getFileUri()));
+    }
+
+    if (!isset($context['sandbox']['to_write'])) {
+      $config = $context['results']['config'];
+      $spreadsheet->createSheet();
+      $sheet = $spreadsheet->setActiveSheetIndex(1);
+
+      $sheet->setCellValue("A1", t('General Meeting Voting Weight for date @date',
+      [
+        '@date' => $general_meeting_config['date'],
+      ]));
+      $sheet->setCellValue("A3", t('Shareholder Number'));
+      $sheet->setCellValue("B3", t('Shareholder Name'));
+      $sheet->setCellValue("C3", t('Type'));
+      $sheet->setCellValue("D3", t('Shares'));
+      $sheet->setCellValue("E3", t('Voting weight'));
+
+      $context['sandbox']['row'] = 5;
+      $context['sandbox']['to_write'] = $context['results']['details'];
+    }
+    else {
+      $sheet = $spreadsheet->getActiveSheet();
+    }
+
+    for ($i = 0; $i < 5000; $i++) {
+      if ($shareholder_group = array_pop($context['sandbox']['to_write'])) {
+        foreach ($shareholder_group as $group) {
+          $row = $context['sandbox']['row'];
+          $shareholder = Shareholder::load($group['shareholder_id']);
+
+          $sheet->setCellValue("A{$row}", $shareholder->getNumber());
+          $sheet->setCellValue("B{$row}", $shareholder->getName());
+          $sheet->setCellValue("C{$row}", $group['label']);
+          $sheet->setCellValue("D{$row}", $group['shares']);
+          $sheet->setCellValue("E{$row}", $group['weight']);
+
+          $context['sandbox']['row']++;
+        }
+      }
+    }
+
+    $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+    $writer->setPreCalculateFormulas(FALSE);
+    $writer->save(drupal_realpath($context['results']['output']->getFileUri()));
+    $context['results']['output']->save();
+    $context['finished'] = (count($context['results']['totals']) - count($context['sandbox']['to_write'])) / count($context['results']['totals']);
+  }
+
   /**
    * {@inheritdoc}
    */
-- 
GitLab