forked from Mapan/odoo17e
402 lines
22 KiB
Python
402 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from odoo import models, fields, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.release import version
|
|
|
|
try:
|
|
from stdnum.au.abn import is_valid_abn
|
|
except ImportError:
|
|
is_valid_abn = None
|
|
|
|
RUN_TYPE = 'P' # T for test or P for production
|
|
|
|
VALID_STATES = {'ACT', 'NSW', 'NT', 'QLD', 'SA', 'TAS', 'VIC', 'WA', 'OTH'}
|
|
|
|
|
|
class AustralianReportCustomHandler(models.AbstractModel):
|
|
"""Generate the TPAR for Australia.
|
|
|
|
This file was generated using https://softwaredevelopers.ato.gov.au/TPARspecification
|
|
as a reference.
|
|
"""
|
|
_name = 'l10n_au.report.handler'
|
|
_inherit = 'account.report.custom.handler'
|
|
_description = 'Australian Report Custom Handler'
|
|
|
|
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
|
# dict of the form {partner_id: {column_group_key: {expression_label: value}}}
|
|
partner_info_dict = {}
|
|
|
|
# dict of the form {column_group_key: total_value}
|
|
total_values_dict = {}
|
|
|
|
# Build and execute query
|
|
results = self._execute_query(options)
|
|
|
|
# Fill dictionaries
|
|
for result in results:
|
|
partner_id = result['id']
|
|
column_group_key = result['column_group_key']
|
|
current_partner_info = partner_info_dict.setdefault(partner_id, {})
|
|
|
|
current_partner_info[column_group_key] = result
|
|
current_partner_info['name'] = result['name']
|
|
|
|
column_group_total = total_values_dict.setdefault(
|
|
column_group_key, {'total_gst': 0, 'gross_paid': 0, 'tax_withheld': 0}
|
|
)
|
|
column_group_total['total_gst'] += result['total_gst']
|
|
column_group_total['gross_paid'] += result['gross_paid']
|
|
column_group_total['tax_withheld'] += result['tax_withheld']
|
|
|
|
# Create lines
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
lines = []
|
|
company_currency = self.env.company.currency_id
|
|
for partner_id, partner_info in partner_info_dict.items():
|
|
columns = []
|
|
for column in options['columns']:
|
|
expression_label = column['expression_label']
|
|
value = partner_info.get(column['column_group_key'], {}).get(expression_label, False)
|
|
columns.append(report._build_column_dict(
|
|
value,
|
|
column,
|
|
options=options,
|
|
currency=company_currency,
|
|
))
|
|
line = {
|
|
'id': report._get_generic_line_id('res.partner', partner_id),
|
|
'caret_options': 'res.partner',
|
|
'model': 'res.partner',
|
|
'name': partner_info['name'],
|
|
'columns': columns,
|
|
}
|
|
lines.append((0, line))
|
|
|
|
# Add total line
|
|
if lines:
|
|
total_columns = []
|
|
for column in options['columns']:
|
|
expression_label = column['expression_label']
|
|
value = total_values_dict.get(column['column_group_key'], {}).get(expression_label, False)
|
|
total_columns.append(report._build_column_dict(
|
|
value if value else None,
|
|
column,
|
|
options=options,
|
|
))
|
|
total_line = {
|
|
'id': report._get_generic_line_id(None, None, markup='total'),
|
|
'name': _('Total'),
|
|
'class': 'total',
|
|
'level': 1,
|
|
'columns': total_columns,
|
|
}
|
|
lines.append((0, total_line))
|
|
|
|
return lines
|
|
|
|
def _caret_options_initializer(self):
|
|
return {
|
|
'res.partner': [
|
|
{'name': _("Open Invoices"), 'action': 'caret_option_open_invoices'},
|
|
{'name': _("View Partner"), 'action': 'caret_option_open_record_form'},
|
|
]
|
|
}
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options=None):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
options['buttons'] += [{
|
|
'name': _('TPAR'), 'sequence': 30, 'action': 'export_file', 'action_param': 'get_txt', 'file_export_type': _('TPAR')
|
|
}]
|
|
|
|
def _build_query(self, options, column_group_key=None):
|
|
tables, where_clause, where_params = self.env.ref('l10n_au_reports.tpar_report')._query_get(options, 'strict_range')
|
|
|
|
self.env['account.move'].flush_model()
|
|
self.env['account.move.line'].flush_model()
|
|
self.env['res.partner'].flush_model()
|
|
|
|
query = f"""
|
|
SELECT
|
|
%s AS column_group_key,
|
|
payee.id as id,
|
|
payee.vat as abn,
|
|
payee.name as name,
|
|
payee.name as commercial_partner_name,
|
|
payee.street as street,
|
|
payee.street2 as street2,
|
|
payee.city as city,
|
|
payee_state.name as state_name,
|
|
payee_state.code as state_code,
|
|
payee.zip as zip,
|
|
payee_country.name as country_name,
|
|
payee.phone as phone,
|
|
payee_bank.sanitized_acc_number as account_number,
|
|
payee.email as email,
|
|
-- # 6.61
|
|
-- the total of all payments made to the payee for the financial year (which may be different to invoiced amounts). It includes:
|
|
-- • any GST in the payments
|
|
-- • any tax withheld where an ABN was not quoted, and
|
|
-- • the market value of any non-cash benefits
|
|
COALESCE(SUM(
|
|
CASE WHEN journal.type IN ('bank', 'cash') THEN account_move_line.credit ELSE 0 END
|
|
), 0) AS gross_paid,
|
|
-- # 6.62
|
|
-- if tax is withheld from payments where an ABN was not quoted, this can be reported
|
|
-- in either a Taxable payments annual report or a PAYG withholding where ABN not
|
|
-- quoted - annual report. If those amounts are included in the Taxable payments annual
|
|
-- report, this field must contain the amount of tax withheld from all relevant payments
|
|
-- for the financial year. This amount includes any amounts withheld on the market value
|
|
-- of non-cash benefits. The amount must be reported in whole dollars.
|
|
COALESCE(-SUM(
|
|
CASE WHEN aml_tag.account_account_tag_id = %s THEN account_move_line.balance ELSE 0 END
|
|
), 0) AS tax_withheld,
|
|
-- # 6.63
|
|
-- the total of any GST included in the amounts paid
|
|
COALESCE(SUM(
|
|
CASE WHEN aml_tag.account_account_tag_id = %s THEN account_move_line.balance ELSE 0 END
|
|
), 0) AS total_gst
|
|
FROM {tables}
|
|
RIGHT JOIN res_partner payee ON payee.id = account_move_line.partner_id
|
|
LEFT JOIN res_country_state payee_state ON payee_state.id = payee.state_id
|
|
LEFT JOIN res_country payee_country ON payee_country.id = payee.country_id
|
|
LEFT JOIN res_partner_bank payee_bank ON payee_bank.partner_id = payee.id
|
|
LEFT JOIN account_journal journal ON account_move_line.journal_id = journal.id
|
|
LEFT JOIN account_account_tag_account_move_line_rel aml_tag ON account_move_line.id = aml_tag.account_move_line_id
|
|
WHERE {where_clause}
|
|
GROUP BY payee.id, payee.vat, payee.name, payee.name, payee.street, payee.street2, payee.city, payee_state.name, payee_state.code, payee.zip, payee_country.name, payee.phone, payee_bank.sanitized_acc_number, payee.email
|
|
HAVING BOOL_OR(aml_tag.account_account_tag_id IN (%s, %s))
|
|
ORDER BY payee.name
|
|
"""
|
|
tag_tpar_id = self.env.ref('l10n_au.service_tag').id
|
|
tag_withheld_id = self.env.ref('l10n_au.tax_withheld_tag').id
|
|
query_params = [column_group_key, tag_withheld_id, tag_tpar_id, *where_params, tag_withheld_id, tag_tpar_id]
|
|
|
|
return query, query_params
|
|
|
|
def _execute_query(self, options, raise_warning=False):
|
|
query_list = []
|
|
full_query_params = []
|
|
|
|
for column_group_key, column_group_options in self.env['account.report']._split_options_per_column_group(options).items():
|
|
query, params = self._build_query(column_group_options, column_group_key)
|
|
query_list.append(f"({query})")
|
|
full_query_params += params
|
|
|
|
full_query = " UNION ALL ".join(query_list)
|
|
self._cr.execute(full_query, full_query_params)
|
|
results = self._cr.dictfetchall()
|
|
|
|
# small optional sanity check
|
|
if raise_warning:
|
|
for partner in results:
|
|
if partner.get('total_gst', 0) > partner.get('gross_paid', 0) and raise_warning:
|
|
raise UserError(_('The total GST is higher than the Gross Paid for %s.', partner['name']))
|
|
|
|
return results
|
|
|
|
def get_txt(self, options):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
sender_data = {
|
|
'abn': report.get_vat_for_export(options),
|
|
'name': self.env.company.name,
|
|
'commercial_partner_name': self.env.company.name,
|
|
'street': self.env.company.street,
|
|
'street2': self.env.company.street2,
|
|
'city': self.env.company.city,
|
|
'state_name': self.env.company.state_id.name,
|
|
'state_code': self.env.company.state_id.code,
|
|
'zip': self.env.company.zip,
|
|
'country_name': self.env.company.country_id.name,
|
|
'phone': self.env.company.phone,
|
|
'email': self.env.company.email,
|
|
}
|
|
self._validate_partner(sender_data)
|
|
data = self._execute_query(options, raise_warning=True)
|
|
lines = [self._sender_data_record_1(options, sender_data), self._sender_data_record_2(sender_data), self._sender_data_record_3(sender_data)]
|
|
lines += [self._payer_identity_data_record(options, sender_data), self._software_data_record()]
|
|
lines += [self._payee_data_record(d) for d in data]
|
|
lines += [self._file_total_data_record(len(lines) + 1)]
|
|
for line in lines:
|
|
if len(line) != 996:
|
|
raise UserError(_('There was an error while writing the file (line length not 996).'
|
|
'\nPlease contact the support.\n\n%s', line))
|
|
file_content = ''.join(lines)
|
|
|
|
return {
|
|
'file_name': report.get_default_report_filename(options, 'txt'),
|
|
'file_content': file_content,
|
|
'file_type': 'txt',
|
|
}
|
|
|
|
def _sender_data_record_1(self, options, data):
|
|
return "%03d%-14s%-11s%-1s%-8s%-1s%-1s%-1s%-10s%-946s" % (
|
|
996, # 6.1 M
|
|
'IDENTREGISTER1', # 6.2 M
|
|
int(data['abn']), # 6.3 M
|
|
RUN_TYPE, # 6.4 M
|
|
fields.Date.to_date(options['date']['date_to']).strftime('%d%m%Y'), # 6.5 M
|
|
'P', # 6.6 M
|
|
'C', # 6.7 M
|
|
'M', # 6.8 M
|
|
'FPAIVV02.0', # 6.9 M
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _sender_data_record_2(self, data):
|
|
return "%03d%-14s%-200s%-38s%-15s%-15s%-16s%-695s" % (
|
|
996, # 6.1 M
|
|
'IDENTREGISTER2', # 6.11 M
|
|
data['name'], # 6.12 M
|
|
data['commercial_partner_name'], # 6.13 M
|
|
data['phone'] or '', # 6.14 M
|
|
'', # 6.15 O
|
|
'', # 6.16 O
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _sender_data_record_3(self, data):
|
|
return "%03d%-14s%-38s%-38s%-27s%-3s%-4s%-20s%-38s%-38s%-27s%-3s%-4s%-20s%-76s%-643s" % (
|
|
996, # 6.1 M
|
|
'IDENTREGISTER3', # 6.17 M
|
|
data['street'], # 6.18 M
|
|
data['street2'] or '', # 6.18 O
|
|
data['city'], # 6.19 M
|
|
data['state_code'], # 6.20 M
|
|
data['zip'], # 6.21 M
|
|
data['country_name'], # 6.22 O
|
|
'', # 6.23 O
|
|
'', # 6.23 O
|
|
'', # 6.24 O
|
|
'', # 6.25 O
|
|
'0000', # 6.26 O
|
|
'', # 6.27 O
|
|
data['email'], # 6.28 O
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _payer_identity_data_record(self, options, data):
|
|
return "%03d%-8s%011d%03d%-4s%-200s%-200s%-38s%-38s%-27s%-3s%-4s%-20s%-38s%-15s%-15s%-76s%-293s" % (
|
|
996, # 6.1 M
|
|
'IDENTITY', # 6.29 M
|
|
int(data['abn']), # 6.30 M
|
|
0, # 6.31 C
|
|
fields.Date.to_date(options['date']['date_to']).strftime('%Y'), # 6.32 M
|
|
data['name'], # 6.33 M
|
|
data['commercial_partner_name'], # 6.34 O
|
|
data['street'], # 6.35 M
|
|
data['street2'] or '', # 6.35 O
|
|
data['city'], # 6.36 M
|
|
data['state_code'], # 6.37 M
|
|
data['zip'], # 6.38 M
|
|
data['country_name'], # 6.39 O
|
|
data['name'], # 6.40 O
|
|
data['phone'] or '', # 6.41 O
|
|
'', # 6.42 O
|
|
data['email'], # 6.43 O
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _software_data_record(self):
|
|
return "%03d%-8s%-80s%-905s" % (
|
|
996, # 6.1 M
|
|
'SOFTWARE', # 6.44 M
|
|
'COMMERCIAL Andre William, Odoo %s' % version, # 6.45 M
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _payee_data_record(self, data):
|
|
self._validate_partner(data, without_abn=bool(data['tax_withheld']))
|
|
return "%03d%-6s%011d%-30s%-15s%-15s%-200s%-200s%-38s%-38s%-27s%-3s%-4s%-20s%-15s%-6s%-9s%011d%011d%011d%-1s%08d%-200s%-76s%-1s%-1s%-36s" % (
|
|
996, # 6.1 M
|
|
'DPAIVS', # 6.46 M
|
|
int(data['abn']), # 6.47 M
|
|
'', # 6.48 C
|
|
'', # 6.49 C
|
|
'', # 6.50 O
|
|
data['name'], # 6.51 M
|
|
'', # 6.52 O
|
|
data['street'], # 6.53 M
|
|
data['street2'] or '', # 6.53 O
|
|
data['city'], # 6.54 M
|
|
data['state_code'], # 6.55 M
|
|
data['zip'], # 6.56 M
|
|
data['country_name'], # 6.57 O
|
|
data['phone'] or '', # 6.58 O
|
|
''.zfill(6), # 6.59 O
|
|
''.zfill(9), # 6.60 O
|
|
data['gross_paid'], # 6.61 M
|
|
data['tax_withheld'], # 6.62 M
|
|
data['total_gst'], # 6.63 M
|
|
'P', # 6.64 M # TODO G for Grant, P for payment
|
|
0, # 6.65 C
|
|
'', # 6.66 C
|
|
data['email'], # 6.67 O
|
|
'Y', # 6.68 M # TODO Y or N depending if "a Statement by a supplier has been provided"
|
|
'O', # 6.69 M
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _file_total_data_record(self, n):
|
|
return "%03d%-10s%08d%-975s" % (
|
|
996, # 6.1 M
|
|
'FILE-TOTAL', # 6.44 M
|
|
n, # 6.45 M
|
|
'', # 6.10 S
|
|
)
|
|
|
|
def _validate_partner(self, data, without_abn=False):
|
|
errors = []
|
|
if not data['street']:
|
|
errors += [_('The street is not set')]
|
|
if len(data['street'] or '') > 38 or len(data['street2'] or '') > 38:
|
|
errors += [_('Maxmimum street length is 38')]
|
|
if not data['city']:
|
|
errors += [_('The city is not set')]
|
|
if not data['zip']:
|
|
errors += [_('The postcode is not set')]
|
|
if data['zip'] and (not (0 <= int(data['zip']) <= 9999) or (len(data['zip']) != 4)):
|
|
errors += [_('The postcode is not valid')]
|
|
if not data['state_name']:
|
|
errors += [_('The state is not set')]
|
|
if data['state_code'] not in VALID_STATES:
|
|
errors += [_('The state is not valid')]
|
|
if data['state_code'] == 'OTH' and data['zip'] != '9999':
|
|
errors += [_('The postalcode must be 9999 because it is in overseas addresses')]
|
|
if len(data.get('phone') or '') > 15:
|
|
errors += [_('The phone number is not valid (max 15 char)')]
|
|
|
|
data['abn'] = (data['abn'] or '0').replace(' ', '')
|
|
if not without_abn:
|
|
errors += self._validate_abn(data['abn'])
|
|
|
|
if errors:
|
|
raise UserError('\n'.join(errors + ['', _('While processing %s', data['name'])]))
|
|
|
|
data['email'] = data['email'] or ''
|
|
|
|
def _validate_abn(self, abn):
|
|
if not abn:
|
|
return [_('The Australian Business Number is not set')]
|
|
try:
|
|
int(abn.replace(' ', '')) # quick check independant from stdnum
|
|
if is_valid_abn and not is_valid_abn(abn):
|
|
raise ValueError()
|
|
except ValueError:
|
|
return [_('The Australian Business Number is not valid')]
|
|
return []
|
|
|
|
def caret_option_open_invoices(self, options, params=None):
|
|
dummy, record_id = self.env['account.report']._get_model_info_from_id(params['line_id'])
|
|
partner = self.env['res.partner'].browse(record_id)
|
|
tags = self.env.ref('l10n_au.service_tag') + self.env.ref('l10n_au.tax_withheld_tag')
|
|
return {
|
|
'name': _('TPAR invoices of %s', partner.display_name),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'account.move',
|
|
'view_mode': 'tree,form',
|
|
'views': [(False, 'tree'), (False, 'form')],
|
|
'domain': [('commercial_partner_id', '=', partner.id), ('line_ids.tax_tag_ids', 'in', tags.ids)],
|
|
}
|