333 lines
11 KiB
Python
333 lines
11 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
|
|
import csv
|
|
import io
|
|
import re
|
|
import zipfile
|
|
from collections import defaultdict
|
|
from decimal import Decimal
|
|
from enum import IntFlag
|
|
|
|
from sql.operators import Equal
|
|
|
|
from trytond.model import Exclude, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval
|
|
|
|
_ACT_FIELDNAMES = [
|
|
'DOCTYPE', 'DBKCODE', 'DBKTYPE', 'DOCNUMBER', 'DOCORDER', 'OPCODE',
|
|
'ACCOUNTGL', 'ACCOUNTRP', 'BOOKYEAR', 'PERIOD', 'DATE', 'DATEDOC',
|
|
'DUEDATE', 'COMMENT', 'COMMENTEXT', 'AMOUNT', 'AMOUNTEUR', 'VATBASE',
|
|
'VATCODE', 'CURRAMOUNT', 'CURRCODE', 'CUREURBASE', 'VATTAX', 'VATIMPUT',
|
|
'CURRATE', 'REMINDLEV', 'MATCHNO', 'OLDDATE', 'ISMATCHED', 'ISLOCKED',
|
|
'ISIMPORTED', 'ISIMPORTED', 'ISTEMP', 'MEMOTYPE', 'ISDOC', 'DOCSTATUS']
|
|
|
|
|
|
def _format_date(date):
|
|
if date:
|
|
return date.strftime('%Y%m%d')
|
|
else:
|
|
return ''
|
|
|
|
|
|
class DOCTYPE(IntFlag):
|
|
CUSTOMER = 1
|
|
SUPPLIER = 2
|
|
GENERAL = 3
|
|
VAT_0 = 4
|
|
|
|
|
|
class DBKTYPE(IntFlag):
|
|
IN_INVOICE = 0
|
|
IN_CREDIT_NOTE = 1
|
|
OUT_INVOICE = 2
|
|
OUT_CREDIT_NOTE = 3
|
|
STATEMENT = 4
|
|
MISC = 5
|
|
|
|
|
|
class Account(metaclass=PoolMeta):
|
|
__name__ = 'account.account'
|
|
|
|
winbooks_code = fields.Char("WinBooks Code", size=8)
|
|
|
|
|
|
class FiscalYear(metaclass=PoolMeta):
|
|
__name__ = 'account.fiscalyear'
|
|
|
|
winbooks_code = fields.Char("WinBooks Code", size=1)
|
|
|
|
@classmethod
|
|
def copy(cls, fiscalyears, default=None):
|
|
default = default.copy() if default is not None else {}
|
|
default.setdefault('winbooks_code')
|
|
return super().copy(fiscalyears, default=default)
|
|
|
|
|
|
class RenewFiscalYear(metaclass=PoolMeta):
|
|
__name__ = 'account.fiscalyear.renew'
|
|
|
|
def fiscalyear_defaults(self):
|
|
defaults = super().fiscalyear_defaults()
|
|
if self.start.previous_fiscalyear.winbooks_code:
|
|
if self.start.previous_fiscalyear.winbooks_code == '9':
|
|
code = 'A'
|
|
elif self.start.previous_fiscalyear.winbooks_code == 'Z':
|
|
code = None
|
|
else:
|
|
i = ord(self.start.previous_fiscalyear.winbooks_code)
|
|
code = chr(i + 1)
|
|
defaults['winbooks_code'] = code
|
|
return defaults
|
|
|
|
|
|
class Period(metaclass=PoolMeta):
|
|
__name__ = 'account.period'
|
|
|
|
@property
|
|
def winbooks_code(self):
|
|
if self.type == 'standard':
|
|
periods = [
|
|
p for p in self.fiscalyear.periods if p.type == self.type]
|
|
i = periods.index(self) + 1
|
|
else:
|
|
middle_year = (
|
|
self.fiscalyear.start_date
|
|
+ (self.fiscalyear.end_date
|
|
- self.fiscalyear.start_date) / 2)
|
|
middle_period = (
|
|
self.start_date + (self.end_date - self.start_date) / 2)
|
|
i = 0 if middle_period < middle_year else 99
|
|
return f'{i:02}'
|
|
|
|
|
|
class Journal(metaclass=PoolMeta):
|
|
__name__ = 'account.journal'
|
|
|
|
winbooks_code = fields.Char("WinBooks Code", size=6)
|
|
winbooks_code_credit_note = fields.Char(
|
|
"WinBooks Code Credit Note",
|
|
states={
|
|
'invisible': ~Eval('type').in_(['revenue', 'expense']),
|
|
},
|
|
help="The code to use for credit note.\n"
|
|
"Leave empty to use the default code.")
|
|
|
|
def get_winbooks_code(self, dbktype):
|
|
code = self.winbooks_code
|
|
if (dbktype in {DBKTYPE.IN_CREDIT_NOTE, DBKTYPE.OUT_CREDIT_NOTE}
|
|
and self.winbooks_code_credit_note):
|
|
code = self.winbooks_code_credit_note
|
|
return code
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'account.move'
|
|
|
|
@property
|
|
def winbooks_number(self):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
if isinstance(self.origin, Invoice):
|
|
number = self.origin.number
|
|
else:
|
|
number = self.number
|
|
return re.sub(r'[^0-9]', '', number)
|
|
|
|
|
|
class MoveLine(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line'
|
|
|
|
@property
|
|
def winbooks_comment(self):
|
|
pool = Pool()
|
|
InvoiceLine = pool.get('account.invoice.line')
|
|
if isinstance(self.origin, InvoiceLine):
|
|
comment = self.origin.rec_name
|
|
else:
|
|
comment = self.description_used or self.move_description_used or ''
|
|
return comment
|
|
|
|
|
|
class TaxTemplate(metaclass=PoolMeta):
|
|
__name__ = 'account.tax.template'
|
|
|
|
winbooks_code = fields.Char("WinBooks Code", size=8)
|
|
|
|
def _get_tax_value(self, tax=None):
|
|
value = super()._get_tax_value(tax=tax)
|
|
if not tax or tax.winbooks_code != self.winbooks_code:
|
|
value['winbooks_code'] = self.winbooks_code
|
|
return value
|
|
|
|
|
|
class Tax(metaclass=PoolMeta):
|
|
__name__ = 'account.tax'
|
|
|
|
winbooks_code = fields.Char(
|
|
"WinBooks Code", size=10,
|
|
states={
|
|
'readonly': (
|
|
Bool(Eval('template', -1)
|
|
& ~Eval('template_override', False))),
|
|
})
|
|
|
|
|
|
class TaxLine(metaclass=PoolMeta):
|
|
__name__ = 'account.tax.line'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('winbooks_tax_move_line_exclude', Exclude(
|
|
t, (t.move_line, Equal),
|
|
where=(t.type == 'tax')),
|
|
'account_export_winbooks.'
|
|
'msg_account_tax_line_tax_move_line_unique'),
|
|
]
|
|
|
|
|
|
class MoveExport(metaclass=PoolMeta):
|
|
__name__ = 'account.move.export'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.type.selection.append(('winbooks', "WinBooks"))
|
|
|
|
def get_filename(self, name):
|
|
name = super().get_filename(name)
|
|
if self.type == 'winbooks':
|
|
name = f'{self.rec_name}-winbooks.zip'
|
|
return name
|
|
|
|
def _process_winbooks(self):
|
|
data = io.BytesIO()
|
|
with zipfile.ZipFile(data, 'w') as file:
|
|
file.writestr(
|
|
'ACT.txt', self._process_winbooks_act(_ACT_FIELDNAMES))
|
|
self.file = data.getvalue()
|
|
|
|
def _process_winbooks_act(
|
|
self, fieldnames, dialect='excel', delimiter=',', **fmtparams):
|
|
data = io.BytesIO()
|
|
writer = csv.DictWriter(
|
|
io.TextIOWrapper(data, encoding='utf-8', write_through=True),
|
|
fieldnames, extrasaction='ignore',
|
|
dialect=dialect, delimiter=delimiter, quoting=csv.QUOTE_ALL,
|
|
**fmtparams)
|
|
writer.writerows(self._process_winbooks_act_rows())
|
|
return data.getvalue()
|
|
|
|
def _process_winbooks_act_rows(self):
|
|
for move in self.moves:
|
|
yield from self._process_winbooks_act_move(move)
|
|
|
|
def _process_winbooks_act_move(self, move):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
try:
|
|
Statement = pool.get('account.statement')
|
|
except KeyError:
|
|
Statement = None
|
|
|
|
bases = defaultdict(Decimal)
|
|
taxes = defaultdict(Decimal)
|
|
for line in move.lines:
|
|
for tax_line in line.tax_lines:
|
|
if tax_line.type == 'base':
|
|
bases[tax_line.tax] += tax_line.amount
|
|
else:
|
|
taxes[tax_line.tax] += tax_line.amount
|
|
for line in move.lines:
|
|
dbktype = ''
|
|
if isinstance(move.origin, Invoice):
|
|
invoice = move.origin
|
|
sequence_field = f'{invoice.type}_{invoice.sequence_type}'
|
|
dbktype = getattr(DBKTYPE, sequence_field.upper())
|
|
elif isinstance(move.origin, Statement):
|
|
dbktype = DBKTYPE.STATEMENT
|
|
row = self._process_winbooks_act_line(line)
|
|
move_row = {
|
|
'DBKCODE': move.journal.get_winbooks_code(dbktype),
|
|
'DBKTYPE': int(dbktype),
|
|
'DOCNUMBER': move.winbooks_number,
|
|
'BOOKYEAR': move.period.fiscalyear.winbooks_code,
|
|
'PERIOD': move.period.winbooks_code,
|
|
'DATEDOC': _format_date(move.date),
|
|
}
|
|
row.update(move_row)
|
|
is_credit_note = (
|
|
dbktype in {DBKTYPE.IN_CREDIT_NOTE, DBKTYPE.OUT_CREDIT_NOTE})
|
|
if row['DOCTYPE'] == DOCTYPE.GENERAL:
|
|
vatcode = ''
|
|
vatbase = 0
|
|
tax = None
|
|
for tax_line in line.tax_lines:
|
|
if (tax_line.type == 'tax'
|
|
and tax_line.tax.winbooks_code):
|
|
tax = tax_line.tax
|
|
vatbase += bases[tax_line.tax]
|
|
break
|
|
else:
|
|
tax_line = None
|
|
if tax_line:
|
|
if taxes[tax] != tax_line.amount:
|
|
vatbase *= round(tax_line.amount / taxes[tax], 3)
|
|
vatcode = tax.winbooks_code
|
|
if is_credit_note:
|
|
vatbase *= -1
|
|
row.setdefault('VATBASE', vatbase)
|
|
row.setdefault('VATCODE', vatcode)
|
|
|
|
if row['DOCTYPE'] == DOCTYPE.GENERAL:
|
|
for tax_line in line.tax_lines:
|
|
tax = tax_line.tax
|
|
if (tax_line.type == 'base'
|
|
and tax.type == 'percentage' and not tax.rate):
|
|
vatcode = tax.winbooks_code
|
|
vatbase = tax_line.amount
|
|
if is_credit_note:
|
|
vatbase *= -1
|
|
vat_0_row = {
|
|
'DOCTYPE': int(DOCTYPE.VAT_0),
|
|
'COMMENT': tax.description,
|
|
'AMOUNT': 0,
|
|
'AMOUNTEUR': 0,
|
|
'VATBASE': vatbase,
|
|
'VATCODE': vatcode,
|
|
}
|
|
vat_0_row.update(move_row)
|
|
yield vat_0_row
|
|
yield row
|
|
|
|
def _process_winbooks_act_line(self, line):
|
|
accountrp = ''
|
|
if line.account.type.receivable and line.party:
|
|
doctype = DOCTYPE.CUSTOMER
|
|
if identifier := line.party.winbooks_customer_identifier:
|
|
accountrp = identifier.code
|
|
elif line.account.type.payable and line.party:
|
|
doctype = DOCTYPE.SUPPLIER
|
|
if identifier := line.party.winbooks_supplier_identifier:
|
|
accountrp = identifier.code
|
|
else:
|
|
doctype = DOCTYPE.GENERAL
|
|
return {
|
|
'DOCTYPE': int(doctype),
|
|
'ACCOUNTGL': (
|
|
line.account.winbooks_code or line.account.code),
|
|
'ACCOUNTRP': accountrp,
|
|
'DUEDATE': _format_date(line.maturity_date),
|
|
'COMMENT': line.winbooks_comment[:40],
|
|
'AMOUNT': 0,
|
|
'AMOUNTEUR': line.debit - line.credit,
|
|
'CURRAMOUNT': line.amount_second_currency,
|
|
'CURRCODE': (
|
|
line.second_currency.code if line.second_currency
|
|
else ''),
|
|
}
|