trytond-aeat_sii/invoice.py

548 lines
19 KiB
Python
Raw Normal View History

2017-06-16 09:32:47 +02:00
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
import hashlib
from ast import literal_eval
from decimal import Decimal
2017-07-04 11:44:44 +02:00
from trytond.model import ModelView, fields
2017-06-16 09:32:47 +02:00
from trytond.pool import Pool, PoolMeta
2018-01-18 17:08:54 +01:00
from trytond.pyson import Eval, Bool
2017-06-16 09:32:47 +02:00
from trytond.transaction import Transaction
2019-03-26 12:23:35 +01:00
from trytond.i18n import gettext
2019-05-10 15:01:23 +02:00
from trytond.exceptions import UserError, UserWarning
from trytond.wizard import Wizard, StateView, StateTransition, Button
2018-01-18 17:08:54 +01:00
from sql import Null
2017-06-16 09:32:47 +02:00
from sql.aggregate import Max
2019-04-17 13:18:48 +02:00
from trytond.tools import grouped_slice
2017-06-16 09:32:47 +02:00
from .aeat import (
OPERATION_KEY, BOOK_KEY, SEND_SPECIAL_REGIME_KEY,
RECEIVE_SPECIAL_REGIME_KEY, AEAT_INVOICE_STATE, IVA_SUBJECTED,
EXCEMPTION_CAUSE, INTRACOMUNITARY_TYPE, COMMUNICATION_TYPE)
__all__ = ['Invoice', 'ResetSIIKeysStart', 'ResetSIIKeys', 'ResetSIIKeysEnd']
2017-06-16 09:32:47 +02:00
_SII_INVOICE_KEYS = ['sii_book_key', 'sii_operation_key', 'sii_issued_key',
'sii_received_key', 'sii_subjected_key', 'sii_excemption_key',
2017-06-22 23:36:46 +02:00
'sii_intracomunity_key']
2019-04-17 13:18:48 +02:00
MAX_SII_LINES = 300
2017-06-16 09:32:47 +02:00
2018-10-11 15:32:09 +02:00
class Invoice(metaclass=PoolMeta):
2017-06-16 09:32:47 +02:00
__name__ = 'account.invoice'
sii_book_key = fields.Selection(BOOK_KEY, 'SII Book Key')
sii_operation_key = fields.Selection(OPERATION_KEY, 'SII Operation Key')
sii_issued_key = fields.Selection(SEND_SPECIAL_REGIME_KEY,
'SII Issued Key',
states={
'invisible': ~Eval('sii_book_key').in_(['E']),
}, depends=['sii_book_key'])
2017-06-16 09:32:47 +02:00
sii_received_key = fields.Selection(RECEIVE_SPECIAL_REGIME_KEY,
'SII Recived Key',
states={
2018-01-18 17:08:54 +01:00
'invisible': ~Eval('sii_book_key').in_(['R']),
}, depends=['sii_book_key'])
2017-06-16 09:32:47 +02:00
sii_subjected_key = fields.Selection(IVA_SUBJECTED, 'Subjected')
sii_excemption_key = fields.Selection(EXCEMPTION_CAUSE,
'Excemption Cause')
sii_intracomunity_key = fields.Selection(INTRACOMUNITARY_TYPE,
'SII Intracommunity Key',
states={
'invisible': ~Eval('sii_book_key').in_(['U']),
}, depends=['sii_book_key'])
2017-06-16 09:32:47 +02:00
sii_records = fields.One2Many('aeat.sii.report.lines', 'invoice',
"SII Report Lines")
sii_state = fields.Selection(AEAT_INVOICE_STATE,
'SII State', readonly=True)
sii_communication_type = fields.Selection(
COMMUNICATION_TYPE, 'SII Communication Type', readonly=True)
sii_pending_sending = fields.Boolean('SII Pending Sending Pending',
readonly=True)
sii_header = fields.Text('Header')
2017-06-16 09:32:47 +02:00
@classmethod
def __setup__(cls):
super(Invoice, cls).__setup__()
sii_fields = ['sii_book_key', 'sii_operation_key',
2017-06-16 09:32:47 +02:00
'sii_received_key', 'sii_issued_key', 'sii_subjected_key',
'sii_excemption_key', 'sii_intracomunity_key','sii_pending_sending',
'sii_communication_type', 'sii_state', 'sii_header']
cls._check_modify_exclude += sii_fields
if hasattr(cls, '_intercompany_excluded_fields'):
cls._intercompany_excluded_fields += sii_fields
cls._intercompany_excluded_fields += ['sii_records']
2017-06-16 09:32:47 +02:00
@staticmethod
def default_sii_pending_sending():
return False
@classmethod
def get_issued_sii_reports(cls):
pool = Pool()
Invoice = pool.get('account.invoice')
2019-11-12 22:27:09 +01:00
SIIReportLine = pool.get('aeat.sii.report.lines')
issued_invoices = {
'A0': {}, # 'A0', 'Registration of invoices/records'
'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)'
'D0': {}, # 'D0', 'Delete Invoices'
}
2019-04-17 13:18:48 +02:00
issued_invs = Invoice.search([
('sii_pending_sending', '=', True),
('sii_state', '=', 'Correcto'),
2019-04-02 10:56:04 +02:00
('sii_header', '!=', None),
2019-06-20 10:29:37 +02:00
('type', 'in', ['out']),
])
2019-04-17 13:18:48 +02:00
# search issued invoices [delete]
delete_issued_invoices = []
# search issued invoices [modify]
modify_issued_invoices = []
for issued_inv in issued_invs:
if not issued_inv.sii_records:
continue
sii_record_id = max([s.id for s in issued_inv.sii_records])
sii_record = SIIReportLine(sii_record_id)
if issued_inv.sii_header:
if (literal_eval(issued_inv.sii_header) ==
literal_eval(sii_record.sii_header)):
2019-04-17 13:18:48 +02:00
modify_issued_invoices.append(issued_inv)
else:
delete_issued_invoices.append(issued_inv)
2019-04-17 13:18:48 +02:00
2019-06-20 10:29:37 +02:00
periods = {}
for invoice in delete_issued_invoices:
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods:
periods[period].append(invoice,)
2019-04-17 13:18:48 +02:00
else:
2019-06-20 10:29:37 +02:00
periods[period] = [invoice]
issued_invoices['D0'] = periods
2019-04-17 13:18:48 +02:00
2019-06-20 10:29:37 +02:00
periods2 = {}
2019-04-17 13:18:48 +02:00
for invoice in modify_issued_invoices:
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods2:
periods2[period].append(invoice,)
else:
2019-06-20 10:29:37 +02:00
periods2[period] = [invoice]
issued_invoices['A1'] = periods2
# search issued invoices [new]
new_issued_invoices = Invoice.search([
('sii_state', 'in', (None, 'Incorrecto')),
('sii_pending_sending', '=', True),
('type', '=', 'out'),
])
2019-04-17 13:18:48 +02:00
# search possible deleted invoices in SII and not uploaded again
new_issued_invoices += Invoice.search([
('sii_state', '=', 'Anulada'),
('sii_pending_sending', '=', True),
('type', '=', 'out'),
2019-04-17 13:18:48 +02:00
('state', 'in', ['paid', 'posted']),
])
2019-04-17 13:18:48 +02:00
new_issued_invoices += delete_issued_invoices
2019-06-20 10:29:37 +02:00
periods1 = {}
2019-04-17 13:18:48 +02:00
for invoice in new_issued_invoices:
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods1:
periods1[period].append(invoice,)
else:
2019-06-20 10:29:37 +02:00
periods1[period] = [invoice]
issued_invoices['A0'] = periods1
2019-04-17 13:18:48 +02:00
book_type = 'E' # Issued
return cls.create_sii_book(issued_invoices, book_type)
@classmethod
def get_received_sii_reports(cls):
pool = Pool()
Invoice = pool.get('account.invoice')
2019-04-17 13:18:48 +02:00
SIIReportLine = pool.get('aeat.sii.report.lines')
received_invoices = {
'A0': {}, # 'A0', 'Registration of invoices/records'
'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)'
'D0': {}, # 'D0', 'Delete Invoices'
}
2019-04-17 13:18:48 +02:00
received_invs = Invoice.search([
('sii_pending_sending', '=', True),
('sii_state', '=', 'Correcto'),
2019-04-02 10:56:04 +02:00
('sii_header', '!=', None),
('type', '=', 'in'),
])
2019-04-17 13:18:48 +02:00
# search received invoices [delete]
delete_received_invoices = []
# search received invoices [modify]
modify_received_invoices = []
for received_inv in received_invs:
if not received_inv.sii_records:
continue
sii_record_id = max([s.id for s in received_inv.sii_records])
sii_record = SIIReportLine(sii_record_id)
if received_inv.sii_header:
if (literal_eval(received_inv.sii_header) ==
literal_eval(sii_record.sii_header)):
2019-04-17 13:18:48 +02:00
modify_received_invoices.append(received_inv)
else:
delete_received_invoices.append(received_inv)
2019-04-17 13:18:48 +02:00
2019-06-20 10:29:37 +02:00
periods2 = {}
for invoice in modify_received_invoices:
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods2:
periods2[period].append(invoice,)
2019-04-17 13:18:48 +02:00
else:
2019-06-20 10:29:37 +02:00
periods2[period] = [invoice]
received_invoices['A1'] = periods2
2019-04-17 13:18:48 +02:00
2019-06-20 10:29:37 +02:00
periods = {}
for invoice in delete_received_invoices:
2019-04-17 13:18:48 +02:00
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods:
periods[period].append(invoice,)
else:
2019-06-20 10:29:37 +02:00
periods[period] = [invoice]
received_invoices['D0'] = periods
# search received invoices [new]
new_received_invoices = Invoice.search([
('sii_state', 'in', (None, 'Incorrecto')),
('sii_pending_sending', '=', True),
('type', '=', 'in'),
])
2019-04-17 13:18:48 +02:00
# search possible deleted invoices in SII and not uploaded again
new_received_invoices += Invoice.search([
('sii_state', '=', 'Anulada'),
2019-04-02 10:56:04 +02:00
('sii_pending_sending', '=', True),
('type', '=', 'in'),
2019-04-17 13:18:48 +02:00
('state', 'in', ['paid', 'posted']),
2019-04-02 10:56:04 +02:00
])
2019-04-17 13:18:48 +02:00
new_received_invoices += delete_received_invoices
2019-06-20 10:29:37 +02:00
periods1 = {}
2019-04-17 13:18:48 +02:00
for invoice in new_received_invoices:
period = invoice.move.period
2019-06-20 10:29:37 +02:00
if period in periods1:
periods1[period].append(invoice,)
else:
2019-06-20 10:29:37 +02:00
periods1[period] = [invoice]
received_invoices['A0'] = periods1
book_type = 'R' # Received
return cls.create_sii_book(received_invoices, book_type)
@classmethod
2019-04-17 13:18:48 +02:00
def create_sii_book(cls, book_invoices, book):
pool = Pool()
SIIReport = pool.get('aeat.sii.report')
SIIReportLine = pool.get('aeat.sii.report.lines')
Company = Pool().get('company.company')
company = Transaction().context.get('company')
company = Company(company)
company_vat = company.party.sii_vat_code
2019-04-17 13:18:48 +02:00
cursor = Transaction().connection.cursor()
report_line_table = SIIReportLine.__table__()
reports = []
for operation in ['D0', 'A1', 'A0']:
values = book_invoices[operation]
delete = True if operation == 'D0' else False
2019-04-27 06:15:34 +02:00
for period, invoices in values.items():
2019-04-17 13:18:48 +02:00
for invs in grouped_slice(invoices, MAX_SII_LINES):
report = SIIReport()
report.company = company
report.company_vat = company_vat
report.fiscalyear = period.fiscalyear
report.period = period
report.operation_type = operation
report.book = book
report.save()
reports.append(report)
values = []
for inv in invs:
sii_header = str(inv.get_sii_header(inv, delete))
values.append([report.id, inv.id, sii_header, company.id])
cursor.execute(*report_line_table.insert(
columns=[report_line_table.report,
report_line_table.invoice,
report_line_table.sii_header,
report_line_table.company],
values=values
))
return reports
2017-06-16 09:32:47 +02:00
@classmethod
def search_sii_state(cls, name, clause):
pool = Pool()
SIILines = pool.get('aeat.sii.report.lines')
table = SIILines.__table__()
cursor = Transaction().connection.cursor()
cursor.execute(*table.select(Max(table.id), table.invoice,
group_by=table.invoice))
invoices = []
lines = []
for id_, invoice in cursor.fetchall():
invoices.append(invoice)
lines.append(id_)
is_none = False
c = clause[-1]
if isinstance(clause[-1], list):
if None in clause[-1]:
is_none = True
c.remove(None)
c0 = []
2019-06-20 10:29:37 +02:00
if clause[-1] == None or is_none:
c0 = [('id', 'not in', invoices)]
2017-06-16 09:32:47 +02:00
2019-06-20 10:29:37 +02:00
clause2 = [tuple(('state',)) + tuple(clause[1:])] + \
[('id', 'in', lines)]
2017-06-16 09:32:47 +02:00
res_lines = SIILines.search(clause2)
if is_none:
2019-04-17 13:18:48 +02:00
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])]
2017-06-16 09:32:47 +02:00
@classmethod
def get_sii_state(cls, invoices, names):
pool = Pool()
SIILines = pool.get('aeat.sii.report.lines')
SIIReport = pool.get('aeat.sii.report')
result = {}
2017-06-16 09:32:47 +02:00
for name in names:
result[name] = dict((i.id, None) for i in invoices)
table = SIILines.__table__()
report = SIIReport.__table__()
cursor = Transaction().connection.cursor()
join = table.join(report, condition=table.report == report.id)
cursor.execute(*table.select(Max(table.id), table.invoice,
where=(table.invoice.in_([x.id for x in invoices]) &
2018-01-18 17:08:54 +01:00
(table.state != Null)),
2017-06-16 09:32:47 +02:00
group_by=table.invoice))
lines = [a[0] for a in cursor.fetchall()]
if lines:
cursor.execute(*join.select(table.state, report.operation_type,
table.invoice,
2018-01-18 17:08:54 +01:00
where=((table.id.in_(lines)) & (table.state != Null) &
(table.company == report.company))))
2017-06-16 09:32:47 +02:00
for state, op, inv in cursor.fetchall():
if 'sii_state' in names:
result['sii_state'][inv] = state
if 'sii_communication_type' in names:
result['sii_communication_type'][inv] = op
return result
def _credit(self):
credit = super(Invoice, self)._credit()
2017-06-22 23:36:46 +02:00
for field in _SII_INVOICE_KEYS:
setattr(credit, field, getattr(self, field))
2017-06-16 09:32:47 +02:00
credit.sii_operation_key = 'R1'
return credit
2017-06-16 09:32:47 +02:00
2017-07-04 11:44:44 +02:00
def _set_sii_keys(self):
tax = None
for t in self.taxes:
if t.tax.sii_book_key:
2018-07-08 23:09:34 +02:00
tax = t.tax
break
2017-07-04 11:44:44 +02:00
if not tax:
return
for field in _SII_INVOICE_KEYS:
2018-07-08 23:09:34 +02:00
setattr(self, field, getattr(tax, field))
2017-07-04 11:44:44 +02:00
2017-06-22 23:36:46 +02:00
@fields.depends(*_SII_INVOICE_KEYS)
2017-06-16 09:32:47 +02:00
def _on_change_lines_taxes(self):
super(Invoice, self)._on_change_lines_taxes()
2017-06-22 23:36:46 +02:00
for field in _SII_INVOICE_KEYS:
2017-06-16 09:32:47 +02:00
if getattr(self, field):
return
2017-07-04 11:44:44 +02:00
self._set_sii_keys()
@classmethod
def copy(cls, records, default=None):
if default is None:
default = {}
default = default.copy()
default.setdefault('sii_records')
default.setdefault('sii_state')
default.setdefault('sii_communication_type')
default.setdefault('sii_operation_key')
default.setdefault('sii_pending_sending')
default.setdefault('sii_header')
return super(Invoice, cls).copy(records, default=default)
def _get_sii_operation_key(self):
return 'R1' if self.untaxed_amount < Decimal('0.0') else 'F1'
2017-07-04 11:44:44 +02:00
@classmethod
def reset_sii_keys(cls, invoices):
2017-07-04 11:44:44 +02:00
to_write = []
for invoice in invoices:
if invoice.state != 'draft':
continue
for field in _SII_INVOICE_KEYS:
setattr(invoice, field, None)
invoice._set_sii_keys()
if not invoice.sii_operation_key:
invoice.sii_operation_key = invoice._get_sii_operation_key()
to_write.extend(([invoice], invoice._save_values))
2017-07-04 11:44:44 +02:00
if to_write:
cls.write(*to_write)
@classmethod
def process(cls, invoices):
super(Invoice, cls).process(invoices)
invoices_sii = ''
for invoice in invoices:
if invoice.state != 'draft':
continue
if invoice.sii_state:
invoices_sii += '\n%s: %s' % (invoice.number, invoice.sii_state)
if invoices_sii:
2019-03-26 12:23:35 +01:00
raise UserError(gettext('aeat_sii.msg_invoices_sii',
invoices=invoices_sii))
@classmethod
def draft(cls, invoices):
2019-05-10 15:01:23 +02:00
pool = Pool()
2019-05-23 12:25:16 +02:00
Warning = pool.get('res.user.warning')
super(Invoice, cls).draft(invoices)
invoices_sii = []
to_write = []
for invoice in invoices:
to_write.extend(([invoice], {'sii_pending_sending': False}))
if invoice.sii_state:
invoices_sii.append('%s: %s' % (
invoice.number, invoice.sii_state))
for record in invoice.sii_records:
if record.report.state == 'draft':
2019-03-26 12:23:35 +01:00
raise UserError(gettext('aeat_sii.invoices_sii_pending'))
if invoices_sii:
warning_name = 'invoices_sii.' + hashlib.md5(
''.join(invoices_sii).encode('utf-8')).hexdigest()
2019-05-10 15:01:23 +02:00
if Warning.check(warning_name):
raise UserWarning(warning_name,
gettext('aeat_sii.msg_invoices_sii',
invoices='\n'.join(invoices_sii)))
if to_write:
cls.write(*to_write)
@classmethod
def post(cls, invoices):
to_write = []
invoices2checksii = []
for invoice in invoices:
if not invoice.move or invoice.move.state == 'draft':
invoices2checksii.append(invoice)
super(Invoice, cls).post(invoices)
for invoice in invoices2checksii:
values = {}
if invoice.sii_book_key:
if not invoice.sii_operation_key:
values['sii_operation_key'] =\
invoice._get_sii_operation_key()
values['sii_pending_sending'] = True
values['sii_header'] = str(cls.get_sii_header(invoice, False))
to_write.extend(([invoice], values))
if to_write:
cls.write(*to_write)
@classmethod
def cancel(cls, invoices):
cls.write(invoices, {'sii_pending_sending': False})
return super(Invoice, cls).cancel(invoices)
@classmethod
def get_sii_header(cls, invoice, delete):
pool = Pool()
IssuedMapper = pool.get('aeat.sii.issued.invoice.mapper')
ReceivedMapper = pool.get('aeat.sii.recieved.invoice.mapper')
if delete:
2019-06-20 10:29:37 +02:00
rline = [x for x in invoice.sii_records if x.state == 'Correcto'
and x.sii_header != None]
if rline:
return rline[0].sii_header
if invoice.type == 'out':
mapper = IssuedMapper()
header = mapper.build_delete_request(invoice)
else:
mapper = ReceivedMapper()
header = mapper.build_delete_request(invoice)
return header
class ResetSIIKeysStart(ModelView):
"""
Reset to default SII Keys Start
"""
__name__ = "aeat.sii.reset.keys.start"
class ResetSIIKeysEnd(ModelView):
"""
Reset to default SII Keys End
"""
__name__ = "aeat.sii.reset.keys.end"
class ResetSIIKeys(Wizard):
"""
Reset to default SII Keys
"""
__name__ = "aeat.sii.reset.keys"
start = StateView('aeat.sii.reset.keys.start',
'aeat_sii.aeat_sii_reset_keys_start_view', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Reset', 'reset', 'tryton-ok', default=True),
])
reset = StateTransition()
done = StateView('aeat.sii.reset.keys.end',
'aeat_sii.aeat_sii_reset_keys_end_view', [
Button('Ok', 'end', 'tryton-ok', default=True),
])
def transition_reset(self):
pool = Pool()
Invoice = pool.get('account.invoice')
invoices = Invoice.browse(Transaction().context['active_ids'])
Invoice.reset_sii_keys(invoices)
return 'done'