1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_au_reports/models/account_report.py
2024-12-10 09:04:09 +07:00

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)],
}