Initial support for basic WooCommerce integration
can export products to WooCommerce, plus maintain local cache, and track Woo ID for each rattail product
This commit is contained in:
commit
950a153342
24 changed files with 2214 additions and 0 deletions
27
rattail_woocommerce/__init__.py
Normal file
27
rattail_woocommerce/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail integration with WooCommerce
|
||||
"""
|
||||
|
||||
from ._version import __version__
|
3
rattail_woocommerce/_version.py
Normal file
3
rattail_woocommerce/_version.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
__version__ = '0.1.0'
|
45
rattail_woocommerce/commands.py
Normal file
45
rattail_woocommerce/commands.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail/WooCommerce Commands
|
||||
"""
|
||||
|
||||
from rattail import commands
|
||||
|
||||
|
||||
class ExportWooCommerce(commands.ImportSubcommand):
|
||||
"""
|
||||
Export data to WooCommerce
|
||||
"""
|
||||
name = 'export-woocommerce'
|
||||
description = __doc__.strip()
|
||||
handler_spec = 'rattail_woocommerce.woocommerce.importing.rattail:FromRattailToWooCommerce'
|
||||
|
||||
|
||||
class ImportWooCommerce(commands.ImportSubcommand):
|
||||
"""
|
||||
Import data from WooCommerce
|
||||
"""
|
||||
name = 'import-woocommerce'
|
||||
description = __doc__.strip()
|
||||
handler_spec = 'rattail_woocommerce.importing.woocommerce:FromWooCommerceToRattail'
|
52
rattail_woocommerce/config.py
Normal file
52
rattail_woocommerce/config.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail/WooCommerce Config
|
||||
"""
|
||||
|
||||
|
||||
def woocommerce_url(config, require=False, **kwargs):
|
||||
"""
|
||||
Returns the base URL for the WooCommerce website. Note that this URL will
|
||||
*not* have a trailing slash.
|
||||
"""
|
||||
args = ['woocommerce', 'url']
|
||||
if require:
|
||||
url = config.require(*args, **kwargs)
|
||||
return url.rstrip('/')
|
||||
else:
|
||||
url = config.get(*args, **kwargs)
|
||||
if url:
|
||||
return url.rstrip('/')
|
||||
|
||||
|
||||
def woocommerce_admin_product_url(config, product_id, base_url=None,
|
||||
require=False):
|
||||
"""
|
||||
Returns the WooCommerce Admin URL for the given product.
|
||||
"""
|
||||
if not base_url:
|
||||
base_url = woocommerce_url(config, require=require)
|
||||
if base_url:
|
||||
return '{}/wp-admin/post.php?post={}&action=edit'.format(
|
||||
base_url, product_id)
|
0
rattail_woocommerce/datasync/__init__.py
Normal file
0
rattail_woocommerce/datasync/__init__.py
Normal file
69
rattail_woocommerce/datasync/woocommerce.py
Normal file
69
rattail_woocommerce/datasync/woocommerce.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
DataSync for WooCommerce
|
||||
"""
|
||||
|
||||
from rattail.datasync import FromRattailConsumer
|
||||
|
||||
|
||||
class FromRattailToWooCommerce(FromRattailConsumer):
|
||||
"""
|
||||
Rattail -> WooCommerce datasync consumer
|
||||
"""
|
||||
handler_spec = 'rattail_woocommerce.woocommerce.importing.rattail:FromRattailToWooCommerce'
|
||||
|
||||
def process_changes(self, session, changes):
|
||||
"""
|
||||
Process all the given changes, coming from Rattail.
|
||||
"""
|
||||
# tell session who is responsible here
|
||||
if self.runas_username:
|
||||
session.set_continuum_user(self.runas_username)
|
||||
|
||||
# get all importers up to speed
|
||||
for importer in self.importers.values():
|
||||
importer.host_session = session
|
||||
importer.datasync_setup()
|
||||
|
||||
# sync all Product changes
|
||||
types = [
|
||||
'Product',
|
||||
'ProductPrice',
|
||||
]
|
||||
for change in [c for c in changes if c.payload_type in types]:
|
||||
product = self.get_product(session, change)
|
||||
if product:
|
||||
self.process_change(session, self.importers['Product'],
|
||||
host_object=product)
|
||||
|
||||
def get_product(self, session, change):
|
||||
model = self.model
|
||||
|
||||
if change.payload_type == 'Product':
|
||||
return session.query(model.Product).get(change.payload_key)
|
||||
|
||||
if change.payload_type == 'ProductPrice':
|
||||
price = session.query(model.ProductPrice).get(change.payload_key)
|
||||
if price:
|
||||
return price.product
|
0
rattail_woocommerce/db/__init__.py
Normal file
0
rattail_woocommerce/db/__init__.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""initial WooCommerce-related tables
|
||||
|
||||
Revision ID: c0bcca7bd1f9
|
||||
Revises: a3a6e2f7c9a5
|
||||
Create Date: 2021-01-19 19:05:51.962092
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c0bcca7bd1f9'
|
||||
down_revision = None
|
||||
branch_labels = ('rattail_woocommerce',)
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import rattail.db.types
|
||||
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
# woocommerce_product
|
||||
op.create_table('woocommerce_product',
|
||||
sa.Column('uuid', sa.String(length=32), nullable=False),
|
||||
sa.Column('woocommerce_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['uuid'], ['product.uuid'], name='woocommerce_product_fk_product'),
|
||||
sa.PrimaryKeyConstraint('uuid')
|
||||
)
|
||||
op.create_table('woocommerce_product_version',
|
||||
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
|
||||
sa.Column('woocommerce_id', sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
|
||||
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('uuid', 'transaction_id')
|
||||
)
|
||||
op.create_index(op.f('ix_woocommerce_product_version_end_transaction_id'), 'woocommerce_product_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_woocommerce_product_version_operation_type'), 'woocommerce_product_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_woocommerce_product_version_transaction_id'), 'woocommerce_product_version', ['transaction_id'], unique=False)
|
||||
|
||||
# woocommerce_cache_product
|
||||
op.create_table('woocommerce_cache_product',
|
||||
sa.Column('uuid', sa.String(length=32), nullable=False),
|
||||
sa.Column('product_uuid', sa.String(length=32), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('slug', sa.String(length=100), nullable=True),
|
||||
sa.Column('permalink', sa.String(length=255), nullable=True),
|
||||
sa.Column('date_created', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_created_gmt', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_modified', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_modified_gmt', sa.DateTime(), nullable=True),
|
||||
sa.Column('type', sa.String(length=20), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('featured', sa.Boolean(), nullable=True),
|
||||
sa.Column('catalog_visibility', sa.String(length=20), nullable=True),
|
||||
sa.Column('description', sa.String(length=255), nullable=True),
|
||||
sa.Column('short_description', sa.String(length=255), nullable=True),
|
||||
sa.Column('sku', sa.String(length=50), nullable=True),
|
||||
sa.Column('price', sa.String(length=20), nullable=True),
|
||||
sa.Column('regular_price', sa.String(length=20), nullable=True),
|
||||
sa.Column('sale_price', sa.String(length=20), nullable=True),
|
||||
sa.Column('date_on_sale_from', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_on_sale_from_gmt', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_on_sale_to', sa.DateTime(), nullable=True),
|
||||
sa.Column('date_on_sale_to_gmt', sa.DateTime(), nullable=True),
|
||||
sa.Column('price_html', sa.String(length=255), nullable=True),
|
||||
sa.Column('on_sale', sa.Boolean(), nullable=True),
|
||||
sa.Column('purchasable', sa.Boolean(), nullable=True),
|
||||
sa.Column('total_sales', sa.Integer(), nullable=True),
|
||||
sa.Column('tax_status', sa.String(length=20), nullable=True),
|
||||
sa.Column('tax_class', sa.String(length=50), nullable=True),
|
||||
sa.Column('manage_stock', sa.Boolean(), nullable=True),
|
||||
sa.Column('stock_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('stock_status', sa.String(length=20), nullable=True),
|
||||
sa.Column('backorders', sa.String(length=20), nullable=True),
|
||||
sa.Column('backorders_allowed', sa.Boolean(), nullable=True),
|
||||
sa.Column('backordered', sa.Boolean(), nullable=True),
|
||||
sa.Column('sold_individually', sa.Boolean(), nullable=True),
|
||||
sa.Column('weight', sa.String(length=20), nullable=True),
|
||||
sa.Column('reviews_allowed', sa.Boolean(), nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('purchase_note', sa.String(length=255), nullable=True),
|
||||
sa.Column('menu_order', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['product_uuid'], ['product.uuid'], name='woocommerce_cache_product_fk_product'),
|
||||
sa.PrimaryKeyConstraint('uuid'),
|
||||
sa.UniqueConstraint('id', name='woocommerce_cache_product_uq_id')
|
||||
)
|
||||
op.create_table('woocommerce_cache_product_version',
|
||||
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
|
||||
sa.Column('product_uuid', sa.String(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('id', sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column('name', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('slug', sa.String(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column('permalink', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('date_created', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('date_created_gmt', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('type', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('status', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('featured', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('catalog_visibility', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('description', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('short_description', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('sku', sa.String(length=50), autoincrement=False, nullable=True),
|
||||
sa.Column('price', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('regular_price', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('sale_price', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('date_on_sale_from', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('date_on_sale_from_gmt', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('date_on_sale_to', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('date_on_sale_to_gmt', sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column('price_html', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('on_sale', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('purchasable', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('tax_status', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('tax_class', sa.String(length=50), autoincrement=False, nullable=True),
|
||||
sa.Column('manage_stock', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('backorders', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('backorders_allowed', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('sold_individually', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('weight', sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column('reviews_allowed', sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column('purchase_note', sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('menu_order', sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
|
||||
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('uuid', 'transaction_id')
|
||||
)
|
||||
op.create_index(op.f('ix_woocommerce_cache_product_version_end_transaction_id'), 'woocommerce_cache_product_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_woocommerce_cache_product_version_operation_type'), 'woocommerce_cache_product_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_woocommerce_cache_product_version_transaction_id'), 'woocommerce_cache_product_version', ['transaction_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
# woocommerce_cache_product
|
||||
op.drop_index(op.f('ix_woocommerce_cache_product_version_transaction_id'), table_name='woocommerce_cache_product_version')
|
||||
op.drop_index(op.f('ix_woocommerce_cache_product_version_operation_type'), table_name='woocommerce_cache_product_version')
|
||||
op.drop_index(op.f('ix_woocommerce_cache_product_version_end_transaction_id'), table_name='woocommerce_cache_product_version')
|
||||
op.drop_table('woocommerce_cache_product_version')
|
||||
op.drop_table('woocommerce_cache_product')
|
||||
|
||||
# woocommerce_product
|
||||
op.drop_index(op.f('ix_woocommerce_product_version_transaction_id'), table_name='woocommerce_product_version')
|
||||
op.drop_index(op.f('ix_woocommerce_product_version_operation_type'), table_name='woocommerce_product_version')
|
||||
op.drop_index(op.f('ix_woocommerce_product_version_end_transaction_id'), table_name='woocommerce_product_version')
|
||||
op.drop_table('woocommerce_product_version')
|
||||
op.drop_table('woocommerce_product')
|
182
rattail_woocommerce/db/model.py
Normal file
182
rattail_woocommerce/db/model.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Database schema extensions for WooCommerce integration
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
from rattail.db import model
|
||||
from rattail.db.model.shopfoo import ShopfooProductBase
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WooProductExtension',
|
||||
'WooCacheProduct',
|
||||
]
|
||||
|
||||
|
||||
class WooProductExtension(model.Base):
|
||||
"""
|
||||
WooCommerce-specific extension to Rattail Product
|
||||
"""
|
||||
__tablename__ = 'woocommerce_product'
|
||||
__table_args__ = (
|
||||
sa.ForeignKeyConstraint(['uuid'], ['product.uuid'],
|
||||
name='woocommerce_product_fk_product'),
|
||||
)
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column(default=None)
|
||||
product = orm.relationship(
|
||||
model.Product,
|
||||
doc="""
|
||||
Reference to the actual product record, which this one extends.
|
||||
""",
|
||||
backref=orm.backref(
|
||||
'_woocommerce',
|
||||
uselist=False,
|
||||
cascade='all, delete-orphan',
|
||||
doc="""
|
||||
Reference to the WooCommerce extension record for this product.
|
||||
"""))
|
||||
|
||||
woocommerce_id = sa.Column(sa.Integer(), nullable=False, doc="""
|
||||
``id`` value for the product, within WooCommerce.
|
||||
""")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.product)
|
||||
|
||||
WooProductExtension.make_proxy(model.Product, '_woocommerce', 'woocommerce_id')
|
||||
|
||||
|
||||
class WooCacheProduct(ShopfooProductBase, model.Base):
|
||||
"""
|
||||
Local cache table for WooCommerce Products
|
||||
|
||||
https://woocommerce.github.io/woocommerce-rest-api-docs/#product-properties
|
||||
"""
|
||||
__tablename__ = 'woocommerce_cache_product'
|
||||
model_title = "WooCommerce Product"
|
||||
|
||||
@declared_attr
|
||||
def __table_args__(cls):
|
||||
return cls.__product_table_args__() + (
|
||||
sa.UniqueConstraint('id', name='woocommerce_cache_product_uq_id'),
|
||||
)
|
||||
|
||||
__versioned__ = {
|
||||
'exclude': [
|
||||
'date_modified',
|
||||
'date_modified_gmt',
|
||||
'total_sales',
|
||||
'stock_quantity',
|
||||
'stock_status',
|
||||
'backordered',
|
||||
]}
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
|
||||
name = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
slug = sa.Column(sa.String(length=100), nullable=True)
|
||||
|
||||
permalink = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
date_created = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_created_gmt = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_modified = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_modified_gmt = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
type = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
status = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
featured = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
catalog_visibility = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
description = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
short_description = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
sku = sa.Column(sa.String(length=50), nullable=True)
|
||||
|
||||
price = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
regular_price = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
sale_price = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
date_on_sale_from = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_on_sale_from_gmt = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_on_sale_to = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
date_on_sale_to_gmt = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
price_html = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
on_sale = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
purchasable = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
total_sales = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
tax_status = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
tax_class = sa.Column(sa.String(length=50), nullable=True)
|
||||
|
||||
manage_stock = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
stock_quantity = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
stock_status = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
backorders = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
backorders_allowed = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
backordered = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
sold_individually = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
weight = sa.Column(sa.String(length=20), nullable=True)
|
||||
|
||||
reviews_allowed = sa.Column(sa.Boolean(), nullable=True)
|
||||
|
||||
parent_id = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
purchase_note = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
menu_order = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
27
rattail_woocommerce/importing/__init__.py
Normal file
27
rattail_woocommerce/importing/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Importing into Rattail
|
||||
"""
|
||||
|
||||
from . import model
|
52
rattail_woocommerce/importing/model.py
Normal file
52
rattail_woocommerce/importing/model.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail/WooCommerce model importers
|
||||
"""
|
||||
|
||||
from rattail import importing
|
||||
from rattail.importing.model import ToRattail
|
||||
from rattail_woocommerce.db import model
|
||||
|
||||
|
||||
##############################
|
||||
# core importer overrides
|
||||
##############################
|
||||
|
||||
class ProductImporter(importing.model.ProductImporter):
|
||||
|
||||
extension_attr = '_woocommerce'
|
||||
extension_fields = [
|
||||
'woocommerce_id',
|
||||
]
|
||||
|
||||
|
||||
##############################
|
||||
# custom models
|
||||
##############################
|
||||
|
||||
class WooCacheProductImporter(ToRattail):
|
||||
"""
|
||||
Importer for WooCacheProduct data
|
||||
"""
|
||||
model_class = model.WooCacheProduct
|
42
rattail_woocommerce/importing/versions.py
Normal file
42
rattail_woocommerce/importing/versions.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail -> Rattail "versions" data import
|
||||
"""
|
||||
|
||||
from rattail.importing import versions as base
|
||||
from rattail_woocommerce.db import model
|
||||
|
||||
|
||||
class WooVersionMixin(object):
|
||||
"""
|
||||
Add default registration of custom importers
|
||||
"""
|
||||
|
||||
def add_woocommerce_importers(self, importers):
|
||||
importers['WooCacheProduct'] = WooCacheProductImporter
|
||||
return importers
|
||||
|
||||
|
||||
class WooCacheProductImporter(base.VersionImporter):
|
||||
host_model_class = model.WooCacheProduct
|
286
rattail_woocommerce/importing/woocommerce.py
Normal file
286
rattail_woocommerce/importing/woocommerce.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
# -*- 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 -> Rattail data import
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import orm
|
||||
|
||||
from woocommerce import API as WooAPI
|
||||
|
||||
from rattail import importing
|
||||
from rattail.core import get_uuid
|
||||
from rattail.util import OrderedDict
|
||||
from rattail.time import localtime, make_utc
|
||||
from rattail_woocommerce import importing as rattail_woocommerce_importing
|
||||
|
||||
|
||||
class FromWooCommerceToRattail(importing.ToRattailHandler):
|
||||
"""
|
||||
Handler for WooCommerce -> Rattail
|
||||
"""
|
||||
host_title = "WooCommerce"
|
||||
|
||||
def get_importers(self):
|
||||
importers = OrderedDict()
|
||||
importers['Product'] = ProductImporter
|
||||
importers['WooCacheProduct'] = WooCacheProductImporter
|
||||
return importers
|
||||
|
||||
|
||||
class FromWooCommerce(importing.Importer):
|
||||
|
||||
def setup(self):
|
||||
super(FromWooCommerce, self).setup()
|
||||
|
||||
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',
|
||||
}
|
||||
self.api = WooAPI(**kwargs)
|
||||
|
||||
def get_woocommerce_products(self):
|
||||
products = []
|
||||
page = 1
|
||||
while True:
|
||||
block = self.api.get('products', params={'per_page': 100,
|
||||
'page': page})
|
||||
products.extend(block.json())
|
||||
link = block.headers.get('Link')
|
||||
if link and 'rel="next"' in link:
|
||||
page += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return products
|
||||
|
||||
|
||||
class ProductImporter(FromWooCommerce, rattail_woocommerce_importing.model.ProductImporter):
|
||||
"""
|
||||
Importer for product data from WooCommerce.
|
||||
"""
|
||||
key = 'uuid'
|
||||
supported_fields = [
|
||||
'uuid',
|
||||
'woocommerce_id',
|
||||
'item_id',
|
||||
'description',
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
super(ProductImporter, self).setup()
|
||||
model = self.model
|
||||
|
||||
query = self.session.query(model.Product)\
|
||||
.join(model.WooProductExtension)\
|
||||
.options(orm.joinedload(model.Product._woocommerce))
|
||||
self.products_by_woo_id = self.cache_model(model.Product,
|
||||
key='woocommerce_id',
|
||||
query=query)
|
||||
|
||||
query = self.session.query(model.Product)\
|
||||
.filter(model.Product.item_id != None)
|
||||
self.products_by_item_id = self.cache_model(model.Product,
|
||||
key='item_id',
|
||||
query=query)
|
||||
|
||||
def get_host_objects(self):
|
||||
return self.get_woocommerce_products()
|
||||
|
||||
def find_rattail_product(self, api_product):
|
||||
product = self.get_product_by_woo_id(api_product['id'])
|
||||
if product:
|
||||
return product
|
||||
|
||||
if api_product['sku']:
|
||||
return self.get_product_by_item_id(api_product['sku'])
|
||||
|
||||
def get_product_by_woo_id(self, woo_id):
|
||||
if hasattr(self, 'products_by_woo_id'):
|
||||
return self.products_by_woo_id.get(woo_id)
|
||||
|
||||
model = self.model
|
||||
try:
|
||||
return self.session.query(model.Product)\
|
||||
.join(model.WooProductExtension)\
|
||||
.filter(model.WooProductExtension.id == woo_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
def get_product_by_item_id(self, item_id):
|
||||
if hasattr(self, 'products_by_item_id'):
|
||||
return self.products_by_item_id.get(item_id)
|
||||
|
||||
model = self.model
|
||||
try:
|
||||
return self.session.query(model.Product)\
|
||||
.filter(model.Product.item_id == item_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
def normalize_host_object(self, api_product):
|
||||
product = self.find_rattail_product(api_product)
|
||||
if product:
|
||||
uuid = product.uuid
|
||||
else:
|
||||
uuid = get_uuid()
|
||||
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'woocommerce_id': api_product['id'],
|
||||
'item_id': api_product['sku'],
|
||||
'description': api_product['name'],
|
||||
}
|
||||
|
||||
|
||||
class WooCacheProductImporter(FromWooCommerce, rattail_woocommerce_importing.model.WooCacheProductImporter):
|
||||
"""
|
||||
WooCommerce Product -> WooCacheProduct
|
||||
"""
|
||||
key = 'id'
|
||||
supported_fields = [
|
||||
'id',
|
||||
'product_uuid',
|
||||
'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',
|
||||
]
|
||||
|
||||
# declare which fields are 'date' type, so can treat specially
|
||||
date_fields = [
|
||||
'date_created',
|
||||
'date_created_gmt',
|
||||
'date_modified',
|
||||
'date_modified_gmt',
|
||||
'date_on_sale_from',
|
||||
'date_on_sale_from_gmt',
|
||||
'date_on_sale_to',
|
||||
'date_on_sale_to_gmt',
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
super(WooCacheProductImporter, self).setup()
|
||||
model = self.model
|
||||
|
||||
query = self.session.query(model.Product)\
|
||||
.filter(model.Product.item_id != None)
|
||||
self.products_by_item_id = self.cache_model(model.Product,
|
||||
key='item_id', query=query)
|
||||
|
||||
query = self.session.query(model.WooCacheProduct)\
|
||||
.options(orm.joinedload(model.WooCacheProduct.product))
|
||||
self.woo_cache_by_id = self.cache_model(model.WooCacheProduct,
|
||||
key='id', query=query)
|
||||
|
||||
def find_rattail_product(self, api_product):
|
||||
woo_product = self.get_woo_product_by_id(api_product['id'])
|
||||
if woo_product and woo_product.product:
|
||||
return woo_product.product
|
||||
|
||||
return self.get_product_by_item_id(api_product['sku'])
|
||||
|
||||
def get_woo_product_by_id(self, woo_id):
|
||||
if hasattr(self, 'woo_cache_by_id'):
|
||||
return self.woo_cache_by_id.get(woo_id)
|
||||
|
||||
model = self.config.get_model()
|
||||
try:
|
||||
return self.session.query(model.WooCacheProduct)\
|
||||
.filter(model.WooCacheProduct.id == woo_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
def get_product_by_item_id(self, item_id):
|
||||
if hasattr(self, 'products_by_item_id'):
|
||||
return self.products_by_item_id.get(item_id)
|
||||
|
||||
model = self.config.get_model()
|
||||
try:
|
||||
return self.session.query(model.Product)\
|
||||
.filter(model.Product.item_id == item_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
def get_host_objects(self):
|
||||
return self.get_woocommerce_products()
|
||||
|
||||
def normalize_host_object(self, api_product):
|
||||
data = dict(api_product)
|
||||
|
||||
product = self.find_rattail_product(api_product)
|
||||
data['product_uuid'] = product.uuid if product else None
|
||||
|
||||
for field in self.date_fields:
|
||||
value = data[field]
|
||||
if value:
|
||||
value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
|
||||
if not field.endswith('_gmt'):
|
||||
value = localtime(self.config, value)
|
||||
value = make_utc(value)
|
||||
data[field] = value
|
||||
|
||||
return data
|
0
rattail_woocommerce/woocommerce/__init__.py
Normal file
0
rattail_woocommerce/woocommerce/__init__.py
Normal file
27
rattail_woocommerce/woocommerce/importing/__init__.py
Normal file
27
rattail_woocommerce/woocommerce/importing/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Importing data into WooCommerce
|
||||
"""
|
||||
|
||||
from . import model
|
193
rattail_woocommerce/woocommerce/importing/model.py
Normal file
193
rattail_woocommerce/woocommerce/importing/model.py
Normal file
|
@ -0,0 +1,193 @@
|
|||
# -*- 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
|
205
rattail_woocommerce/woocommerce/importing/rattail.py
Normal file
205
rattail_woocommerce/woocommerce/importing/rattail.py
Normal file
|
@ -0,0 +1,205 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail -> WooCommerce importing
|
||||
"""
|
||||
|
||||
from rattail import importing
|
||||
from rattail.db import model
|
||||
from rattail.util import OrderedDict
|
||||
from rattail_woocommerce.db.model import WooCacheProduct
|
||||
from rattail_woocommerce.woocommerce import importing as woocommerce_importing
|
||||
|
||||
|
||||
class FromRattailToWooCommerce(importing.FromRattailHandler):
|
||||
"""
|
||||
Rattail -> WooCommerce import handler
|
||||
"""
|
||||
local_title = "WooCommerce"
|
||||
direction = 'export'
|
||||
|
||||
# necessary to save new WooCacheProduct records along the way
|
||||
# (since they really are created immediately in Woo via API)
|
||||
commit_host_partial = True
|
||||
|
||||
@property
|
||||
def host_title(self):
|
||||
return self.config.app_title(default="Rattail")
|
||||
|
||||
def get_importers(self):
|
||||
importers = OrderedDict()
|
||||
importers['Product'] = ProductImporter
|
||||
return importers
|
||||
|
||||
|
||||
class FromRattail(importing.FromSQLAlchemy):
|
||||
"""
|
||||
Base class for WooCommerce -> Rattail importers
|
||||
"""
|
||||
|
||||
|
||||
class ProductImporter(FromRattail, woocommerce_importing.model.ProductImporter):
|
||||
"""
|
||||
Product data importer
|
||||
"""
|
||||
host_model_class = model.Product
|
||||
key = 'id'
|
||||
supported_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'sku',
|
||||
'regular_price',
|
||||
'sale_price',
|
||||
'date_on_sale_from_gmt',
|
||||
'date_on_sale_to_gmt',
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
super(ProductImporter, self).setup()
|
||||
self.init_woo_id_counter()
|
||||
self.establish_cache_importer()
|
||||
|
||||
def datasync_setup(self):
|
||||
super(ProductImporter, self).datasync_setup()
|
||||
self.init_woo_id_counter()
|
||||
self.establish_cache_importer()
|
||||
|
||||
def init_woo_id_counter(self):
|
||||
self.next_woo_id = 1
|
||||
|
||||
def establish_cache_importer(self):
|
||||
from rattail_woocommerce.importing.woocommerce import WooCacheProductImporter
|
||||
|
||||
# we'll use this importer to update our local cache
|
||||
self.cache_importer = WooCacheProductImporter(config=self.config)
|
||||
self.cache_importer.session = self.host_session
|
||||
|
||||
def normalize_host_object(self, product):
|
||||
|
||||
woo_id = product.woocommerce_id
|
||||
if not woo_id:
|
||||
# note, we set to negative to ensure it won't exist but is unique.
|
||||
# but we will not actually try to push this value to Woo
|
||||
woo_id = -self.next_woo_id
|
||||
self.next_woo_id += 1
|
||||
|
||||
regular_price = ''
|
||||
if product.regular_price and product.regular_price.price:
|
||||
regular_price = '{:0.2f}'.format(product.regular_price.price)
|
||||
|
||||
sale_price = ''
|
||||
if product.sale_price and product.sale_price.price:
|
||||
sale_price = '{:0.2f}'.format(product.sale_price.price)
|
||||
|
||||
date_on_sale_from_gmt = '1900-01-01T00:00:00'
|
||||
if product.sale_price and product.sale_price.starts:
|
||||
dt = localtime(self.config, product.sale_price.starts,
|
||||
from_utc=True, zoneinfo=False)
|
||||
date_on_sale_from_gmt = dt.isoformat()
|
||||
|
||||
date_on_sale_to_gmt = '1900-01-01T00:00:00'
|
||||
if product.sale_price and product.sale_price.starts:
|
||||
dt = localtime(self.config, product.sale_price.starts,
|
||||
from_utc=True, zoneinfo=False)
|
||||
date_on_sale_to_gmt = dt.isoformat()
|
||||
|
||||
return {
|
||||
'id': woo_id,
|
||||
'name': product.description,
|
||||
'sku': product.item_id,
|
||||
'regular_price': regular_price,
|
||||
'sale_price': sale_price,
|
||||
'date_on_sale_from_gmt': date_on_sale_from_gmt,
|
||||
'date_on_sale_to_gmt': date_on_sale_to_gmt,
|
||||
}
|
||||
|
||||
def create_object(self, key, host_data):
|
||||
|
||||
# push create to API as normal
|
||||
api_product = super(ProductImporter, self).create_object(key, host_data)
|
||||
|
||||
# also create local cache record, if running in datasync
|
||||
if self.batch_size == 1: # datasync
|
||||
self.update_woocache(api_product)
|
||||
|
||||
return api_product
|
||||
|
||||
def update_object(self, api_product, host_data, local_data=None, **kwargs):
|
||||
|
||||
# push update to API as normal
|
||||
api_product = super(ProductImporter, self).update_object(api_product, host_data,
|
||||
local_data=local_data,
|
||||
**kwargs)
|
||||
|
||||
# also update local cache record, if running in datasync
|
||||
if self.batch_size == 1:
|
||||
self.update_woocache(api_product)
|
||||
|
||||
return api_product
|
||||
|
||||
def post_products_batch(self):
|
||||
|
||||
# first post batch to API as normal
|
||||
response = super(ProductImporter, self).post_products_batch()
|
||||
data = response.json()
|
||||
|
||||
def create_cache(api_product, i):
|
||||
self.update_woocache(api_product)
|
||||
|
||||
self.progress_loop(create_cache, data.get('create', []),
|
||||
message="Updating cache for created items")
|
||||
self.host_session.flush()
|
||||
|
||||
def update_cache(api_product, i):
|
||||
# re-fetch the api_product to make sure we have right info. for
|
||||
# some reason at least one field is represented differently, when
|
||||
# we fetch the record vs. how it appears in the batch response.
|
||||
api_product = self.api.get('products/{}'.format(api_product['id']))\
|
||||
.json()
|
||||
self.update_woocache(api_product)
|
||||
|
||||
self.progress_loop(update_cache, data.get('update', []),
|
||||
message="Updating cache for updated items")
|
||||
self.host_session.flush()
|
||||
|
||||
return response
|
||||
|
||||
def update_woocache(self, api_product):
|
||||
|
||||
# normalize data and process importer update
|
||||
normal = self.cache_importer.normalize_host_object(api_product)
|
||||
key = self.cache_importer.get_key(normal)
|
||||
cache_product = self.cache_importer.get_local_object(key)
|
||||
if cache_product:
|
||||
cache_normal = self.cache_importer.normalize_local_object(cache_product)
|
||||
cache_product = self.cache_importer.update_object(
|
||||
cache_product, normal, local_data=cache_normal)
|
||||
else:
|
||||
cache_product = self.cache_importer.create_object(key, normal)
|
||||
|
||||
# update cached woo_id too, if we can
|
||||
self.host_session.flush()
|
||||
if cache_product and cache_product.product:
|
||||
product = cache_product.product
|
||||
if product.woocommerce_id != api_product['id']:
|
||||
product.woocommerce_id = api_product['id']
|
Loading…
Add table
Add a link
Reference in a new issue