diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..9f91ecb --- /dev/null +++ b/.drone.yml @@ -0,0 +1,49 @@ +clone: + hg: + image: plugins/hg + +pipeline: + tox: + image: ${IMAGE} + environment: + - CFLAGS=-O0 + - DB_CACHE=/cache + - TOX_TESTENV_PASSENV=CFLAGS DB_CACHE + - POSTGRESQL_URI=postgresql://postgres@postgresql:5432/ + commands: + - pip install tox + - tox -e "${TOXENV}-${DATABASE}" + +services: + postgresql: + image: postgres + when: + matrix: + DATABASE: postgresql + +matrix: + include: + - IMAGE: python:2.7 + TOXENV: py27 + DATABASE: sqlite + - IMAGE: python:2.7 + TOXENV: py27 + DATABASE: postgresql + - IMAGE: python:3.4 + TOXENV: py34 + DATABASE: sqlite + - IMAGE: python:3.4 + TOXENV: py34 + DATABASE: postgresql + - IMAGE: python:3.5 + TOXENV: py35 + DATABASE: sqlite + - IMAGE: python:3.5 + TOXENV: py35 + DATABASE: postgresql + - IMAGE: python:3.6 + TOXENV: py36 + DATABASE: sqlite + - IMAGE: python:3.6 + TOXENV: py36 + DATABASE: postgresql diff --git a/account.py b/account.py index 77f8e60..3ed65d7 100644 --- a/account.py +++ b/account.py @@ -1,13 +1,10 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. -from trytond.model import ModelSQL, ModelView, fields -from trytond.wizard import Wizard, StateView, StateTransition, Button -from trytond.pool import Pool, PoolMeta -from trytond.pyson import Eval, And, Bool -from trytond.transaction import Transaction -from .aeat import (OPERATION_KEY, BOOK_KEY, SEND_SPECIAL_REGIME_KEY, - RECEIVE_SPECIAL_REGIME_KEY, AEAT_INVOICE_STATE, IVA_SUBJECTED, - EXCEMPTION_CAUSE, INTRACOMUNITARY_TYPE) +from trytond.model import fields +from trytond.pool import PoolMeta +from .aeat import (BOOK_KEY, SEND_SPECIAL_REGIME_KEY, + RECEIVE_SPECIAL_REGIME_KEY, IVA_SUBJECTED, EXCEMPTION_CAUSE, + INTRACOMUNITARY_TYPE) __all__ = ['TemplateTax', 'Tax'] diff --git a/aeat.py b/aeat.py index 1cc3cec..c3475e9 100644 --- a/aeat.py +++ b/aeat.py @@ -28,6 +28,7 @@ _ZERO = Decimal('0.0') # AEAT SII test SII_TEST = config.getboolean('aeat', 'sii_test', default=True) + def _decimal(x): return Decimal(x) if x is not None else None @@ -46,7 +47,7 @@ COMMUNICATION_TYPE = [ # L0 # ('A4', 'Amendment of Invoice for Travellers'), # Not suported ('C0', 'Query Invoices'), # Not in L0 ('D0', 'Delete Invoices'), # Not In L0 -] + ] BOOK_KEY = [ (None, ''), @@ -54,13 +55,14 @@ BOOK_KEY = [ ('I', 'Investment Goods'), ('R', 'Received Invoices'), ('U', 'Particular Intracommunity Operations'), -] + ] OPERATION_KEY = [ # L2_EMI - L2_RECI (None, ''), ('F1', 'Invoice'), ('F2', 'Simplified Invoice (ticket)'), - ('R1', 'Corrected Invoice (Art 80.1, 80.2 and 80.6 and error grounded in law)'), + ('R1', 'Corrected Invoice ' + '(Art 80.1, 80.2 and 80.6 and error grounded in law)'), ('R2', 'Corrected Invoice (Art. 80.3)'), ('R3', 'Credit Note (Art 80.4)'), ('R4', 'Corrected Invoice (Other)'), @@ -69,19 +71,18 @@ OPERATION_KEY = [ # L2_EMI - L2_RECI ('F4', 'Invoice summary entry'), ('F5', 'Import (DUA)'), ('F6', 'Other accounting documents'), - -] + ] PARTY_IDENTIFIER_TYPE = [ (None, ''), ('02', 'NIF-VAT'), ('03', 'Passport'), ('04', 'Official identification document issued by the country ' - 'or region of residence'), + 'or region of residence'), ('05', 'Residence certificate'), ('06', 'Other supporting document'), ('07', 'Not registered'), -] + ] SEND_SPECIAL_REGIME_KEY = [ # L3.1 @@ -89,58 +90,58 @@ SEND_SPECIAL_REGIME_KEY = [ # L3.1 ('01', 'General tax regime activity'), ('02', 'Export'), ('03', 'Activities to which the special scheme of used goods, ' - 'works of art, antiquities and collectables (135-139 of the VAT Law)'), + 'works of art, antiquities and collectables (135-139 of the VAT Law)'), ('04', 'Special scheme for investment gold'), ('05', 'Special scheme for travel agencies'), ('06', 'Special scheme applicable to groups of entities, VAT (Advanced)'), ('07', 'Special cash basis scheme'), ('08', 'Activities subject to Canary Islands General Indirect Tax/Tax on ' - 'Production, Services and Imports'), + 'Production, Services and Imports'), ('09', 'Invoicing of the provision of travel agency services acting as ' - 'intermediaries in the name of and on behalf of other persons ' - '(Additional Provision 4, Royal Decree 1619/2012)'), + 'intermediaries in the name of and on behalf of other persons ' + '(Additional Provision 4, Royal Decree 1619/2012)'), ('10', 'Collections on behalf of third parties of professional fees or ' - 'industrial property, copyright or other such rights by partners, ' - 'associates or members undertaken by companies, associations, ' - 'professional organisations or other entities that, amongst their ' - 'functions, undertake collections'), + 'industrial property, copyright or other such rights by partners, ' + 'associates or members undertaken by companies, associations, ' + 'professional organisations or other entities that, amongst their ' + 'functions, undertake collections'), ('11', 'Business premises lease activities subject to withholding'), ('12', 'Business premises lease activities not subject to withholding'), ('13', 'Business premises lease activities subject and not subject ' - 'to withholding'), + 'to withholding'), ('14', 'Invoice with VAT pending accrual (work certifications with Public ' - 'Administration recipients)'), + 'Administration recipients)'), ('15', 'Invoice with VAT pending accrual - ' - 'operations of successive tract'), + 'operations of successive tract'), ('16', 'First semester 2017'), -] + ] RECEIVE_SPECIAL_REGIME_KEY = [ (None, ''), ('01', 'General tax regime activity'), ('02', 'Activities through which businesses pay compensation for special ' - 'VAT arrangements for agriculture and fisheries'), - ('03', 'Activities to which the special scheme of used goods, works of art, ' - 'antiquities and collectables (135-139 of the VAT Law)'), + 'VAT arrangements for agriculture and fisheries'), + ('03', 'Activities to which the special scheme of used goods, ' + 'works of art, antiquities and collectables (135-139 of the VAT Law)'), ('04', 'Special scheme for investment gold'), ('05', 'Special scheme for travel agencies'), ('06', 'Special scheme applicable to groups of entities, VAT (Advanced)'), ('07', 'Special cash basis scheme'), ('08', 'Activities subject to Canary Islands General Indirect Tax/Tax ' - 'on Production, Services and Imports'), + 'on Production, Services and Imports'), ('09', 'Intra-Community acquisition of assets and provisions of services'), ('12', 'Business premises lease activities'), ('13', 'Invoice corresponding to an import ' - '(reported without been associated with a DUA)'), + '(reported without been associated with a DUA)'), ('14', 'First semester 2017'), -] + ] AEAT_COMMUNICATION_STATE = [ (None, ''), ('Correcto', 'Accepted'), ('ParcialmenteCorrecto', 'Partially Accepted'), ('Incorrecto', 'Rejected') -] + ] AEAT_INVOICE_STATE = [ (None, ''), @@ -156,13 +157,13 @@ AEAT_INVOICE_STATE = [ PROPERTY_STATE = [ # L6 ('0', ''), ('1', '1. Property with a land register reference located in any part ' - 'of Spain, with the exception of the Basque Country and Navarre'), + 'of Spain, with the exception of the Basque Country and Navarre'), ('2', '2. Property located in the Autonomous Community of the Basque ' - 'Country or the Chartered Community of Navarre.'), - ('3', '3. Property in any of the foregoing locations with no land register ' - 'reference'), + 'Country or the Chartered Community of Navarre.'), + ('3', '3. Property in any of the foregoing locations ' + 'with no land register reference'), ('4', '4. Property located abroad'), -] + ] # L7 - Iva Subjected @@ -171,8 +172,8 @@ IVA_SUBJECTED = [ ('S1', 'Subject - Not exempt. Non VAT reverse charge'), ('S2', 'Subject - Not exempt. VAT reverse charge'), ('S3', 'Subject - Not exempt. Both non VAT reverse charge ' - 'and VAT reverse charge') -] + 'and VAT reverse charge') + ] # L9 - Excemption cause EXCEMPTION_CAUSE = [ @@ -183,26 +184,26 @@ EXCEMPTION_CAUSE = [ ('E4', 'Exempt on account of Article 23 and Article 24'), ('E5', 'Exempt on account of Article 25'), ('E6', 'Exempt on other grounds'), -] + ] # L11 Payment Type PAYMENT_TYPE = [ ('01', 'Transfer'), ('02', 'Cheque'), ('03', 'Not to be collected/paid (deadline for accrual/forced accrual ' - 'as part of insolvency proceedings)'), + 'as part of insolvency proceedings)'), ('04', 'Other methods of collection/payment') -] + ] # L12 INTRACOMUNITARY_TYPE = [ (None, ''), ('A', 'The transmission or receipt of goods to undertake partial reports ' - 'or works stipulated in Article 70, section one, Number 7 ' - 'of the Tax Law (Law 37/1992)'), + 'or works stipulated in Article 70, section one, Number 7 ' + 'of the Tax Law (Law 37/1992)'), ('B', 'Transfers of goods or intra-Community acquisitions of goods listed ' - 'in Article 9.3 and Article 16.2 of the Tax Law (Law 37/1992)'), -] + 'in Article 9.3 and Article 16.2 of the Tax Law (Law 37/1992)'), + ] def remove_accents(unicode_string): @@ -241,10 +242,12 @@ class SIIReport(Workflow, ModelSQL, ModelView): states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) - company_vat = fields.Char('VAT', size=9, states={ + company_vat = fields.Char('VAT', size=9, + states={ 'required': Eval('state').in_(['confirmed', 'done']), 'readonly': ~Eval('state').in_(['draft', 'confirmed']), - }, depends=['state']) + }, + depends=['state']) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', @@ -275,21 +278,21 @@ class SIIReport(Workflow, ModelSQL, ModelView): ('done', 'Done'), ('cancelled', 'Cancelled'), ('sent', 'Sent'), - ], 'State', readonly=True) + ], 'State', readonly=True) communication_state = fields.Selection(AEAT_COMMUNICATION_STATE, 'Communication State', readonly=True) csv = fields.Char('CSV', readonly=True) version = fields.Selection([ ('0.7', '0.7'), - ('1.0', '1.0'), - ], 'Version', required=True, + ('1.0', '1.0'), + ], 'Version', required=True, states={ 'readonly': Eval('state') != 'draft', - }, + }, depends=['state']) lines = fields.One2Many('aeat.sii.report.lines', 'report', 'Lines', states={ - 'readonly': Eval('state') != 'draft', + 'readonly': Eval('state') != 'draft', }, depends=['state']) # TODO crash GTK client 4.x with widget date in XML view and attribute # readonly = True. At the moment, use PYSON to readonly field in XML views. @@ -567,8 +570,10 @@ class SIIReport(Workflow, ModelSQL, ModelView): base=_decimal(detail.BaseImponible), rate=_decimal(detail.TipoImpositivo), amount=_decimal(detail.CuotaRepercutida), - surcharge_rate=_decimal(detail.TipoRecargoEquivalencia), - surcharge_amount=_decimal(detail.CuotaRecargoEquivalencia), + surcharge_rate=_decimal( + detail.TipoRecargoEquivalencia), + surcharge_amount=_decimal( + detail.CuotaRecargoEquivalencia), ) for detail in reg.DatosFacturaEmitida.TipoDesglose. DesgloseFactura.Sujeta.NoExenta.DesgloseIVA.DetalleIVA @@ -706,10 +711,13 @@ class SIIReport(Workflow, ModelSQL, ModelView): base=_decimal(detail.BaseImponible), rate=_decimal(detail.TipoImpositivo), amount=_decimal(detail.CuotaSoportada), - surcharge_rate=_decimal(detail.TipoRecargoEquivalencia), - surcharge_amount=_decimal(detail.CuotaRecargoEquivalencia), + surcharge_rate=_decimal( + detail.TipoRecargoEquivalencia), + surcharge_amount=_decimal( + detail.CuotaRecargoEquivalencia), reagyp_rate=_decimal(detail.PorcentCompensacionREAGYP), - reagyp_amount=_decimal(detail.ImporteCompensacionREAGYP), + reagyp_amount=_decimal( + detail.ImporteCompensacionREAGYP), ) for detail in reg.DatosFacturaRecibida. DesgloseFactura.DesgloseIVA.DetalleIVA diff --git a/invoice.py b/invoice.py index 3ea36c3..14d84ee 100644 --- a/invoice.py +++ b/invoice.py @@ -3,9 +3,10 @@ from decimal import Decimal from trytond.model import ModelView, fields from trytond.pool import Pool, PoolMeta -from trytond.pyson import Eval +from trytond.pyson import Eval, Bool from trytond.transaction import Transaction +from sql import Null from sql.aggregate import Max from .aeat import ( @@ -35,7 +36,7 @@ class Invoice: sii_received_key = fields.Selection(RECEIVE_SPECIAL_REGIME_KEY, 'SII Recived Key', states={ - 'invisible': ~Eval('sii_book_key').in_(['R']), + 'invisible': ~Eval('sii_book_key').in_(['R']), }, depends=['sii_book_key']) sii_subjected_key = fields.Selection(IVA_SUBJECTED, 'Subjected') sii_excemption_key = fields.Selection(EXCEMPTION_CAUSE, @@ -62,7 +63,7 @@ class Invoice: cls._check_modify_exclude += sii_fields cls._buttons.update({ 'reset_sii_keys': { - 'invisible': Eval('sii_state', None) != None, + 'invisible': Bool(Eval('sii_state', None)), 'icon': 'tryton-executable'} }) if hasattr(cls, '_intercompany_excluded_fields'): @@ -101,16 +102,17 @@ class Invoice: c.remove(None) c0 = [] - if clause[-1] == None or is_none: + if clause[-1] is None or is_none: c0 = [('id', 'not in', invoices)] - clause2 = [tuple(('state',)) + tuple(clause[1:])] + \ - [('id', 'in', lines)] + clause2 = [tuple(('state',)) + tuple(clause[1:]), + ('id', 'in', lines)] res_lines = SIILines.search(clause2) if is_none: - return ['OR', c0, [('id', 'in', [x.invoice.id for x in res_lines])]] + return ['OR', c0, [ + ('id', 'in', [x.invoice.id for x in res_lines])]] else: return [('id', 'in', [x.invoice.id for x in res_lines])] @@ -132,7 +134,7 @@ class Invoice: cursor.execute(*table.select(Max(table.id), table.invoice, where=(table.invoice.in_([x.id for x in invoices]) & - (table.state != None)), + (table.state != Null)), group_by=table.invoice)) lines = [a[0] for a in cursor.fetchall()] @@ -140,7 +142,7 @@ class Invoice: if lines: cursor.execute(*join.select(table.state, report.operation_type, table.invoice, - where=((table.id.in_(lines)) & (table.state != None) & + where=((table.id.in_(lines)) & (table.state != Null) & (table.company == report.company)))) for state, op, inv in cursor.fetchall(): diff --git a/setup.py b/setup.py index ce95e87..351aee3 100644 --- a/setup.py +++ b/setup.py @@ -4,17 +4,21 @@ from setuptools import setup import re import os -import ConfigParser +import io +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser MODULE = 'aeat_sii' PREFIX = 'trytonspain' -MODULE2PREFIX = { - 'account_es': 'trytonspain' - } +MODULE2PREFIX = {} def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() + return io.open( + os.path.join(os.path.dirname(__file__), fname), + 'r', encoding='utf-8').read() def get_require_version(name): @@ -26,7 +30,7 @@ def get_require_version(name): major_version, minor_version + 1) return require -config = ConfigParser.ConfigParser() +config = ConfigParser() config.readfp(open('tryton.cfg')) info = dict(config.items('tryton')) for key in ('depends', 'extras_depend', 'xml'): @@ -41,17 +45,20 @@ minor_version = int(minor_version) requires = [ 'cryptography', 'pyOpenSSL', - 'pyAEATsii>=0.2.4' -] + 'pyAEATsii>=0.2.4', + 'python-sql', + ] for dep in info.get('depends', []): - if not re.match(r'(ir|res|webdav)(\W|$)', dep): + if not re.match(r'(ir|res)(\W|$)', dep): prefix = MODULE2PREFIX.get(dep, 'trytond') - requires.append('%s_%s >= %s.%s, < %s.%s' % - (prefix, dep, major_version, minor_version, - major_version, minor_version + 1)) + requires.append(get_require_version('%s_%s' % (prefix, dep))) requires.append(get_require_version('trytond')) tests_require = [get_require_version('proteus')] +dependency_links = [] +if minor_version % 2: + # Add development index for testing with proteus + dependency_links.append('https://trydevpi.tryton.org/') setup(name='%s_%s' % (PREFIX, MODULE), version=version, @@ -68,7 +75,7 @@ setup(name='%s_%s' % (PREFIX, MODULE), ], package_data={ 'trytond.modules.%s' % MODULE: (info.get('xml', []) - + ['tryton.cfg', 'locale/*.po', 'tests/*.rst', 'view/*.xml']), + + ['tryton.cfg', 'view/*.xml', 'locale/*.po', 'tests/*.rst']), }, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -88,12 +95,17 @@ setup(name='%s_%s' % (PREFIX, MODULE), 'Natural Language :: Russian', 'Natural Language :: Spanish', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Office/Business', ], license='GPL-3', install_requires=requires, + dependency_links=dependency_links, zip_safe=False, entry_points=""" [trytond.modules] @@ -102,4 +114,8 @@ setup(name='%s_%s' % (PREFIX, MODULE), test_suite='tests', test_loader='trytond.test_loader:Loader', tests_require=tests_require, + use_2to3=True, + convert_2to3_doctests=[ + 'tests/scenario_aeat_sii.rst', + ], ) diff --git a/tests/__init__.py b/tests/__init__.py index dd9cab0..ebe6dd3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,8 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. -from .test_aeat_sii import suite +try: + from trytond.modules.aeat_sii.tests.test_aeat_sii import suite +except ImportError: + from .test_aeat_sii import suite + +__all__ = ['suite'] diff --git a/tests/scenario_aeat_sii.rst b/tests/scenario_aeat_sii.rst index 8180b8a..f175b7c 100644 --- a/tests/scenario_aeat_sii.rst +++ b/tests/scenario_aeat_sii.rst @@ -13,15 +13,10 @@ Imports:: ... get_company >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ ... create_chart, get_accounts, create_tax, set_tax_code - >>> from.trytond.modules.account_invoice.tests.tools import \ + >>> from trytond.modules.account_invoice.tests.tools import \ ... set_fiscalyear_invoice_sequences >>> today = datetime.date.today() -Create database:: - - >>> config = config.set_trytond() - >>> config.pool.test = True - Install account_sii:: >>> config = activate_modules('aeat_sii') diff --git a/tools.py b/tools.py index 87f7f9a..69a3c85 100644 --- a/tools.py +++ b/tools.py @@ -3,18 +3,18 @@ # copyright notices and license terms. import unicodedata -src_chars = """"/*+?¿!$[]{}@#`^:;<>=~%\\""" -src_chars = unicode(src_chars, 'iso-8859-1') -dst_chars = """________________________""" -dst_chars = unicode(dst_chars, 'iso-8859-1') +src_chars = u"/*+?¿!$[]{}@#`^:;<>=~%\\" +dst_chars = u"________________________" + def normalize(text): if isinstance(text, unicode): text = text.encode('utf-8') return text + def unaccent(text): - if isinstance(text, str): + if isinstance(text, bytes): text = unicode(text, 'utf-8') output = text for c in xrange(len(src_chars)): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3d844ea --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = {py27,py34,py35,py36}-{sqlite,postgresql,mysql},pypy-{sqlite,postgresql} + +[testenv] +commands = {envpython} setup.py test +deps = + {py27,py34,py35,py36}-postgresql: psycopg2 >= 2.5 + pypy-postgresql: psycopg2cffi >= 2.5 + mysql: MySQL-python + sqlite: sqlitebck +setenv = + sqlite: TRYTOND_DATABASE_URI={env:SQLITE_URI:sqlite://} + postgresql: TRYTOND_DATABASE_URI={env:POSTGRESQL_URI:postgresql://} + mysql: TRYTOND_DATABASE_URI={env:MYSQL_URI:mysql://} + sqlite: DB_NAME={env:SQLITE_NAME::memory:} + postgresql: DB_NAME={env:POSTGRESQL_NAME:test} + mysql: DB_NAME={env:MYSQL_NAME:test} +install_command = pip install --pre --process-dependency-links {opts} {packages} diff --git a/tryton.cfg b/tryton.cfg index a63bf63..28f767c 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=4.3.0 +version=4.7.0 depends: account_invoice extras_depend: