You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
8.5 KiB
Python
201 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import datetime
|
|
import logging
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError, UserError
|
|
from odoo.tools import float_compare, float_round
|
|
from odoo.tests.common import Form
|
|
|
|
from .taxcloud_request import TaxCloudRequest
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = 'account.move'
|
|
|
|
# Used to determine whether or not to warn the user to configure TaxCloud
|
|
is_taxcloud_configured = fields.Boolean(related='company_id.is_taxcloud_configured')
|
|
# Technical field to determine whether to hide taxes in views or not
|
|
is_taxcloud = fields.Boolean(related='fiscal_position_id.is_taxcloud')
|
|
|
|
def _post(self, soft=True):
|
|
# OVERRIDE
|
|
|
|
# Don't change anything on moves used to cancel another ones.
|
|
if self._context.get('move_reverse_cancel'):
|
|
return super()._post(soft)
|
|
|
|
invoices_to_validate = self.filtered(
|
|
lambda move: move.is_sale_document() and move.fiscal_position_id.is_taxcloud)
|
|
|
|
if invoices_to_validate:
|
|
for invoice in invoices_to_validate.with_context(taxcloud_authorize_transaction=True):
|
|
invoice.validate_taxes_on_invoice()
|
|
return super()._post(soft)
|
|
|
|
def button_draft(self):
|
|
"""At confirmation below, the AuthorizedWithCapture encodes the invoice
|
|
in TaxCloud. Returned cancels it for a refund.
|
|
See https://dev.taxcloud.com/taxcloud/guides/5%20Returned%20Orders
|
|
"""
|
|
if self.filtered(lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.fiscal_position_id.is_taxcloud):
|
|
raise UserError(_("You cannot cancel an invoice sent to TaxCloud.\n"
|
|
"You need to issue a refund (credit note) for it instead.\n"
|
|
"This way the tax entries will be cancelled in TaxCloud."))
|
|
return super(AccountMove, self).button_draft()
|
|
|
|
@api.model
|
|
def _get_TaxCloudRequest(self, api_id, api_key):
|
|
return TaxCloudRequest(api_id, api_key)
|
|
|
|
def get_taxcloud_reporting_date(self):
|
|
if self.invoice_date:
|
|
return datetime.datetime.combine(self.invoice_date, datetime.datetime.min.time())
|
|
else:
|
|
return fields.Datetime.context_timestamp(self, datetime.datetime.now())
|
|
|
|
def validate_taxes_on_invoice(self):
|
|
self.ensure_one()
|
|
company = self.company_id
|
|
shipper = company or self.env.company
|
|
api_id = shipper.taxcloud_api_id
|
|
api_key = shipper.taxcloud_api_key
|
|
request = self._get_TaxCloudRequest(api_id, api_key)
|
|
|
|
request.set_location_origin_detail(shipper)
|
|
request.set_location_destination_detail(self.partner_shipping_id)
|
|
|
|
request.set_invoice_items_detail(self)
|
|
|
|
response = request.get_all_taxes_values()
|
|
|
|
if response.get('error_message'):
|
|
raise ValidationError(
|
|
_('Unable to retrieve taxes from TaxCloud: ') + '\n' +
|
|
response['error_message']
|
|
)
|
|
|
|
tax_values = response['values']
|
|
|
|
# warning: this is tightly coupled to TaxCloudRequest's _process_lines method
|
|
# do not modify without syncing the other method
|
|
raise_warning = False
|
|
taxes_to_set = []
|
|
for index, line in enumerate(self.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_note', 'line_section'))):
|
|
if line._get_taxcloud_price() >= 0.0 and line.quantity >= 0.0:
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) * line.quantity
|
|
if not price:
|
|
tax_rate = 0.0
|
|
else:
|
|
tax_rate = tax_values[index] / price * 100
|
|
if len(line.tax_ids) != 1 or float_compare(line.tax_ids.amount, tax_rate, precision_digits=3):
|
|
raise_warning = True
|
|
tax_rate = float_round(tax_rate, precision_digits=3)
|
|
tax = self.env['account.tax'].sudo().with_context(active_test=False).search([
|
|
('amount', '=', tax_rate),
|
|
('amount_type', '=', 'percent'),
|
|
('type_tax_use', '=', 'sale'),
|
|
('company_id', '=', company.id),
|
|
], limit=1)
|
|
if tax:
|
|
# Only set if not already set, otherwise it triggers a
|
|
# needless and potentially heavy recompute for
|
|
# everything related to the tax.
|
|
if not tax.active:
|
|
tax.active = True # Needs to be active to be included in invoice total computation
|
|
else:
|
|
tax = self.env['account.tax'].sudo().with_context(default_company_id=company.id).create({
|
|
'name': 'Tax %.3f %%' % (tax_rate),
|
|
'amount': tax_rate,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'sale',
|
|
'description': 'Sales Tax',
|
|
})
|
|
taxes_to_set.append((index, tax))
|
|
|
|
with Form(self) as move_form:
|
|
for index, tax in taxes_to_set:
|
|
with move_form.invoice_line_ids.edit(index) as line_form:
|
|
line_form.tax_ids.clear()
|
|
line_form.tax_ids.add(tax)
|
|
|
|
if self.env.context.get('taxcloud_authorize_transaction'):
|
|
reporting_date = self.get_taxcloud_reporting_date()
|
|
|
|
if self.move_type == 'out_invoice':
|
|
request.client.service.AuthorizedWithCapture(
|
|
request.api_login_id,
|
|
request.api_key,
|
|
request.customer_id,
|
|
request.cart_id,
|
|
self.id,
|
|
reporting_date, # DateAuthorized
|
|
reporting_date, # DateCaptured
|
|
)
|
|
elif self.move_type == 'out_refund':
|
|
request.set_invoice_items_detail(self)
|
|
origin_invoice = self.reversed_entry_id
|
|
if origin_invoice:
|
|
request.client.service.Returned(
|
|
request.api_login_id,
|
|
request.api_key,
|
|
origin_invoice.id,
|
|
request.cart_items,
|
|
fields.Datetime.from_string(self.invoice_date)
|
|
)
|
|
else:
|
|
_logger.warning("The source document on the refund is not valid and thus the refunded cart won't be logged on your taxcloud account.")
|
|
|
|
if raise_warning:
|
|
return {'warning': _('The tax rates have been updated, you may want to check it before validation')}
|
|
else:
|
|
return True
|
|
|
|
def _invoice_paid_hook(self):
|
|
for invoice in self:
|
|
company = invoice.company_id
|
|
if invoice.fiscal_position_id.is_taxcloud:
|
|
api_id = company.taxcloud_api_id
|
|
api_key = company.taxcloud_api_key
|
|
request = TaxCloudRequest(api_id, api_key)
|
|
if invoice.move_type == 'out_invoice':
|
|
request.client.service.Captured(
|
|
request.api_login_id,
|
|
request.api_key,
|
|
invoice.id,
|
|
)
|
|
else:
|
|
request.set_invoice_items_detail(invoice)
|
|
origin_invoice = invoice.reversed_entry_id
|
|
if origin_invoice:
|
|
request.client.service.Returned(
|
|
request.api_login_id,
|
|
request.api_key,
|
|
origin_invoice.id,
|
|
request.cart_items,
|
|
fields.Datetime.from_string(invoice.invoice_date)
|
|
)
|
|
else:
|
|
_logger.warning(
|
|
"The source document on the refund %i is not valid and thus the refunded cart won't be logged on your taxcloud account",
|
|
invoice.id,
|
|
)
|
|
|
|
return super(AccountMove, self)._invoice_paid_hook()
|
|
|
|
|
|
class AccountMoveLine(models.Model):
|
|
"""Defines getters to have a common facade for order and move lines in TaxCloud."""
|
|
_inherit = 'account.move.line'
|
|
|
|
def _get_taxcloud_price(self):
|
|
self.ensure_one()
|
|
return self.price_unit
|
|
|
|
def _get_qty(self):
|
|
self.ensure_one()
|
|
return self.quantity
|