commit 00e2a9fcb2e21f9ba2b26b65394df0da9d95ed7d Author: Lance Edgar Date: Fri Sep 2 17:14:58 2022 -0500 Initial commit basic import from Wave API to cache tables for Customers, Invoices diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bac158 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +rattail_wave.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7899c8b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ + +# Changelog +All notable changes to rattail-wave will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.1.0] - ?? +### Added +- Initial version. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b807da8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *.md +include *.rst +recursive-include rattail_wave/db/alembic *.mako diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7e17f4c --- /dev/null +++ b/README.rst @@ -0,0 +1,14 @@ + +rattail-wave +============ + +Rattail is a retail software framework, released under the GNU General +Public License. + +This package contains software interfaces for `Wave`_. + +.. _`Wave`: https://www.waveapps.com/ + +Please see the `Rattail Project`_ for more information. + +.. _`Rattail Project`: https://rattailproject.org/ diff --git a/rattail_wave/__init__.py b/rattail_wave/__init__.py new file mode 100644 index 0000000..2a9844d --- /dev/null +++ b/rattail_wave/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +rattail-wave package root +""" + +from ._version import __version__ diff --git a/rattail_wave/_version.py b/rattail_wave/_version.py new file mode 100644 index 0000000..e41b669 --- /dev/null +++ b/rattail_wave/_version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8; -*- + +__version__ = '0.1.0' diff --git a/rattail_wave/commands.py b/rattail_wave/commands.py new file mode 100644 index 0000000..2bcc303 --- /dev/null +++ b/rattail_wave/commands.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +rattail-wave commands +""" + +from rattail import commands + + +class ImportWave(commands.ImportSubcommand): + """ + Import data to Rattail, from Wave API + """ + name = 'import-wave' + description = __doc__.strip() + handler_key = 'to_rattail.from_wave.import' diff --git a/rattail_wave/config.py b/rattail_wave/config.py new file mode 100644 index 0000000..0c7f9e6 --- /dev/null +++ b/rattail_wave/config.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Config Extension +""" + +from rattail.config import ConfigExtension + + +class RattailWaveExtension(ConfigExtension): + """ + Config extension for rattail-wave. + """ + key = 'rattail_wave' + + def configure(self, config): + + # rattail import-wave + config.setdefault('rattail.importing', 'to_rattail.from_wave.import.default_handler', + 'rattail_wave.importing.wave:FromWaveToRattail') + config.setdefault('rattail.importing', 'to_rattail.from_wave.import.default_cmd', + 'rattail import-wave') + + +def get_wave_url(config): + url = config.get('wave', 'url') + if url: + return url.rstrip('/') diff --git a/rattail_wave/db/__init__.py b/rattail_wave/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_wave/db/alembic/versions/6a20ed366981_initial_wave_cache_tables.py b/rattail_wave/db/alembic/versions/6a20ed366981_initial_wave_cache_tables.py new file mode 100644 index 0000000..b87fad6 --- /dev/null +++ b/rattail_wave/db/alembic/versions/6a20ed366981_initial_wave_cache_tables.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8; -*- +"""initial Wave cache tables + +Revision ID: 6a20ed366981 +Revises: 7d009a925f21 +Create Date: 2022-09-02 13:27:36.945137 + +""" + +# revision identifiers, used by Alembic. +revision = '6a20ed366981' +down_revision = None +branch_labels = ('rattail_wave',) +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + ############################## + # cache tables + ############################## + + # wave_cache_customer + op.create_table('wave_cache_customer', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('id', sa.String(length=100), nullable=False), + sa.Column('internal_id', sa.String(length=100), nullable=True), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('is_archived', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('modified_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('uuid'), + sa.UniqueConstraint('id', name='wave_cache_customer_uq_id') + ) + op.create_table('wave_cache_customer_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('id', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('internal_id', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('name', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('email', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('is_archived', sa.Boolean(), autoincrement=False, nullable=True), + sa.Column('created_at', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('modified_at', sa.DateTime(), 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_wave_cache_customer_version_end_transaction_id'), 'wave_cache_customer_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_wave_cache_customer_version_operation_type'), 'wave_cache_customer_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_wave_cache_customer_version_transaction_id'), 'wave_cache_customer_version', ['transaction_id'], unique=False) + + # wave_cache_invoice + op.create_table('wave_cache_invoice', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('id', sa.String(length=100), nullable=False), + sa.Column('internal_id', sa.String(length=100), nullable=True), + sa.Column('customer_id', sa.String(length=100), nullable=False), + sa.Column('status', sa.String(length=10), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('subhead', sa.String(length=255), nullable=True), + sa.Column('invoice_number', sa.String(length=10), nullable=False), + sa.Column('invoice_date', sa.Date(), nullable=False), + sa.Column('due_date', sa.Date(), nullable=False), + sa.Column('amount_due', sa.Numeric(precision=9, scale=2), nullable=False), + sa.Column('amount_paid', sa.Numeric(precision=9, scale=2), nullable=False), + sa.Column('tax_total', sa.Numeric(precision=9, scale=2), nullable=False), + sa.Column('total', sa.Numeric(precision=9, scale=2), nullable=False), + sa.Column('discount_total', sa.Numeric(precision=9, scale=2), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('modified_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['customer_id'], ['wave_cache_customer.id'], name='wave_cache_invoice_fk_customer'), + sa.PrimaryKeyConstraint('uuid'), + sa.UniqueConstraint('id', name='wave_cache_invoice_uq_id') + ) + op.create_table('wave_cache_invoice_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('id', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('internal_id', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('customer_id', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('status', sa.String(length=10), autoincrement=False, nullable=True), + sa.Column('title', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('subhead', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('invoice_number', sa.String(length=10), autoincrement=False, nullable=True), + sa.Column('invoice_date', sa.Date(), autoincrement=False, nullable=True), + sa.Column('due_date', sa.Date(), autoincrement=False, nullable=True), + sa.Column('amount_due', sa.Numeric(precision=9, scale=2), autoincrement=False, nullable=True), + sa.Column('amount_paid', sa.Numeric(precision=9, scale=2), autoincrement=False, nullable=True), + sa.Column('tax_total', sa.Numeric(precision=9, scale=2), autoincrement=False, nullable=True), + sa.Column('total', sa.Numeric(precision=9, scale=2), autoincrement=False, nullable=True), + sa.Column('discount_total', sa.Numeric(precision=9, scale=2), autoincrement=False, nullable=True), + sa.Column('created_at', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('modified_at', sa.DateTime(), 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_wave_cache_invoice_version_end_transaction_id'), 'wave_cache_invoice_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_wave_cache_invoice_version_operation_type'), 'wave_cache_invoice_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_wave_cache_invoice_version_transaction_id'), 'wave_cache_invoice_version', ['transaction_id'], unique=False) + + ############################## + # integration tables + ############################## + + # wave_customer + op.create_table('wave_customer', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('wave_id', sa.String(length=100), nullable=False), + sa.ForeignKeyConstraint(['uuid'], ['customer.uuid'], name='wave_customer_fk_customer'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('wave_customer_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('wave_id', sa.String(length=100), 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_wave_customer_version_end_transaction_id'), 'wave_customer_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_wave_customer_version_operation_type'), 'wave_customer_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_wave_customer_version_transaction_id'), 'wave_customer_version', ['transaction_id'], unique=False) + + +def downgrade(): + + ############################## + # integration tables + ############################## + + # wave_customer + op.drop_index(op.f('ix_wave_customer_version_transaction_id'), table_name='wave_customer_version') + op.drop_index(op.f('ix_wave_customer_version_operation_type'), table_name='wave_customer_version') + op.drop_index(op.f('ix_wave_customer_version_end_transaction_id'), table_name='wave_customer_version') + op.drop_table('wave_customer_version') + op.drop_table('wave_customer') + + ############################## + # cache tables + ############################## + + # wave_cache_invoice + op.drop_index(op.f('ix_wave_cache_invoice_version_transaction_id'), table_name='wave_cache_invoice_version') + op.drop_index(op.f('ix_wave_cache_invoice_version_operation_type'), table_name='wave_cache_invoice_version') + op.drop_index(op.f('ix_wave_cache_invoice_version_end_transaction_id'), table_name='wave_cache_invoice_version') + op.drop_table('wave_cache_invoice_version') + op.drop_table('wave_cache_invoice') + + # wave_cache_customer + op.drop_index(op.f('ix_wave_cache_customer_version_transaction_id'), table_name='wave_cache_customer_version') + op.drop_index(op.f('ix_wave_cache_customer_version_operation_type'), table_name='wave_cache_customer_version') + op.drop_index(op.f('ix_wave_cache_customer_version_end_transaction_id'), table_name='wave_cache_customer_version') + op.drop_table('wave_cache_customer_version') + op.drop_table('wave_cache_customer') diff --git a/rattail_wave/db/model/__init__.py b/rattail_wave/db/model/__init__.py new file mode 100644 index 0000000..76bcb02 --- /dev/null +++ b/rattail_wave/db/model/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Wave integration data models +""" + +from .wave_cache import WaveCacheCustomer, WaveCacheInvoice + +from .customers import WaveCustomer diff --git a/rattail_wave/db/model/customers.py b/rattail_wave/db/model/customers.py new file mode 100644 index 0000000..a46d8ea --- /dev/null +++ b/rattail_wave/db/model/customers.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Wave integration data models +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model + + +class WaveCustomer(model.Base): + """ + Wave-specific extension to Customer model + """ + __tablename__ = 'wave_customer' + __table_args__ = ( + sa.ForeignKeyConstraint(['uuid'], ['customer.uuid'], + name='wave_customer_fk_customer'), + ) + __versioned__ = {} + + uuid = model.uuid_column(default=None) + customer = orm.relationship( + model.Customer, + doc=""" + Reference to the actual customer record, which this one extends. + """, + backref=orm.backref( + '_wave', + uselist=False, + cascade='all, delete-orphan', + doc=""" + Reference to the Wave extension record for this customer. + """)) + + wave_id = sa.Column(sa.String(length=100), nullable=False, doc=""" + ``id`` value for the customer, within Wave. + """) + + def __str__(self): + return str(self.customer) + + +WaveCustomer.make_proxy(model.Customer, '_wave', 'wave_id') diff --git a/rattail_wave/db/model/wave_cache.py b/rattail_wave/db/model/wave_cache.py new file mode 100644 index 0000000..9ec0618 --- /dev/null +++ b/rattail_wave/db/model/wave_cache.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Wave "cache" data models +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model + + +class WaveCacheCustomer(model.Base): + """ + Represents a customer record in Wave. + + https://developer.waveapps.com/hc/en-us/articles/360019968212#customer + """ + __tablename__ = 'wave_cache_customer' + __table_args__ = ( + sa.UniqueConstraint('id', name='wave_cache_customer_uq_id'), + ) + __versioned__ = {} + model_title = "Wave Customer" + + uuid = model.uuid_column() + + id = sa.Column(sa.String(length=100), nullable=False) + internal_id = sa.Column(sa.String(length=100), nullable=True) + name = sa.Column(sa.String(length=255), nullable=True) + email = sa.Column(sa.String(length=255), nullable=True) + is_archived = sa.Column(sa.Boolean(), nullable=True) + created_at = sa.Column(sa.DateTime(), nullable=True) + modified_at = sa.Column(sa.DateTime(), nullable=True) + + def __str__(self): + return self.name or "" + + +class WaveCacheInvoice(model.Base): + """ + Represents an invoice record in Wave. + + https://developer.waveapps.com/hc/en-us/articles/360019968212#invoice + """ + __tablename__ = 'wave_cache_invoice' + __table_args__ = ( + sa.UniqueConstraint('id', name='wave_cache_invoice_uq_id'), + sa.ForeignKeyConstraint(['customer_id'], ['wave_cache_customer.id'], + name='wave_cache_invoice_fk_customer'), + ) + __versioned__ = {} + model_title = "Wave Invoice" + + uuid = model.uuid_column() + + id = sa.Column(sa.String(length=100), nullable=False) + internal_id = sa.Column(sa.String(length=100), nullable=True) + + customer_id = sa.Column(sa.String(length=100), nullable=False) + customer = orm.relationship(WaveCacheCustomer, + backref=orm.backref('invoices')) + + status = sa.Column(sa.String(length=10), nullable=False) + title = sa.Column(sa.String(length=255), nullable=False) + subhead = sa.Column(sa.String(length=255), nullable=True) + invoice_number = sa.Column(sa.String(length=10), nullable=False) + invoice_date = sa.Column(sa.Date(), nullable=False) + due_date = sa.Column(sa.Date(), nullable=False) + amount_due = sa.Column(sa.Numeric(precision=9, scale=2), nullable=False) + amount_paid = sa.Column(sa.Numeric(precision=9, scale=2), nullable=False) + tax_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=False) + total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=False) + discount_total = sa.Column(sa.Numeric(precision=9, scale=2), nullable=False) + # currency_code = sa.Column(sa.String(length=3), nullable=False) + # exchange_rate = sa.Column(sa.Numeric(precision=10, scale=5), nullable=False) + created_at = sa.Column(sa.DateTime(), nullable=True) + modified_at = sa.Column(sa.DateTime(), nullable=True) + + def __str__(self): + return self.title or "" diff --git a/rattail_wave/importing/__init__.py b/rattail_wave/importing/__init__.py new file mode 100644 index 0000000..7f2c740 --- /dev/null +++ b/rattail_wave/importing/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +rattail-harvest importing +""" + +from . import model diff --git a/rattail_wave/importing/model.py b/rattail_wave/importing/model.py new file mode 100644 index 0000000..e1b4682 --- /dev/null +++ b/rattail_wave/importing/model.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +rattail-wave model importers +""" + +from rattail.importing.model import ToRattail +from rattail_wave.db import model + + +############################## +# cache models +############################## + +class WaveCacheCustomerImporter(ToRattail): + model_class = model.WaveCacheCustomer + +class WaveCacheInvoiceImporter(ToRattail): + model_class = model.WaveCacheInvoice + + +############################## +# integration models +############################## + +class WaveCustomerImporter(ToRattail): + model_class = model.WaveCustomer diff --git a/rattail_wave/importing/rattail.py b/rattail_wave/importing/rattail.py new file mode 100644 index 0000000..6986e06 --- /dev/null +++ b/rattail_wave/importing/rattail.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Rattail -> Rattail data import for Wave integration +""" + +from rattail.importing import rattail as base +from rattail_wave import importing as rattail_wave_importing + + +class FromRattailToRattailWaveMixin(object): + """ + Add default registration of custom importers + """ + + def add_wave_importers(self, importers): + importers['WaveCacheCustomer'] = WaveCacheCustomerImporter + importers['WaveCacheInvoice'] = WaveCacheInvoiceImporter + importers['WaveCustomer'] = WaveCustomerImporter + return importers + + +############################## +# cache models +############################## + +class WaveCacheCustomerImporter(base.FromRattail, rattail_wave_importing.model.WaveCacheCustomerImporter): + pass + +class WaveCacheInvoiceImporter(base.FromRattail, rattail_wave_importing.model.WaveCacheInvoiceImporter): + pass + + +############################## +# integration models +############################## + +class WaveCustomerImporter(base.FromRattail, rattail_wave_importing.model.WaveCustomerImporter): + pass diff --git a/rattail_wave/importing/versions.py b/rattail_wave/importing/versions.py new file mode 100644 index 0000000..832eb89 --- /dev/null +++ b/rattail_wave/importing/versions.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Rattail -> Rattail "versions" data import +""" + +from rattail.importing import versions as base + + +class WaveVersionMixin(object): + + def add_wave_importers(self, importers): + importers['WaveCacheCustomer'] = WaveCacheCustomerImporter + importers['WaveCacheInvoice'] = WaveCacheInvoiceImporter + importers['WaveCustomer'] = WaveCustomerImporter + return importers + + +class WaveCacheCustomerImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.WaveCacheCustomer + + +class WaveCacheInvoiceImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.WaveCacheInvoice + + +class WaveCustomerImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.WaveCustomer diff --git a/rattail_wave/importing/wave.py b/rattail_wave/importing/wave.py new file mode 100644 index 0000000..df77f29 --- /dev/null +++ b/rattail_wave/importing/wave.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Wave -> Rattail ("wave cache") data import +""" + +import datetime +import decimal + +from gql import Client, gql +from gql.transport.requests import RequestsHTTPTransport + +from rattail import importing +from rattail.util import OrderedDict +from rattail_wave import importing as rattail_wave_importing + + +class FromWaveToRattail(importing.ToRattailHandler): + """ + Import handler for data coming from the Wave API + """ + host_key = 'wave' + host_title = "Wave (API)" + generic_host_title = "Wave (API)" + + def get_importers(self): + importers = OrderedDict() + importers['WaveCacheCustomer'] = WaveCacheCustomerImporter + importers['WaveCacheInvoice'] = WaveCacheInvoiceImporter + return importers + + +class FromWave(importing.Importer): + """ + Base class for all Wave importers + """ + key = 'id' + + @property + def supported_fields(self): + fields = list(super(FromWave, self).supported_fields) + fields.remove('uuid') + return fields + + def setup(self): + super(FromWave, self).setup() + self.setup_wave_api() + + def setup_wave_api(self): + token = self.config.require('wave', 'api.full_access_token') + + self.wave_transport = RequestsHTTPTransport( + url="https://gql.waveapps.com/graphql/public", + headers={'Authorization': 'Bearer {}'.format(token)}, + verify=True, + retries=3, + ) + + self.wave_client = Client(transport=self.wave_transport, + fetch_schema_from_transport=True) + + self.wave_business_id = self.config.require('wave', 'business.id') + + def date_from_wave(self, value): + return datetime.datetime.strptime(value, '%Y-%m-%d').date() + + def money_from_wave(self, value): + return decimal.Decimal('{:0.2f}'.format(value['raw'] / 100.0)) + + def time_from_wave(self, value): + # all wave times appear to come as UTC, so no conversion needed + value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') + return value + + def normalize_host_object(self, obj): + data = dict(obj) + + if 'internal_id' in self.fields: + data['internal_id'] = data.pop('internalId') + + if 'created_at' in self.fields: + data['created_at'] = self.time_from_wave(data.pop('createdAt')) + + if 'modified_at' in self.fields: + data['modified_at'] = self.time_from_wave(data.pop('modifiedAt')) + + return data + + +class WaveCacheCustomerImporter(FromWave, rattail_wave_importing.model.WaveCacheCustomerImporter): + """ + Import customer data from Wave + """ + + def get_host_objects(self): + customers = [] + page = 1 + while True: + + query = gql( + """ + query { + business(id: "%s") { + id + customers(page: %u, pageSize: 20, sort: [NAME_ASC]) { + pageInfo { + currentPage + totalPages + totalCount + } + edges { + node { + id + internalId + name + email + isArchived + createdAt + modifiedAt + } + } + } + } + } + """ % (self.wave_business_id, page) + ) + + result = self.wave_client.execute(query) + data = result['business']['customers'] + customers.extend([edge['node'] for edge in data['edges']]) + + if page >= data['pageInfo']['totalPages']: + break + + page += 1 + + return customers + + def normalize_host_object(self, customer): + data = super(WaveCacheCustomerImporter, self).normalize_host_object(customer) + + data['is_archived'] = data.pop('isArchived') + + return data + + +class WaveCacheInvoiceImporter(FromWave, rattail_wave_importing.model.WaveCacheInvoiceImporter): + """ + Import invoice data from Wave + """ + + def get_host_objects(self): + invoices = [] + page = 1 + while True: + + query = gql( + """ + query { + business(id: "%s") { + id + invoices(page: %u, pageSize: 20) { + pageInfo { + currentPage + totalPages + totalCount + } + edges { + node { + id + internalId + customer { + id + } + status + title + subhead + invoiceNumber + invoiceDate + dueDate + amountDue { + raw + } + amountPaid { + raw + } + taxTotal { + raw + } + total { + raw + } + discountTotal { + raw + } + createdAt + modifiedAt + } + } + } + } + } + """ % (self.wave_business_id, page) + ) + + result = self.wave_client.execute(query) + data = result['business']['invoices'] + invoices.extend([edge['node'] for edge in data['edges']]) + + if page >= data['pageInfo']['totalPages']: + break + + page += 1 + + return invoices + + def normalize_host_object(self, invoice): + data = super(WaveCacheInvoiceImporter, self).normalize_host_object(invoice) + + customer = data.pop('customer') + data['customer_id'] = customer['id'] + + data['invoice_number'] = data.pop('invoiceNumber') + data['invoice_date'] = self.date_from_wave(data.pop('invoiceDate')) + data['due_date'] = self.date_from_wave(data.pop('dueDate')) + data['amount_due'] = self.money_from_wave(data.pop('amountDue')) + data['amount_paid'] = self.money_from_wave(data.pop('amountPaid')) + data['tax_total'] = self.money_from_wave(data.pop('taxTotal')) + data['total'] = self.money_from_wave(data.pop('total')) + data['discount_total'] = self.money_from_wave(data.pop('discountTotal')) + + return data diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..06a2d5e --- /dev/null +++ b/setup.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +rattail-wave setup script +""" + +import os +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'rattail_wave', '_version.py')).read()) +README = open(os.path.join(here, 'README.rst')).read() + + +requires = [ + # + # Version numbers within comments below have specific meanings. + # Basically the 'low' value is a "soft low," and 'high' a "soft high." + # In other words: + # + # If either a 'low' or 'high' value exists, the primary point to be + # made about the value is that it represents the most current (stable) + # version available for the package (assuming typical public access + # methods) whenever this project was started and/or documented. + # Therefore: + # + # If a 'low' version is present, you should know that attempts to use + # versions of the package significantly older than the 'low' version + # may not yield happy results. (A "hard" high limit may or may not be + # indicated by a true version requirement.) + # + # Similarly, if a 'high' version is present, and especially if this + # project has laid dormant for a while, you may need to refactor a bit + # when attempting to support a more recent version of the package. (A + # "hard" low limit should be indicated by a true version requirement + # when a 'high' version is present.) + # + # In any case, developers and other users are encouraged to play + # outside the lines with regard to these soft limits. If bugs are + # encountered then they should be filed as such. + # + # package # low high + + 'invoke', # 1.5.0 + 'rattail[db]', # 0.9.246 +] + + +setup( + name = "rattail-wave", + version = __version__, + author = "Lance Edgar", + author_email = "lance@edbob.org", + url = "https://rattailproject.org/", + description = "Rattail integration package for Wave", + long_description = README, + + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Office/Business', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + install_requires = requires, + packages = find_packages(), + include_package_data = True, + + entry_points = { + + 'rattail.commands': [ + 'import-wave = rattail_wave.commands:ImportWave', + ], + + 'rattail.config.extensions': [ + 'rattail_wave = rattail_wave.config:RattailWaveExtension', + ], + + 'rattail.importing': [ + 'to_rattail.from_wave.import = rattail_wave.importing.wave:FromWaveToRattail', + ], + }, +) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0c51e8c --- /dev/null +++ b/tasks.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 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 . +# +################################################################################ +""" +Tasks for rattail-wave +""" + +import os +import shutil + +from invoke import task + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'rattail_wave', '_version.py')).read()) + + +@task +def release(c): + """ + Release a new version of rattail-wave + """ + # rebuild local tar.gz file for distribution + if os.path.exists('rattail_wave.egg-info'): + shutil.rmtree('rattail_wave.egg-info') + c.run('python -m build --sdist') + + # upload to public PyPI + filename = 'rattail-wave-{}.tar.gz'.format(__version__) + c.run('twine upload dist/{}'.format(filename))