194 lines
5.8 KiB
Python
194 lines
5.8 KiB
Python
|
# -*- coding: utf-8; -*-
|
||
|
################################################################################
|
||
|
#
|
||
|
# Rattail -- Retail Software Framework
|
||
|
# Copyright © 2010-2021 Lance Edgar
|
||
|
#
|
||
|
# This file is part of Rattail.
|
||
|
#
|
||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||
|
# terms of the GNU General Public License as published by the Free Software
|
||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||
|
# version.
|
||
|
#
|
||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||
|
# details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License along with
|
||
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||
|
#
|
||
|
################################################################################
|
||
|
"""
|
||
|
WooCommerce model importers
|
||
|
"""
|
||
|
|
||
|
from woocommerce import API as WooAPI
|
||
|
|
||
|
from rattail import importing
|
||
|
|
||
|
|
||
|
class ToWooCommerce(importing.Importer):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class ProductImporter(ToWooCommerce):
|
||
|
"""
|
||
|
WooCommerce product data importer
|
||
|
"""
|
||
|
model_name = 'Product'
|
||
|
key = 'sku'
|
||
|
supported_fields = [
|
||
|
'id',
|
||
|
'name',
|
||
|
'slug',
|
||
|
'permalink',
|
||
|
'date_created',
|
||
|
'date_created_gmt',
|
||
|
'date_modified',
|
||
|
'date_modified_gmt',
|
||
|
'type',
|
||
|
'status',
|
||
|
'featured',
|
||
|
'catalog_visibility',
|
||
|
'description',
|
||
|
'short_description',
|
||
|
'sku',
|
||
|
'price',
|
||
|
'regular_price',
|
||
|
'sale_price',
|
||
|
'date_on_sale_from',
|
||
|
'date_on_sale_from_gmt',
|
||
|
'date_on_sale_to',
|
||
|
'date_on_sale_to_gmt',
|
||
|
'price_html',
|
||
|
'on_sale',
|
||
|
'purchasable',
|
||
|
'total_sales',
|
||
|
'tax_status',
|
||
|
'tax_class',
|
||
|
'manage_stock',
|
||
|
'stock_quantity',
|
||
|
'stock_status',
|
||
|
'backorders',
|
||
|
'backorders_allowed',
|
||
|
'backordered',
|
||
|
'sold_individually',
|
||
|
'weight',
|
||
|
'reviews_allowed',
|
||
|
'parent_id',
|
||
|
'purchase_note',
|
||
|
'menu_order',
|
||
|
]
|
||
|
|
||
|
caches_local_data = True
|
||
|
|
||
|
# TODO: would be nice to just set this here, but command args will always
|
||
|
# overwrite, b/c that defaults to 200 even when not specified by user
|
||
|
#batch_size = 100
|
||
|
|
||
|
def setup(self):
|
||
|
super(ProductImporter, self).setup()
|
||
|
self.establish_api()
|
||
|
self.to_be_created = []
|
||
|
self.to_be_updated = []
|
||
|
|
||
|
def datasync_setup(self):
|
||
|
super(ProductImporter, self).datasync_setup()
|
||
|
self.establish_api()
|
||
|
self.batch_size = 1
|
||
|
|
||
|
def establish_api(self):
|
||
|
kwargs = {
|
||
|
'url': self.config.require('woocommerce', 'url'),
|
||
|
'consumer_key': self.config.require('woocommerce', 'api_consumer_key'),
|
||
|
'consumer_secret': self.config.require('woocommerce', 'api_consumer_secret'),
|
||
|
'version': 'wc/v3',
|
||
|
'timeout': 30,
|
||
|
}
|
||
|
self.api = WooAPI(**kwargs)
|
||
|
|
||
|
def cache_local_data(self, host_data=None):
|
||
|
"""
|
||
|
Fetch existing products from WooCommerce.
|
||
|
"""
|
||
|
cache = {}
|
||
|
page = 1
|
||
|
while True:
|
||
|
response = self.api.get('products', params={'per_page': 100,
|
||
|
'page': page})
|
||
|
for product in response.json():
|
||
|
data = self.normalize_local_object(product)
|
||
|
normal = self.normalize_cache_object(product, data)
|
||
|
key = self.get_cache_key(product, normal)
|
||
|
cache[key] = normal
|
||
|
|
||
|
# TODO: this seems a bit hacky, is there a better way?
|
||
|
link = response.headers.get('Link')
|
||
|
if link and 'rel="next"' in link:
|
||
|
page += 1
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
return cache
|
||
|
|
||
|
def get_single_local_object(self, key):
|
||
|
assert self.key == ('id',)
|
||
|
woo_id = key[0]
|
||
|
# note, we avoid negative id here b/c that trick is used elsewhere
|
||
|
if woo_id > 0:
|
||
|
response = self.api.get('products/{}'.format(key[0]))
|
||
|
return response.json()
|
||
|
|
||
|
def normalize_local_object(self, api_product):
|
||
|
return dict(api_product)
|
||
|
|
||
|
def create_object(self, key, host_data):
|
||
|
data = dict(host_data)
|
||
|
data.pop('id', None)
|
||
|
|
||
|
if self.batch_size == 1: # datasync
|
||
|
if self.dry_run:
|
||
|
return data
|
||
|
response = self.api.post('products', data)
|
||
|
return response.json()
|
||
|
|
||
|
# collect data to be posted later
|
||
|
self.to_be_created.append(data)
|
||
|
return data
|
||
|
|
||
|
def update_object(self, api_product, host_data, local_data=None, all_fields=False):
|
||
|
|
||
|
if self.batch_size == 1: # datasync
|
||
|
if self.dry_run:
|
||
|
return host_data
|
||
|
response = self.api.post('products/{}'.format(api_product['id']), host_data)
|
||
|
return response.json()
|
||
|
|
||
|
# collect data to be posted later
|
||
|
self.to_be_updated.append(host_data)
|
||
|
return host_data
|
||
|
|
||
|
def flush_create_update(self):
|
||
|
if not self.dry_run and self.batch_size > 1: # not datasync
|
||
|
self.post_products_batch()
|
||
|
|
||
|
def post_products_batch(self):
|
||
|
"""
|
||
|
Push pending create/update data batch to WooCommerce API.
|
||
|
"""
|
||
|
assert not self.dry_run
|
||
|
response = self.api.post('products/batch', {'create': self.to_be_created,
|
||
|
'update': self.to_be_updated})
|
||
|
# clear the pending lists, since we've now pushed that data
|
||
|
self.to_be_created = []
|
||
|
self.to_be_updated = []
|
||
|
return response
|
||
|
|
||
|
def delete_object(self, api_product):
|
||
|
if self.dry_run:
|
||
|
return True
|
||
|
|
||
|
raise NotImplementedError
|