# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import json
import requests
import logging
import datetime
from re import match
from dateutil.relativedelta import relativedelta
from markupsafe import Markup
_logger = logging.getLogger(__name__)
class HmrcVatObligation(models.Model):
""" VAT obligations retrieved from HMRC """
_name = 'l10n_uk.vat.obligation'
_description = 'HMRC VAT Obligation'
_inherit = ['mail.thread', 'mail.activity.mixin']
_rec_name = 'date_due'
_order = 'date_due'
# fields retrieved from HMRC
date_start = fields.Date('Period Start', readonly=True)
date_end = fields.Date('Period End', readonly=True)
date_due = fields.Date('Period Due', readonly=True)
status = fields.Selection([('open', 'Open'), ('fulfilled', 'Fulfilled')], string='Period Status', readonly=True)
period_key = fields.Char('Period Key', readonly=True)
date_received = fields.Date('Received Submission date', readonly=True)
company_id = fields.Many2one('res.company', 'Company', required=True,
default=lambda self: self.env.company)
@api.depends('date_due', 'date_start', 'date_end')
def _compute_display_name(self):
for o in self:
o.display_name = f"{o.date_due} ({o.date_start} - {o.date_end})"
@api.model
def _get_auth_headers(self, bearer, client_data=None):
headers = {
'Accept': 'application/vnd.hmrc.1.0+json',
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % bearer,
**self.env['hmrc.service']._get_fraud_prevention_info(client_data),
}
return headers
@api.model
def retrieve_vat_obligations(self, vat, from_date, to_date, status=''):
""" Retrieve vat obligations
The User should be logged in before doing this
:param vat:
:param from_date:
:param to_date:
:param status:
:return: list of obligations of the status type for the requested period
"""
if not match(r'^[0-9]{9}$', vat or ''):
raise UserError(_("VAT numbers of UK companies should have exactly 9 figures or 11 with the GB or XI prefix. Please check the settings of the current company."))
user = self.env.user
bearer = user.l10n_uk_hmrc_vat_token
headers = self._get_auth_headers(bearer)
url = self.env['hmrc.service']._get_endpoint_url('/organisations/vat/%s/obligations' % vat)
params = {
'from': from_date,
'to': to_date,
}
if status:
status = 'O' if status == 'open' else 'F'
params.update({'status': status})
resp = requests.get(url, headers=headers, params=params)
response = json.loads(resp.content.decode())
if resp.status_code == 200:
# Create obligations
return response.get('obligations')
# Show a nice error when something goes wrong
error_code = response.get('code')
if error_code == 'VRN_INVALID':
error_message = _('Invalid Company VAT number. Please fill in the correct VAT on the company form. ')
elif error_code in ('INVALID_DATE_FROM', 'INVALID_DATE_TO', 'INVALID_DATE_RANGE'):
error_message = _('Issue with the selected dates.')
elif error_code == 'INVALID_STATUS':
error_message = _('Invalid Status.')
elif error_code == 'NOT_FOUND':
error_message = _('No open obligations were found for the moment.')
elif error_code == 'CLIENT_OR_AGENT_NOT_AUTHORISED':
# In case one user needs to submit the report for two companies, they will need to re-login.
self.env['hmrc.service'].sudo()._clean_tokens()
return []
else:
error_message = response.get('message', error_code)
raise UserError(error_message)
def _get_vat(self):
# Use company's VAT if company is British, otherwise try to look for a UK fiscal position.
foreign_vat = False
if not self.env.company.country_id.code == 'GB':
foreign_vat = self.env.company.fiscal_position_ids.filtered(lambda fp: fp.country_id.code == 'GB').foreign_vat
vat = foreign_vat or self.env.company.vat
# The VAT sent to HMRC should not include the GB or XI prefix.
if vat.startswith(('GB', 'XI')):
vat = vat[2:]
return vat
def import_vat_obligations(self):
today = datetime.date.today()
res = self.env['hmrc.service']._login()
if res: # If you can not login, return url for re-login
return res
# look for open obligations in the -6 months +6 months range
obligations = self.retrieve_vat_obligations(
self._get_vat(),
(today + relativedelta(months=-6)).strftime('%Y-%m-%d'),
(today + relativedelta(months=6,leapdays=-1)).strftime('%Y-%m-%d'))
for new_obligation in obligations:
obligation = self.env['l10n_uk.vat.obligation'].search([('period_key', '=', new_obligation.get('periodKey')),
('company_id', '=', self.env.company.id)])
status = 'open' if new_obligation['status'] == 'O' else 'fulfilled'
if not obligation:
self.sudo().create({'date_start': new_obligation['start'],
'date_end': new_obligation['end'],
'date_received': new_obligation.get('received'),
'date_due': new_obligation['due'],
'status': status,
'period_key': new_obligation['periodKey'],
'company_id': self.env.company.id,
})
elif obligation.status != status or obligation.date_received != new_obligation.get('received'):
obligation.sudo().write({'status': status,
'date_received': new_obligation.get('received')})
def _fetch_values_from_report(self, lines):
translation_table = {
'vatDueSales': 'account_tax_report_line_vat_box1',
'vatDueAcquisitions': 'account_tax_report_line_vat_box2',
'totalVatDue': 'account_tax_report_line_vat_box3',
'vatReclaimedCurrPeriod': 'account_tax_report_line_vat_box4',
'netVatDue': 'account_tax_report_line_vat_box5',
'totalValueSalesExVAT': 'account_tax_report_line_exd_vat_box6',
'totalValuePurchasesExVAT': 'account_tax_report_line_exd_vat_box7',
'totalValueGoodsSuppliedExVAT': 'account_tax_report_line_exd_vat_box8',
'totalAcquisitionsExVAT': 'account_tax_report_line_exd_vat_box9',
}
reverse_table = {}
for line_xml_id in translation_table:
uk_report_id = self.env.ref('l10n_uk.' + translation_table[line_xml_id])
reverse_table[uk_report_id.id] = line_xml_id
values = {}
for line in lines:
line_id = self.env['account.report']._parse_line_id(line['id'])[-1][-1]
if reverse_table.get(line_id):
# Do a get for the no_format as for the totals you have twice the line, without and with amount
# We cannot pass a negative netVatDue to the API and the amounts of sales/purchases/goodssupplied/ ... must be rounded
if reverse_table[line_id] == 'netVatDue':
values[reverse_table[line_id]] = abs(round(line['columns'][0].get('no_format', 0.0), 2))
elif reverse_table[line_id] in ('totalValueSalesExVAT', 'totalValuePurchasesExVAT', 'totalValueGoodsSuppliedExVAT', 'totalAcquisitionsExVAT'):
values[reverse_table[line_id]] = round(line['columns'][0].get('no_format', 0.0))
else:
values[reverse_table[line_id]] = round(line['columns'][0].get('no_format', 0.0), 2)
return values
def action_submit_vat_return(self, data=None):
self.ensure_one()
report = self.env.ref('l10n_uk.tax_report')
options = report.get_options()
options['date'].update({'date_from': fields.Date.to_string(self.date_start),
'date_to': fields.Date.to_string(self.date_end),
'filter': 'custom',
'mode': 'range'})
report_values = report._get_lines(options)
values = self._fetch_values_from_report(report_values)
vat = self._get_vat()
res = self.env['hmrc.service']._login()
if res: # If you can not login, return url for re-login
return res
headers = self._get_auth_headers(self.env.user.l10n_uk_hmrc_vat_token, data)
url = self.env['hmrc.service']._get_endpoint_url('/organisations/vat/%s/returns' % vat)
data = values.copy()
data.update({
'periodKey': self.period_key,
'finalised': True
})
# Need to check with which credentials it needs to be done
r = requests.post(url, headers=headers, data=json.dumps(data))
# Need to do something with the result?
if r.status_code == 201: #Successful post
response = json.loads(r.content.decode())
msg = _('Tax return successfully posted:') + Markup('
')
msg += Markup('%s : %s
') % (_('Date Processed'), response['processingDate'])
if response.get('paymentIndicator'):
msg += Markup('%s : %s
') % (_('Payment Indicator'), response['paymentIndicator'])
msg += Markup('%s : %s
') % (_('Form Bundle Number'), response['formBundleNumber'])
if response.get('chargeRefNumber'):
msg += Markup('%s : %s
') % (_('Charge Ref Number'), response['chargeRefNumber'])
msg += Markup('
%s
') % _('Sent Values:')
for sent_key in data:
if sent_key != 'periodKey':
msg += Markup('%s: %s') % (sent_key, data[sent_key])
self.sudo().message_post(body=msg)
self.sudo().write({'status': "fulfilled"})
# Show a confirmation popup.
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
'type': 'success',
'message': _("The VAT report has been successfully submitted to HMRC."),
})
elif r.status_code == 401: # auth issue
_logger.exception("HMRC auth issue : %s", r.content)
raise UserError(_(
"Sorry, your credentials were refused by HMRC or your permission grant has expired. You may try to authenticate again."))
else: # other issues
_logger.exception("HMRC other issue : %s", r.content)
# even 'normal' hmrc errors have a json body. Otherwise will also raise.
response = json.loads(r.content.decode())
# Recuperate error message
if response.get('errors'):
msgs = ""
for err in response['errors']:
msgs += err.get('message', '')
else:
msgs = response.get('message') or response
raise UserError(_("Sorry, something went wrong: %s", msgs))