commit 9394171ba5cdbb36d6c46916ffadb41fccacdb42 Author: Lance Edgar Date: Tue Dec 20 19:20:11 2022 -0600 Initial tables and importer stubs, mostly for invoice exports to QB diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0570610 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +rattail_quickbooks.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..693b89d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ + +# Changelog +All notable changes to rattail-quickbooks 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..cb927fa --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *.md +include *.rst +recursive-include rattail_quickbooks/db/alembic *.mako diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2ae85b6 --- /dev/null +++ b/README.rst @@ -0,0 +1,14 @@ + +rattail-quickbooks +================== + +Rattail is a retail software framework, released under the GNU General +Public License. + +This package contains software interfaces for `Quickbooks`_. + +.. _`Quickbooks`: https://quickbooks.intuit.com/ + +Please see the `Rattail Project`_ for more information. + +.. _`Rattail Project`: https://rattailproject.org/ diff --git a/rattail_quickbooks/__init__.py b/rattail_quickbooks/__init__.py new file mode 100644 index 0000000..01ce861 --- /dev/null +++ b/rattail_quickbooks/__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-quickbooks package root +""" + +from ._version import __version__ diff --git a/rattail_quickbooks/_version.py b/rattail_quickbooks/_version.py new file mode 100644 index 0000000..e41b669 --- /dev/null +++ b/rattail_quickbooks/_version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8; -*- + +__version__ = '0.1.0' diff --git a/rattail_quickbooks/db/__init__.py b/rattail_quickbooks/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_quickbooks/db/alembic/versions/72134a69c576_initial_integration_tables.py b/rattail_quickbooks/db/alembic/versions/72134a69c576_initial_integration_tables.py new file mode 100644 index 0000000..fe235fb --- /dev/null +++ b/rattail_quickbooks/db/alembic/versions/72134a69c576_initial_integration_tables.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8; -*- +"""initial integration tables + +Revision ID: 72134a69c576 +Revises: dc28b97c33ff +Create Date: 2022-12-08 15:19:54.448794 + +""" + +from __future__ import unicode_literals, absolute_import + +# revision identifiers, used by Alembic. +revision = '72134a69c576' +down_revision = None +branch_labels = ('rattail_quickbooks',) +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # quickbooks_store + op.create_table('quickbooks_store', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('quickbooks_location', sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint(['uuid'], ['store.uuid'], name='quickbooks_store_fk_store'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('quickbooks_store_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('quickbooks_location', 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_quickbooks_store_version_end_transaction_id'), 'quickbooks_store_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_quickbooks_store_version_operation_type'), 'quickbooks_store_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_quickbooks_store_version_transaction_id'), 'quickbooks_store_version', ['transaction_id'], unique=False) + + # quickbooks_department + op.create_table('quickbooks_department', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('quickbooks_expense_account', sa.String(length=100), nullable=True), + sa.Column('quickbooks_expense_class', sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint(['uuid'], ['department.uuid'], name='quickbooks_department_fk_department'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('quickbooks_department_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('quickbooks_expense_account', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_expense_class', 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_quickbooks_department_version_end_transaction_id'), 'quickbooks_department_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_quickbooks_department_version_operation_type'), 'quickbooks_department_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_quickbooks_department_version_transaction_id'), 'quickbooks_department_version', ['transaction_id'], unique=False) + + # quickbooks_vendor + op.create_table('quickbooks_vendor', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('quickbooks_name', sa.String(length=100), nullable=True), + sa.Column('quickbooks_bank_account', sa.String(length=100), nullable=True), + sa.Column('quickbooks_terms', sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint(['uuid'], ['vendor.uuid'], name='quickbooks_vendor_fk_vendor'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('quickbooks_vendor_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('quickbooks_name', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_bank_account', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_terms', 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_quickbooks_vendor_version_end_transaction_id'), 'quickbooks_vendor_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_quickbooks_vendor_version_operation_type'), 'quickbooks_vendor_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_quickbooks_vendor_version_transaction_id'), 'quickbooks_vendor_version', ['transaction_id'], unique=False) + + # quickbooks_exportable_invoice + op.create_table('quickbooks_exportable_invoice', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('store_id', sa.String(length=4), nullable=True), + sa.Column('vendor_id', sa.String(length=14), nullable=True), + sa.Column('txn_id', sa.String(length=20), nullable=True), + sa.Column('invoice_number', sa.String(length=20), nullable=True), + sa.Column('invoice_date', sa.Date(), nullable=True), + sa.Column('invoice_total', sa.Numeric(precision=8, scale=2), nullable=False), + sa.Column('store_uuid', sa.String(length=32), nullable=True), + sa.Column('vendor_uuid', sa.String(length=32), nullable=True), + sa.Column('quickbooks_vendor_name', sa.String(length=100), nullable=True), + sa.Column('quickbooks_vendor_terms', sa.String(length=100), nullable=True), + sa.Column('quickbooks_bank_account', sa.String(length=100), nullable=True), + sa.Column('quickbooks_export_template', sa.String(length=100), nullable=True), + sa.Column('status_code', sa.Integer(), nullable=False), + sa.Column('status_text', sa.String(length=255), nullable=True), + sa.Column('deleted', sa.DateTime(), nullable=True), + sa.Column('deleted_by_uuid', sa.String(length=32), nullable=True), + sa.Column('exported', sa.DateTime(), nullable=True), + sa.Column('exported_by_uuid', sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(['deleted_by_uuid'], ['user.uuid'], name='quickbooks_exportable_invoice_fk_deleted_by'), + sa.ForeignKeyConstraint(['exported_by_uuid'], ['user.uuid'], name='quickbooks_exportable_invoice_fk_exported_by'), + sa.ForeignKeyConstraint(['store_uuid'], ['store.uuid'], name='quickbooks_exportable_invoice_fk_store'), + sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'], name='quickbooks_exportable_invoice_fk_vendor'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('quickbooks_exportable_invoice_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('store_id', sa.String(length=4), autoincrement=False, nullable=True), + sa.Column('vendor_id', sa.String(length=14), autoincrement=False, nullable=True), + sa.Column('txn_id', sa.String(length=20), autoincrement=False, nullable=True), + sa.Column('invoice_number', sa.String(length=20), autoincrement=False, nullable=True), + sa.Column('invoice_date', sa.Date(), autoincrement=False, nullable=True), + sa.Column('invoice_total', sa.Numeric(precision=8, scale=2), autoincrement=False, nullable=True), + sa.Column('store_uuid', sa.String(length=32), autoincrement=False, nullable=True), + sa.Column('vendor_uuid', sa.String(length=32), autoincrement=False, nullable=True), + sa.Column('quickbooks_vendor_name', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_vendor_terms', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_bank_account', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('quickbooks_export_template', sa.String(length=100), autoincrement=False, nullable=True), + sa.Column('status_code', sa.Integer(), autoincrement=False, nullable=True), + sa.Column('status_text', sa.String(length=255), autoincrement=False, nullable=True), + sa.Column('deleted', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('deleted_by_uuid', sa.String(length=32), autoincrement=False, nullable=True), + sa.Column('exported', sa.DateTime(), autoincrement=False, nullable=True), + sa.Column('exported_by_uuid', sa.String(length=32), 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_quickbooks_exportable_invoice_version_end_transaction_id'), 'quickbooks_exportable_invoice_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_quickbooks_exportable_invoice_version_operation_type'), 'quickbooks_exportable_invoice_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_quickbooks_exportable_invoice_version_transaction_id'), 'quickbooks_exportable_invoice_version', ['transaction_id'], unique=False) + + # quickbooks_exportable_invoice_dist + op.create_table('quickbooks_exportable_invoice_dist', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('invoice_uuid', sa.String(length=32), nullable=False), + sa.Column('department_id', sa.Integer(), nullable=False), + sa.Column('source_amount', sa.Numeric(precision=8, scale=2), nullable=False), + sa.Column('department_uuid', sa.String(length=32), nullable=True), + sa.Column('quickbooks_expense_account', sa.String(length=100), nullable=True), + sa.Column('quickbooks_expense_class', sa.String(length=100), nullable=True), + sa.Column('calculated_percent', sa.Numeric(precision=8, scale=3), nullable=True), + sa.Column('calculated_amount', sa.Numeric(precision=8, scale=2), nullable=True), + sa.Column('status_code', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['department_uuid'], ['department.uuid'], name='quickbooks_exportable_invoice_dist_fk_department'), + sa.ForeignKeyConstraint(['invoice_uuid'], ['quickbooks_exportable_invoice.uuid'], name='quickbooks_exportable_invoice_dist_fk_invoice'), + sa.PrimaryKeyConstraint('uuid') + ) + + # quickbooks_invoice_export + op.create_table('quickbooks_invoice_export', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('created_by_uuid', sa.String(length=32), nullable=False), + sa.Column('record_count', sa.Integer(), nullable=True), + sa.Column('filename', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name='quickbooks_invoice_export_fk_created_by'), + sa.PrimaryKeyConstraint('uuid') + ) + + +def downgrade(): + + # quickbooks_invoice_export + op.drop_table('quickbooks_invoice_export') + + # quickbooks_exportable_invoice_dist + op.drop_table('quickbooks_exportable_invoice_dist') + + # quickbooks_exportable_invoice_dist + op.drop_index(op.f('ix_quickbooks_exportable_invoice_version_transaction_id'), table_name='quickbooks_exportable_invoice_version') + op.drop_index(op.f('ix_quickbooks_exportable_invoice_version_operation_type'), table_name='quickbooks_exportable_invoice_version') + op.drop_index(op.f('ix_quickbooks_exportable_invoice_version_end_transaction_id'), table_name='quickbooks_exportable_invoice_version') + op.drop_table('quickbooks_exportable_invoice_version') + op.drop_table('quickbooks_exportable_invoice') + + # quickbooks_vendor + op.drop_index(op.f('ix_quickbooks_vendor_version_transaction_id'), table_name='quickbooks_vendor_version') + op.drop_index(op.f('ix_quickbooks_vendor_version_operation_type'), table_name='quickbooks_vendor_version') + op.drop_index(op.f('ix_quickbooks_vendor_version_end_transaction_id'), table_name='quickbooks_vendor_version') + op.drop_table('quickbooks_vendor_version') + op.drop_table('quickbooks_vendor') + + # quickbooks_department + op.drop_index(op.f('ix_quickbooks_department_version_transaction_id'), table_name='quickbooks_department_version') + op.drop_index(op.f('ix_quickbooks_department_version_operation_type'), table_name='quickbooks_department_version') + op.drop_index(op.f('ix_quickbooks_department_version_end_transaction_id'), table_name='quickbooks_department_version') + op.drop_table('quickbooks_department_version') + op.drop_table('quickbooks_department') + + # quickbooks_store + op.drop_index(op.f('ix_quickbooks_store_version_transaction_id'), table_name='quickbooks_store_version') + op.drop_index(op.f('ix_quickbooks_store_version_operation_type'), table_name='quickbooks_store_version') + op.drop_index(op.f('ix_quickbooks_store_version_end_transaction_id'), table_name='quickbooks_store_version') + op.drop_table('quickbooks_store_version') + op.drop_table('quickbooks_store') diff --git a/rattail_quickbooks/db/model/__init__.py b/rattail_quickbooks/db/model/__init__.py new file mode 100644 index 0000000..d2dd403 --- /dev/null +++ b/rattail_quickbooks/db/model/__init__.py @@ -0,0 +1,32 @@ +# -*- 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 . +# +################################################################################ +""" +Quickbooks integration data models +""" + +from .stores import QuickbooksStore +from .org import QuickbooksDepartment +from .vendors import QuickbooksVendor +from .invoices import (QuickbooksExportableInvoice, + QuickbooksExportableInvoiceDistribution, + QuickbooksInvoiceExport) diff --git a/rattail_quickbooks/db/model/invoices.py b/rattail_quickbooks/db/model/invoices.py new file mode 100644 index 0000000..0c001f1 --- /dev/null +++ b/rattail_quickbooks/db/model/invoices.py @@ -0,0 +1,302 @@ +# -*- 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 . +# +################################################################################ +""" +Invoice data models +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model +from rattail.db.core import filename_column +from rattail.util import OrderedDict + + +class QuickbooksExportableInvoice(model.Base): + """ + Represents a vendor invoice capable of being exported to Quickbooks. + """ + __tablename__ = 'quickbooks_exportable_invoice' + __table_args__ = ( + sa.ForeignKeyConstraint(['store_uuid'], ['store.uuid'], + name='quickbooks_exportable_invoice_fk_store'), + sa.ForeignKeyConstraint(['vendor_uuid'], ['vendor.uuid'], + name='quickbooks_exportable_invoice_fk_vendor'), + sa.ForeignKeyConstraint(['deleted_by_uuid'], ['user.uuid'], + name='quickbooks_exportable_invoice_fk_deleted_by'), + sa.ForeignKeyConstraint(['exported_by_uuid'], ['user.uuid'], + name='quickbooks_exportable_invoice_fk_exported_by'), + ) + __versioned__ = {} + + STATUS_NOT_YET_REFRESHED = 1 + STATUS_EXPORTABLE = 2 + STATUS_STORE_NOT_FOUND = 3 + STATUS_VENDOR_NOT_FOUND = 4 + STATUS_VENDOR_BAD_INFO = 5 + STATUS_PO_NOT_FOUND = 6 + STATUS_PO_BAD_INFO = 7 + STATUS_DIST_PROBLEMS = 8 + STATUS_DEPTS_IGNORED = 9 + STATUS_EXPORTED = 10 + STATUS_DELETED = 11 + + STATUS = OrderedDict([ + (STATUS_NOT_YET_REFRESHED, "data not yet refreshed"), + (STATUS_EXPORTABLE, "exportable"), + (STATUS_STORE_NOT_FOUND, "store not found"), + (STATUS_VENDOR_NOT_FOUND, "vendor not found"), + (STATUS_VENDOR_BAD_INFO, "vendor info is invalid"), + (STATUS_PO_NOT_FOUND, "PO not found"), + (STATUS_PO_BAD_INFO, "PO info is invalid"), + (STATUS_DIST_PROBLEMS, "see distribution problem(s)"), + (STATUS_DEPTS_IGNORED, "all departments ignored"), + (STATUS_EXPORTED, "exported"), + (STATUS_DELETED, "deleted"), + ]) + + uuid = model.uuid_column() + + ################################################## + # columns whose values come from source data + ################################################## + + store_id = sa.Column(sa.String(length=4), nullable=True, doc=""" + ID string for the store which must pay the invoice. + """) + + vendor_id = sa.Column(sa.String(length=14), nullable=True, doc=""" + ID string for the vendor which sent the invoice. + """) + + txn_id = sa.Column(sa.String(length=20), nullable=True, doc=""" + ID string for the transaction represented by the invoice, if + applicable. + """) + + invoice_number = sa.Column(sa.String(length=20), nullable=True, doc=""" + Invoice number, per source data. + """) + + invoice_date = sa.Column(sa.Date(), nullable=True, doc=""" + Date of the invoice, per source data. + """) + + invoice_total = sa.Column(sa.Numeric(precision=8, scale=2), nullable=False, doc=""" + Total amount for the invoice, per source data. + """) + + ################################################## + # columns whose values come from data refresh + ################################################## + + store_uuid = sa.Column(sa.String(length=32), nullable=True) + store = orm.relationship( + model.Store, + doc=""" + Reference to the :class:`rattail:~rattail.db.model.Store` + instance associated with the invoice, or ``None``. + """) + + vendor_uuid = sa.Column(sa.String(length=32), nullable=True) + vendor = orm.relationship( + model.Vendor, + doc=""" + Reference to the vendor associated with the invoice, or + ``None``. + """, + backref=orm.backref( + 'dtail_qbo_invoices', + cascade='all')) + + quickbooks_vendor_name = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks name for the invoice vendor. + """) + + quickbooks_vendor_terms = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks terms for the invoice vendor. + """) + + quickbooks_bank_account = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks bank account for the invoice vendor. + """) + + quickbooks_export_template = sa.Column(sa.String(length=100), nullable=True, doc=""" + Name of the Quickbooks export template to use for this invoice. + """) + + status_code = sa.Column(sa.Integer(), nullable=False, doc=""" + Status code for the invoice; indicates whether it's exportable etc. + """) + + status_text = sa.Column(sa.String(length=255), nullable=True, doc=""" + Extra text relating to the invoice status, if applicable. + """) + + ################################################## + # other columns + ################################################## + + deleted = sa.Column(sa.DateTime(), nullable=True, doc=""" + Timestamp when the invoice was marked as deleted, or ``None``. + """) + + deleted_by_uuid = sa.Column(sa.String(length=32), nullable=True) + deleted_by = orm.relationship( + model.User, + primaryjoin='User.uuid == QuickbooksExportableInvoice.deleted_by_uuid', + doc=""" + Reference to the :class:`rattail:~rattail.db.model.User` + instance who marked the invoice as deleted, or ``None``. + """) + + exported = sa.Column(sa.DateTime(), nullable=True, doc=""" + Timestamp when the invoice was exported, or ``None``. + """) + + exported_by_uuid = sa.Column(sa.String(length=32), nullable=True) + exported_by = orm.relationship( + model.User, + primaryjoin='User.uuid == QuickbooksExportableInvoice.exported_by_uuid', + doc=""" + Reference to the :class:`rattail:~rattail.db.model.User` + instance who exported the invoice, or ``None``. + """) + + def __str__(self): + return "{}, {}, {} (trans. {})".format(self.store_id, + self.vendor_id, + self.invoice_number, + self.txn_id) + + def add_distribution(self, **kwargs): + dist = QuickbooksExportableInvoiceDistribution(**kwargs) + if not dist.status_code: + dist.status_code = dist.STATUS_NOT_YET_REFRESHED + self.distributions.append(dist) + + +class QuickbooksExportableInvoiceDistribution(model.Base): + """ + Represents a "distribution" for a department, within the context + of a vendor invoice which is to be exported to Quickbooks. + """ + __tablename__ = 'quickbooks_exportable_invoice_dist' + __table_args__ = ( + sa.ForeignKeyConstraint(['invoice_uuid'], ['quickbooks_exportable_invoice.uuid'], + name='quickbooks_exportable_invoice_dist_fk_invoice'), + sa.ForeignKeyConstraint(['department_uuid'], ['department.uuid'], + name='quickbooks_exportable_invoice_dist_fk_department'), + ) + + STATUS_NOT_YET_REFRESHED = 1 + STATUS_EXPORTABLE = 2 + STATUS_DEPT_NOT_FOUND = 3 + STATUS_DEPT_IGNORED = 4 + STATUS_DEPT_BAD_INFO = 5 + STATUS_EXPORTED = 6 + + STATUS = OrderedDict([ + (STATUS_NOT_YET_REFRESHED, "data not yet refreshed"), + (STATUS_EXPORTABLE, "exportable"), + (STATUS_DEPT_NOT_FOUND, "department not found"), + (STATUS_DEPT_IGNORED, "department ignored"), + (STATUS_DEPT_BAD_INFO, "department info is invalid"), + (STATUS_EXPORTED, "exported"), + ]) + + uuid = model.uuid_column() + + invoice_uuid = sa.Column(sa.String(length=32), nullable=False) + invoice = orm.relationship( + QuickbooksExportableInvoice, + doc=""" + Reference to the parent invoice. + """, + backref=orm.backref( + 'distributions', + cascade='all', + doc=""" + Sequence of + :class:`QuickbooksExportableInvoiceDistribution` instances + which belong to the invoice. + """)) + + ################################################## + # columns whose values come from source data + ################################################## + + department_id = sa.Column(sa.Integer(), nullable=False, doc=""" + Department ID for the distribution, per source data. + """) + + source_amount = sa.Column(sa.Numeric(precision=8, scale=2), nullable=False, doc=""" + Dollar amount for the distribution, per source data. + """) + + ################################################## + # columns whose values come from data refresh + ################################################## + + department_uuid = sa.Column(sa.String(length=32), nullable=True) + department = orm.relationship( + model.Department, + doc=""" + Reference to the :class:`rattail:~rattail.db.model.Department` + instance associated with the distribution, or ``None``. + """) + + quickbooks_expense_account = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks expense account for the department. + """) + + quickbooks_expense_class = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks expense class for the department. + """) + + calculated_percent = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc=""" + Percentage of the invoice total which should be considered + attributable to the current distribution (department). This is + calculated after taking "ignored" departments into consideration, + etc. + """) + + calculated_amount = sa.Column(sa.Numeric(precision=8, scale=2), nullable=True, doc=""" + Dollar amount for the distribution, per business logic. + """) + + status_code = sa.Column(sa.Integer(), nullable=False, doc=""" + Status code for the distribution; indicates whether it's + exportable etc. + """) + + +class QuickbooksInvoiceExport(model.ExportMixin, model.Base): + """ + Invoice export file, for *consumption* by Quickbooks. + """ + __tablename__ = 'quickbooks_invoice_export' + + filename = filename_column(nullable=True, doc=""" + Filename for the export. + """) diff --git a/rattail_quickbooks/db/model/org.py b/rattail_quickbooks/db/model/org.py new file mode 100644 index 0000000..b74adc5 --- /dev/null +++ b/rattail_quickbooks/db/model/org.py @@ -0,0 +1,69 @@ +# -*- 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 . +# +################################################################################ +""" +Organizational data model extensions +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model + + +class QuickbooksDepartment(model.Base): + """ + Quickbooks extensions to core Department model + """ + __tablename__ = 'quickbooks_department' + __table_args__ = ( + sa.ForeignKeyConstraint(['uuid'], ['department.uuid'], + name='quickbooks_department_fk_department'), + ) + __versioned__ = {} + + uuid = model.uuid_column(default=None) + + department = orm.relationship( + model.Department, + doc=""" + Department to which this extension record pertains. + """, + backref=orm.backref( + '_quickbooks', + uselist=False, + cascade='all, delete-orphan', + doc=""" + Quickbooks extension record for the department. + """)) + + quickbooks_expense_account = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "expense account" for the department. + """) + + quickbooks_expense_class = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "expense class" for the department. + """) + + +QuickbooksDepartment.make_proxy(model.Department, '_quickbooks', 'quickbooks_expense_account') +QuickbooksDepartment.make_proxy(model.Department, '_quickbooks', 'quickbooks_expense_class') diff --git a/rattail_quickbooks/db/model/stores.py b/rattail_quickbooks/db/model/stores.py new file mode 100644 index 0000000..ce751b5 --- /dev/null +++ b/rattail_quickbooks/db/model/stores.py @@ -0,0 +1,64 @@ +# -*- 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 . +# +################################################################################ +""" +Store data model extensions +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model + + +class QuickbooksStore(model.Base): + """ + Quickbooks extensions to core Store model + """ + __tablename__ = 'quickbooks_store' + __table_args__ = ( + sa.ForeignKeyConstraint(['uuid'], ['store.uuid'], + name='quickbooks_store_fk_store'), + ) + __versioned__ = {} + + uuid = model.uuid_column(default=None) + + store = orm.relationship( + model.Store, + doc=""" + Store to which this extension record pertains. + """, + backref=orm.backref( + '_quickbooks', + uselist=False, + cascade='all, delete-orphan', + doc=""" + Quickbooks extension record for the store. + """)) + + quickbooks_location = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "location" for the store. + """) + + +QuickbooksStore.make_proxy(model.Store, '_quickbooks', 'quickbooks_location') diff --git a/rattail_quickbooks/db/model/vendors.py b/rattail_quickbooks/db/model/vendors.py new file mode 100644 index 0000000..5918aec --- /dev/null +++ b/rattail_quickbooks/db/model/vendors.py @@ -0,0 +1,74 @@ +# -*- 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 . +# +################################################################################ +""" +Vendor data model extensions +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from rattail.db import model + + +class QuickbooksVendor(model.Base): + """ + Quickbooks extensions to core Vendor model + """ + __tablename__ = 'quickbooks_vendor' + __table_args__ = ( + sa.ForeignKeyConstraint(['uuid'], ['vendor.uuid'], + name='quickbooks_vendor_fk_vendor'), + ) + __versioned__ = {} + + uuid = model.uuid_column(default=None) + + vendor = orm.relationship( + model.Vendor, + doc=""" + Vendor to which this extension record pertains. + """, + backref=orm.backref( + '_quickbooks', + uselist=False, + cascade='all, delete-orphan', + doc=""" + Quickbooks extension record for the vendor. + """)) + + quickbooks_name = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "name" for the vendor. + """) + + quickbooks_bank_account = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "bank account" for the vendor. + """) + + quickbooks_terms = sa.Column(sa.String(length=100), nullable=True, doc=""" + Quickbooks "terms" for the vendor. + """) + + +QuickbooksVendor.make_proxy(model.Vendor, '_quickbooks', 'quickbooks_name') +QuickbooksVendor.make_proxy(model.Vendor, '_quickbooks', 'quickbooks_bank_account') +QuickbooksVendor.make_proxy(model.Vendor, '_quickbooks', 'quickbooks_terms') diff --git a/rattail_quickbooks/importing/__init__.py b/rattail_quickbooks/importing/__init__.py new file mode 100644 index 0000000..41287be --- /dev/null +++ b/rattail_quickbooks/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-quickbooks importing +""" + +from . import model diff --git a/rattail_quickbooks/importing/model.py b/rattail_quickbooks/importing/model.py new file mode 100644 index 0000000..24fb9db --- /dev/null +++ b/rattail_quickbooks/importing/model.py @@ -0,0 +1,70 @@ +# -*- 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-quickbooks model importers +""" + +from rattail import importing + + +############################## +# core importer overrides +############################## + +class DepartmentImporter(importing.model.DepartmentImporter): + + extension_attr = '_quickbooks' + extension_fields = [ + 'quickbooks_expense_account', + 'quickbooks_expense_class', + ] + + +class VendorImporter(importing.model.VendorImporter): + + extension_attr = '_quickbooks' + extension_fields = [ + 'quickbooks_name', + 'quickbooks_bank_account', + 'quickbooks_terms', + ] + + +############################## +# custom models +############################## + +class QuickbooksExportableInvoiceImporter(importing.model.ToRattail): + + def get_model_class(self): + return self.model.QuickbooksExportableInvoice + + def cache_query(self): + query = super(QuickbooksExportableInvoiceImporter, self).cache_query() + model = self.model + # TODO: possibly should filter this way only if config says so? + if self.start_date: + query = query.filter(model.QuickbooksExportableInvoice.invoice_date >= self.start_date) + if self.end_date: + query = query.filter(model.QuickbooksExportableInvoice.invoice_date <= self.end_date) + return query diff --git a/rattail_quickbooks/importing/versions.py b/rattail_quickbooks/importing/versions.py new file mode 100644 index 0000000..f5a07e3 --- /dev/null +++ b/rattail_quickbooks/importing/versions.py @@ -0,0 +1,41 @@ +# -*- 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" import w/ Quickbooks schema +""" + +from rattail.importing import versions as base + + +class QuickbooksVersionMixin(object): + + def add_quickbooks_importers(self, importers): + importers['QuickbooksExportableInvoiceImporter'] = QuickbooksExportableInvoiceImporter + return importers + + +class QuickbooksExportableInvoiceImporter(base.VersionImporter): + + @property + def host_model_class(self): + return self.model.QuickbooksExportableInvoice diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7d6e7a9 --- /dev/null +++ b/setup.py @@ -0,0 +1,95 @@ +# -*- 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-quickbooks 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_quickbooks', '_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 + + 'rattail', # 0.9.246 +] + + +setup( + name = "rattail-quickbooks", + version = __version__, + author = "Lance Edgar", + author_email = "lance@edbob.org", + url = "https://rattailproject.org/", + description = "Rattail integration package for Quickbooks", + 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, + +) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..82c6131 --- /dev/null +++ b/tasks.py @@ -0,0 +1,46 @@ +# -*- 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-quickbooks +""" + +import os + +from invoke import task + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'rattail_quickbooks', '_version.py')).read()) + + +@task +def release(c): + """ + Release a new version of rattail-quickbooks + """ + # rebuild local tar.gz file for distribution + c.run('rm -rf rattail_quickbooks.egg-info') + c.run('python setup.py sdist --formats=gztar') + + filename = 'rattail-quickbooks-{}.tar.gz'.format(__version__) + c.run('twine upload dist/{}'.format(filename))