link product sku & stock to Google spreadsheet!

Created on 17 November 2021, almost 3 years ago
Updated 2 October 2024, about 1 month ago

Problem/Motivation

Hi, as the e-commerce is getting bigger in size everyday, we need a common place to manage our product stock for different platform or website. As Drupal supports multi vendor facility, our vendor is selling his product to many websites- amazon, alibaba and also on my site. Each time manually update the stock for different website, is hard and impossible to be accurate. So, if we can export product stock for each sku, and sync with the spreadsheet, it will be a great move! We can get so many vendors and the dopshipping business can go boom with this feature. But I don't have any idea how tough it can be... specially to make it work out of the box!

Steps to reproduce

Proposed resolution

Export and import back won't be helpful much; we need online sync all the time in real time

may be a sub-module with the help of rss feed can work?

Thanks for reading and thinking :)
May be I''m asking for too much.....but it will be a great move

✨ Feature request
Status

Active

Component

Code

Created by

πŸ‡¬πŸ‡§United Kingdom sharif.tanveer

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡ΊπŸ‡ΈUnited States rsnyd

    @sharif.tanveer or anyone else finding this,

    Try to follow this article: Reading and Writing Google Sheets in PHP.

    Also, here's is my custom Drush command that only reads from a Google sheet:

    
    namespace Drupal\custom_module\Drush\Commands;
    
    use Drupal\commerce_stock_local\Event\LocalStockTransactionEvent;
    use Drupal\commerce_stock_local\Event\LocalStockTransactionEvents;
    use Drupal\commerce_stock\StockTransactionsInterface;
    use Drupal\Core\Database\Connection;
    use Drupal\Core\Entity\EntityTypeManager;
    use Drupal\Core\File\FileSystemInterface;
    use Drush\Attributes as CLI;
    use Drush\Commands\DrushCommands;
    use GuzzleHttp\ClientInterface;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    /**
     * Drush commands for Google.
     */
    class GoogleCommands extends DrushCommands {
    
      /**
       * Name for module's configuration object.
       */
      const SETTINGS = 'custom_module.settings';
    
      /**
       * The entity type manager.
       *
       * @var \Drupal\Core\Entity\EntityTypeManager
       */
      protected $entityTypeManager;
    
      /**
       * The Guzzle client for http requests.
       *
       * @var \GuzzleHttp\ClientInterface
       */
      protected $httpClient;
    
      /**
       * Default database connection.
       *
       * @var \Drupal\Core\Database\Connection
       */
      protected $connection;
    
      /**
       * The file system service.
       */
      protected FileSystemInterface $fileSystem;
    
      /**
       * TaxjarCommands constructor.
       *
       * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
       *   The entity_type.manager service.
       * @param \GuzzleHttp\ClientInterface $http_client
       *   The module_handler service.
       * @param \Drupal\Core\Database\Connection $connection
       *   Default database connection.
       * @param \Drupal\Core\File\FileUrlGenerator $file_url_generator
       *   For getting the url of images.
       */
      public function __construct(EntityTypeManager $entity_type_manager, ClientInterface $http_client, Connection $connection, FileSystemInterface $fileSystem) {
        parent::__construct();
        $this->entityTypeManager = $entity_type_manager;
        $this->httpClient = $http_client;
        $this->connection = $connection;
        $this->fileSystem = $fileSystem;
      }
    
      /**
       * {@inheritdoc}
       */
      public static function create(ContainerInterface $container) {
        return new static(
          $container->get('entity_type.manager'),
          $container->get('http_client'),
          $container->get('database'),
          $container->get('file_system'),
        );
      }
    
      /**
       * Sync inventory with a specific Google Sheet.
       *
       * @validate-module-enabled commerce_stock_product
       *
       * @command google:syncInventory
       * @aliases google:syncInventory
       */
      #[CLI\Command(name: 'google:syncInventory', aliases: ['google-syncInventory'])]
      #[CLI\Usage(name: 'google:syncInventory google-syncInventory', description: 'Sync inventory for the last 1 day')]
      public function syncInventory() {
        $messages = [];
        // configure the Google Client
        //https://www.nidup.io/blog/manipulate-google-sheets-in-php-with-api
        $client = new \Google_Client();
        $client->setApplicationName('Google Sheets API');
        $client->setScopes([\Google_Service_Sheets::SPREADSHEETS]);
        $client->setAccessType('offline');
    
        $config = \Drupal::config(self::SETTINGS);
        $fid = $config->get('credentials_file')[0];
        $file = $this->entityTypeManager->getStorage('file')->load($fid);
        $credentials_path = $this->fileSystem->realpath('private://' . $file->getFilename());
    
        $client->setAuthConfig($credentials_path);
    
        // configure the Sheets Service
        $service = new \Google_Service_Sheets($client);
        $spreadsheetId = 'qwerttyik57684-0easdfsadfasdfsdd3sphUs0bLx8';
        $spreadsheet_url = $config->get('url');
        $path = parse_url($spreadsheet_url)['path'];
        $parts = explode('/', $path);
        foreach ($parts as $index => $part) {
          if ($part == 'd') {
            $spreadsheetId = $parts[$index + 1];
            break;
          }
        }
    
        // get all the rows of a sheet
        $range = 'Inventory Check!A:J'; // here we use the name of the Sheet to get all the rows
        $response = $service->spreadsheets_values->get($spreadsheetId, $range);
        $values = $response->getValues();
        $stockCheckerService = \Drupal::service('commerce_stock_product.product_local_stock_checker');
        $eventDispatcher = \Drupal::service('event_dispatcher');
        $database = \Drupal::database();
        $config_factory = \Drupal::service('config.factory');
        $stock_config = $config_factory->get('commerce_stock_local.transactions');
    
        //  Remove the top hidden row
        array_shift($values);
        //  Remove the "Orders counted to:" row
        array_shift($values);
    
        $headers = reset($values);
        $headers = array_map('strtolower', str_replace(' ', '', $headers));
        foreach ($values as $row) {
          $row = array_pad($row, count($headers), '');
          $row = array_combine($headers, $row);
          if (!array_key_exists('sku', $row)) {
            continue;
          }
          $products = $this->entityTypeManager->getStorage('commerce_product')->loadByProperties([
            'status' => TRUE,
            'field_sku_root' => $row['sku'],
          ]);
          /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
          $product = reset($products);
          if (!$product) {
            continue;
          }
          if ($product->hasField('field_stock_level')) {
            $stock_level = $product->get('field_stock_level')->first();
            ($stock_level)
              ? $available_stock = $stock_level->get('available_stock')->getValue()
              : $available_stock = 0;
          } else
            $available_stock = 0;
    
          if ((array_key_exists('availableinventory', $row)) && (is_numeric($row['availableinventory']))) {
    
            $metadata = [];
            $location_id = 1;
    
            // Get optional fields.
            $related_tid = $metadata['related_tid'] ?? NULL;
            $related_oid = $metadata['related_oid'] ?? NULL;
            $related_uid = $metadata['related_uid'] ?? NULL;
            $data = $metadata['data'] ?? NULL;
    
            // Create a transaction record.
            $field_values = [
              'entity_id' => $product->id(),
              'entity_type' => $product->getEntityTypeId(),
              'qty' => $row['availableinventory'],
              'location_id' => $location_id,
              'location_zone' => '',
              'unit_cost' => 0,
              'currency_code' => 'USD',
              'transaction_time' => time(),
              'transaction_type_id' => StockTransactionsInterface::STOCK_IN,
              'related_tid' => $related_tid,
              'related_oid' => $related_oid,
              'related_uid' => $related_uid,
              'data' => serialize($data),
            ];
            $event = new LocalStockTransactionEvent($this->entityTypeManager, $field_values);
    
            //  Since we basically reset the inventory every day, delete all previous transactions
            $database->delete('commerce_stock_transaction')
            ->condition('location_id', $location_id)
              ->condition('entity_id', $product->id())
              ->condition('entity_type', $product->getEntityTypeId())
              ->execute();
    
            // Create a STOCK_IN transaction
            $eventDispatcher->dispatch($event, LocalStockTransactionEvents::LOCAL_STOCK_TRANSACTION_CREATE);
            $insert = $database->insert('commerce_stock_transaction')
              ->fields(array_keys($field_values))
              ->values(array_values($field_values))->execute();
    
            $eventDispatcher->dispatch($event, LocalStockTransactionEvents::LOCAL_STOCK_TRANSACTION_INSERT);
    
            //  Update the field_stock_level attached to the product
            $existing = $database->select('commerce_product__field_stock_level', 'fsl')
              ->fields('fsl')
              ->condition('entity_id', $product->id())
              ->execute()->fetch();
            if ($existing) {
              $database->update('commerce_product__field_stock_level')
                ->fields([
                  'field_stock_level_value' => $row['availableinventory'],
                ])
                ->condition('entity_id', $product->id(), '=')
                ->execute();
            } else {
              $database->insert('commerce_product__field_stock_level')
                ->fields([
                  'bundle',
                  'deleted',
                  'entity_id',
                  'revision_id',
                  'langcode',
                  'delta',
                  'field_stock_level_value',
                ])
                ->values([
                  $product->bundle(),
                  0,
                  $product->id(),
                  $product->id(),
                  'en',
                  0,
                  $row['availableinventory'],
                ])
                ->execute();
            }
    
            // Find out if we have real-time aggregation turned on.
            $transactions_aggregation_mode = $stock_config->get('transactions_aggregation_mode');
            if ($transactions_aggregation_mode == 'real-time') {
              // Aggregate if we do.
              $locations = $this->entityTypeManager->getStorage('commerce_stock_location')->loadEnabled($product->getDefaultVariation());
    
              foreach ($locations as $location) {
    
                //  updateLocationStockLevel()
    
                $current_level = $stockCheckerService->getLocationStockLevel($location->getId(), $product->getDefaultVariation());
                $last_update = $current_level['last_transaction'];
                $latest_txn = $stockCheckerService->getLocationStockTransactionLatest($location->getId(), $product->getDefaultVariation());
                $latest_sum = $stockCheckerService->getLocationStockTransactionSum($location->getId(), $product->getDefaultVariation(), $last_update, $latest_txn);
                $new_level = $row['availableinventory'];
    
                // setLocationStockLevel()
    
                $existing = $database->select('commerce_stock_location_level', 'll')
                  ->fields('ll')
                  ->condition('location_id', $location->getId())
                  ->condition('entity_id', $product->id())
                  ->condition('entity_type', $product->getEntityTypeId())
                  ->execute()->fetch();
                if ($existing) {
                  $database->update('commerce_stock_location_level')
                    ->fields([
                      'qty' => $new_level,
                      'last_transaction_id' => $latest_txn,
                    ])
                    ->condition('location_id', $location->getId(), '=')
                    ->condition('entity_id', $product->id(), '=')
                    ->condition('entity_type', $product->getEntityTypeId())
                    ->execute();
                } else {
                  $database->insert('commerce_stock_location_level')
                    ->fields([
                      'location_id',
                      'entity_id',
                      'entity_type',
                      'qty',
                      'last_transaction_id',
                    ])
                    ->values([
                      $location->getId(),
                      $product->id(),
                      $product->getEntityTypeId(),
                      $new_level,
                      $latest_txn,
                    ])
                    ->execute();
                }
    
              }
            }
    
            $product->save();
    
          }
          $messages[] = $product->getTitle() . ' Inventory Updated. (' . $available_stock . ' to ' . $row['availableinventory'] . ')';
        }
        \Drupal::logger('custom_module')->info(implode("<br/>", $messages));
      }
    
    }
    
    
    
Production build 0.71.5 2024