mirror of
https://github.com/bringout/oca-financial.git
synced 2026-04-26 22:22:07 +02:00
532 lines
21 KiB
Python
532 lines
21 KiB
Python
import logging
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_compare
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
"""Inherit to implement the tax calculation using avatax API"""
|
|
|
|
_inherit = "account.move"
|
|
|
|
@api.depends("partner_shipping_id", "partner_id", "company_id")
|
|
def _compute_onchange_exemption(self):
|
|
"""
|
|
Set the exemption to use for the Invoice.
|
|
An exemption can be applied if
|
|
there an exemption for the delivery address Country + State
|
|
- Get the delivery address Country & State
|
|
- Search the invoiced commercial partner addresses
|
|
for an exemption in this Country & State
|
|
- In case there is a "country wide" exemption, use it.
|
|
|
|
Example:
|
|
- ACME company partner, USA CA, has exemption status
|
|
- ACME Invoicing address, USA CA, no exemption status
|
|
- ACME Delivery address, USA CA, no exemption status
|
|
|
|
Invoice to ACME Invoicing, Shipped to ACME Delivery with be exempt.
|
|
|
|
For this to work properly, the "exemption_lock" is no longer supported.
|
|
"""
|
|
for invoice in self.filtered(lambda x: x.state == "draft"):
|
|
invoice_partner = invoice.partner_id.commercial_partner_id
|
|
ship_to_address = (
|
|
hasattr(invoice, "partner_shipping_id")
|
|
and invoice.partner_shipping_id
|
|
or invoice_partner
|
|
)
|
|
# Find an exemption address matching the Country + State
|
|
# of the Delivery address
|
|
exemption_addresses = (
|
|
invoice_partner | invoice_partner.child_ids
|
|
).filtered("property_tax_exempt")
|
|
exemption_address_naive = exemption_addresses.filtered(
|
|
lambda a: a.country_id == ship_to_address.country_id
|
|
and (
|
|
a.state_id == ship_to_address.state_id
|
|
or invoice_partner.property_exemption_country_wide
|
|
)
|
|
)[:1]
|
|
# Force Company to get the correct values from the Property fields
|
|
exemption_address = exemption_address_naive.with_company(
|
|
invoice.company_id.id
|
|
)
|
|
invoice.exemption_code = exemption_address.property_exemption_number
|
|
invoice.exemption_code_id = exemption_address.property_exemption_code_id
|
|
|
|
@api.onchange("warehouse_id")
|
|
def onchange_warehouse_id(self):
|
|
if self.warehouse_id:
|
|
if self.warehouse_id.company_id:
|
|
self.company_id = self.warehouse_id.company_id
|
|
if self.warehouse_id.code:
|
|
self.location_code = self.warehouse_id.code
|
|
|
|
is_avatax = fields.Boolean(related="fiscal_position_id.is_avatax")
|
|
invoice_doc_no = fields.Char(
|
|
"Source/Ref Invoice No",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
help="Reference of the invoice",
|
|
)
|
|
exemption_code = fields.Char(
|
|
"Exemption Number",
|
|
compute=_compute_onchange_exemption,
|
|
readonly=False, # New computed writeable fields
|
|
store=True,
|
|
help="It show the customer exemption number",
|
|
)
|
|
exemption_code_id = fields.Many2one(
|
|
"exemption.code",
|
|
"Exemption Code",
|
|
compute=_compute_onchange_exemption,
|
|
readonly=False, # New computed writeable fields
|
|
store=True,
|
|
help="It show the customer exemption code",
|
|
)
|
|
exemption_locked = fields.Boolean(
|
|
help="Exemption code won't be automatically changed, "
|
|
"for instance, when changing the Customer."
|
|
)
|
|
tax_on_shipping_address = fields.Boolean(
|
|
"Tax based on shipping address", default=True
|
|
)
|
|
tax_address_id = fields.Many2one(
|
|
"res.partner", "Tax Shipping Address", compute="_compute_tax_address_id"
|
|
)
|
|
location_code = fields.Char(readonly=True, states={"draft": [("readonly", False)]})
|
|
warehouse_id = fields.Many2one("stock.warehouse", "Warehouse")
|
|
avatax_amount = fields.Float(string="AvaTax", copy=False)
|
|
calculate_tax_on_save = fields.Boolean()
|
|
so_partner_id = fields.Many2one(comodel_name="res.partner", string="SO Partner")
|
|
avatax_request_log = fields.Text(
|
|
"Avatax API Request Log", readonly=True, copy=False
|
|
)
|
|
avatax_response_log = fields.Text(
|
|
"Avatax API Response Log", readonly=True, copy=False
|
|
)
|
|
|
|
@api.model
|
|
@api.depends("company_id")
|
|
def _compute_hide_exemption(self):
|
|
avatax_config = self.env.company.get_avatax_config_company()
|
|
for inv in self:
|
|
inv.hide_exemption = avatax_config.hide_exemption
|
|
|
|
hide_exemption = fields.Boolean(
|
|
"Hide Exemption & Tax Based on shipping address",
|
|
compute=_compute_hide_exemption, # For past transactions visibility
|
|
default=lambda self: self.env.company.get_avatax_config_company,
|
|
help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
|
|
"Also, it will show Tax based on shipping address button",
|
|
)
|
|
|
|
@api.depends("tax_on_shipping_address", "partner_id", "partner_shipping_id")
|
|
def _compute_tax_address_id(self):
|
|
for invoice in self:
|
|
invoice.tax_address_id = (
|
|
invoice.partner_shipping_id
|
|
if invoice.tax_on_shipping_address
|
|
else invoice.partner_id
|
|
)
|
|
|
|
@api.onchange("tax_address_id", "fiscal_position_id")
|
|
def onchange_reset_avatax_amount(self):
|
|
"""
|
|
When changing quantities or prices, reset the Avatax computed amount.
|
|
The Odoo computed tax amount will then be shown, as a reference.
|
|
The Avatax amount will be recomputed upon document validation.
|
|
"""
|
|
for inv in self:
|
|
inv.avatax_amount = 0
|
|
for line in inv.invoice_line_ids:
|
|
line.avatax_amt_line = 0
|
|
|
|
# Same as v12
|
|
def get_origin_tax_date(self):
|
|
if self.invoice_doc_no:
|
|
orig_invoice = self.search(
|
|
[
|
|
("name", "=", self.invoice_doc_no),
|
|
("partner_id", "=", self.partner_id.id),
|
|
]
|
|
)
|
|
return orig_invoice.invoice_date
|
|
return False
|
|
|
|
# Same as v12
|
|
def _get_avatax_doc_type(self, commit=True):
|
|
self.ensure_one()
|
|
avatax_config = self.company_id.get_avatax_config_company()
|
|
if avatax_config.disable_tax_reporting:
|
|
commit = False
|
|
if "refund" in self.move_type:
|
|
doc_type = "ReturnInvoice" if commit else "ReturnOrder"
|
|
else:
|
|
doc_type = "SalesInvoice" if commit else "SalesOrder"
|
|
return doc_type
|
|
|
|
# Same as v12
|
|
def _avatax_prepare_lines(self, doc_type=None):
|
|
"""
|
|
Prepare the lines to use for Avatax computation.
|
|
Returns a list of dicts
|
|
"""
|
|
sign = 1 if self.move_type.startswith("out") else -1
|
|
lines = [
|
|
line._avatax_prepare_line(sign, doc_type)
|
|
for line in self.invoice_line_ids
|
|
if line.price_subtotal or line.quantity
|
|
]
|
|
return [x for x in lines if x]
|
|
|
|
def update_tax_details(self, tax, line, tax_result_line):
|
|
"""Method to update details in tax"""
|
|
return tax, line
|
|
|
|
# Same as v12
|
|
def _avatax_compute_tax(self, commit=False):
|
|
"""Contact REST API and recompute taxes for a Sale Order"""
|
|
# Override to handle lines with split taxes (e.g. TN)
|
|
self and self.ensure_one()
|
|
avatax_config = self.company_id.get_avatax_config_company()
|
|
if not avatax_config:
|
|
# Skip Avatax computation if no configuration is found
|
|
return
|
|
doc_type = self._get_avatax_doc_type(commit=commit)
|
|
tax_date = self.get_origin_tax_date() or self.invoice_date
|
|
taxable_lines = self._avatax_prepare_lines(doc_type)
|
|
tax_result = avatax_config.create_transaction(
|
|
self.invoice_date or fields.Date.today(),
|
|
self.name,
|
|
doc_type,
|
|
(
|
|
self.so_partner_id
|
|
if self.so_partner_id and avatax_config.use_so_partner_id
|
|
else self.partner_id
|
|
),
|
|
self.warehouse_id.partner_id or self.company_id.partner_id,
|
|
self.tax_address_id or self.partner_id,
|
|
taxable_lines,
|
|
self.user_id,
|
|
self.exemption_code or None,
|
|
self.exemption_code_id.code or None,
|
|
commit,
|
|
tax_date,
|
|
# TODO: can we report self.invoice_doc_no?
|
|
self.name if self.move_type == "out_refund" else "",
|
|
self.location_code or "",
|
|
is_override=self.move_type == "out_refund",
|
|
currency_id=self.currency_id,
|
|
ignore_error=300 if commit else None,
|
|
log_to_record=self,
|
|
)
|
|
# If commiting, and document exists, try unvoiding it
|
|
# Error number 300 = GetTaxError, Expected Saved|Posted
|
|
if commit and tax_result.get("number") == 300:
|
|
_logger.info(
|
|
"Document %s (%s) already exists in Avatax. "
|
|
"Should be a voided transaction. "
|
|
"Unvoiding and re-commiting.",
|
|
self.name,
|
|
doc_type,
|
|
)
|
|
avatax_config.unvoid_transaction(self.name, doc_type)
|
|
avatax_config.commit_transaction(self.name, doc_type)
|
|
return tax_result
|
|
|
|
if self.state == "draft":
|
|
Tax = self.env["account.tax"]
|
|
tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
|
|
taxes_to_set = {}
|
|
for line in self.invoice_line_ids:
|
|
tax_result_line = tax_result_lines.get(line.id)
|
|
if tax_result_line:
|
|
# rate = tax_result_line.get("rate", 0.0)
|
|
tax_calculation = 0.0
|
|
if tax_result_line["taxableAmount"]:
|
|
tax_calculation = (
|
|
tax_result_line["taxCalculated"]
|
|
/ tax_result_line["taxableAmount"]
|
|
)
|
|
rate = round(tax_calculation * 100, 4)
|
|
tax = Tax.get_avalara_tax(rate, doc_type)
|
|
tax, line = self.update_tax_details(tax, line, tax_result_line)
|
|
if tax and tax not in line.tax_ids:
|
|
line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
|
|
taxes_to_set[line.id] = line_taxes | tax
|
|
line.avatax_amt_line = tax_result_line["tax"]
|
|
self.with_context(check_move_validity=False).avatax_amount = tax_result[
|
|
"totalTax"
|
|
]
|
|
container = {"records": self}
|
|
|
|
# Set Taxes on lines in a way that properly triggers onchanges
|
|
# This same approach is also used by the official account_taxcloud connector
|
|
with self.with_context(
|
|
avatax_invoice=self, check_move_validity=False
|
|
)._sync_dynamic_lines(container), self.line_ids.mapped(
|
|
"move_id"
|
|
)._check_balanced(
|
|
container
|
|
):
|
|
for line_id in taxes_to_set.keys():
|
|
line = self.invoice_line_ids.filtered(lambda x: x.id == line_id)
|
|
line.write({"tax_ids": [(6, 0, [])]})
|
|
line.with_context(
|
|
avatax_invoice=self, check_move_validity=False
|
|
).write({"tax_ids": taxes_to_set.get(line_id).ids})
|
|
# After taxes are changed is needed to force compute taxes again, in 16 version
|
|
# change of tax doesn't trigger compute of taxes on header for unknown reason
|
|
self._compute_amount()
|
|
if float_compare(
|
|
self.amount_untaxed + max(self.amount_tax, abs(self.avatax_amount)),
|
|
self.amount_residual,
|
|
precision_rounding=self.currency_id.rounding or 0.001,
|
|
):
|
|
taxes_data = {
|
|
iline.id: iline.tax_ids for iline in self.invoice_line_ids
|
|
}
|
|
self.invoice_line_ids.write({"tax_ids": [(6, 0, [])]})
|
|
for line in self.invoice_line_ids:
|
|
line.write({"tax_ids": taxes_data[line.id].ids})
|
|
return tax_result
|
|
|
|
# Same as v13
|
|
def avatax_compute_taxes(self, commit=False):
|
|
"""
|
|
Called from Invoice's Action menu.
|
|
Forces computation of the Invoice taxes
|
|
"""
|
|
for invoice in self:
|
|
if (
|
|
invoice.move_type in ["out_invoice", "out_refund"]
|
|
and invoice.fiscal_position_id.is_avatax
|
|
and (invoice.state == "draft" or commit)
|
|
):
|
|
invoice._avatax_compute_tax(commit=commit)
|
|
return True
|
|
|
|
def avatax_commit_taxes(self):
|
|
for invoice in self:
|
|
avatax_config = invoice.company_id.get_avatax_config_company()
|
|
if not avatax_config.disable_tax_reporting:
|
|
doc_type = invoice._get_avatax_doc_type()
|
|
avatax_config.commit_transaction(invoice.name, doc_type)
|
|
return True
|
|
|
|
def is_avatax_calculated(self):
|
|
"""
|
|
Only apply Avatax for these types of documents.
|
|
Can be extended to support other types.
|
|
"""
|
|
return self.is_sale_document()
|
|
|
|
def _post(self, soft=True):
|
|
for invoice in self:
|
|
if invoice.is_avatax_calculated():
|
|
avatax_config = self.company_id.get_avatax_config_company()
|
|
if avatax_config and avatax_config.force_address_validation:
|
|
for addr in [self.partner_id, self.partner_shipping_id]:
|
|
if not addr.date_validation:
|
|
# The Validate action will be interrupted
|
|
# if the address is not validated
|
|
raise UserError(_("Avatax address is not validated!"))
|
|
# We should compute taxes before validating the invoice
|
|
# to ensure correct account moves
|
|
# However, we can't save the invoice because it wasn't assigned a
|
|
# number yet
|
|
invoice.avatax_compute_taxes(commit=False)
|
|
res = super()._post(soft=soft)
|
|
for invoice in res:
|
|
if invoice.is_avatax_calculated():
|
|
# We can only commit to Avatax after validating the invoice
|
|
# because we need the generated Invoice number
|
|
invoice.avatax_compute_taxes(commit=True)
|
|
return res
|
|
|
|
# prepare_return in v12
|
|
def _reverse_move_vals(self, default_values, cancel=True):
|
|
# OVERRIDE
|
|
# Don't keep anglo-saxon lines if not cancelling an existing invoice.
|
|
move_vals = super(AccountMove, self)._reverse_move_vals(
|
|
default_values, cancel=cancel
|
|
)
|
|
move_vals.update(
|
|
{
|
|
"invoice_doc_no": self.name,
|
|
"invoice_date": default_values
|
|
and default_values.get("invoice_date")
|
|
or self.invoice_date,
|
|
"tax_on_shipping_address": self.tax_on_shipping_address,
|
|
"warehouse_id": self.warehouse_id.id,
|
|
"location_code": self.location_code,
|
|
"exemption_code": self.exemption_code or "",
|
|
"exemption_code_id": self.exemption_code_id.id or None,
|
|
"tax_address_id": self.tax_address_id.id,
|
|
}
|
|
)
|
|
return move_vals
|
|
|
|
# action_cancel in v12
|
|
def button_draft(self):
|
|
"""
|
|
Sets invoice to Draft, either from the Posted or Cancelled states
|
|
"""
|
|
posted_invoices = self.filtered(
|
|
lambda invoice: invoice.move_type in ["out_invoice", "out_refund"]
|
|
and invoice.fiscal_position_id.is_avatax
|
|
and invoice.state == "posted"
|
|
)
|
|
res = super(AccountMove, self).button_draft()
|
|
for invoice in posted_invoices:
|
|
avatax_config = invoice.company_id.get_avatax_config_company()
|
|
if avatax_config:
|
|
doc_type = invoice._get_avatax_doc_type()
|
|
avatax_config.void_transaction(invoice.name, doc_type)
|
|
return res
|
|
|
|
@api.onchange(
|
|
"invoice_line_ids",
|
|
"warehouse_id",
|
|
"tax_address_id",
|
|
"tax_on_shipping_address",
|
|
"partner_id",
|
|
)
|
|
def onchange_avatax_calculation(self):
|
|
avatax_config = self.env.company.get_avatax_config_company()
|
|
self.calculate_tax_on_save = False
|
|
if avatax_config.invoice_calculate_tax:
|
|
if (
|
|
self._origin.warehouse_id != self.warehouse_id
|
|
or self._origin.tax_address_id.street != self.tax_address_id.street
|
|
or self._origin.partner_id != self.partner_id
|
|
or self._origin.tax_on_shipping_address != self.tax_on_shipping_address
|
|
):
|
|
self.calculate_tax_on_save = True
|
|
return
|
|
for line in self.invoice_line_ids:
|
|
if (
|
|
line._origin.price_unit != line.price_unit
|
|
or line._origin.discount != line.discount
|
|
or line._origin.quantity != line.quantity
|
|
) and line.display_type == "product":
|
|
self.calculate_tax_on_save = True
|
|
break
|
|
|
|
def write(self, vals):
|
|
result = super(AccountMove, self).write(vals)
|
|
avatax_config = self.env.company.get_avatax_config_company()
|
|
for record in self:
|
|
if (
|
|
avatax_config.invoice_calculate_tax
|
|
and record.calculate_tax_on_save
|
|
and record.state == "draft"
|
|
and not self._context.get("skip_second_write", False)
|
|
):
|
|
record.with_context(skip_second_write=True).write(
|
|
{"calculate_tax_on_save": False}
|
|
)
|
|
record.avatax_compute_taxes()
|
|
return result
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
moves = super().create(vals_list)
|
|
avatax_config = self.env.company.get_avatax_config_company()
|
|
for move in moves:
|
|
if (
|
|
avatax_config.invoice_calculate_tax
|
|
and move.calculate_tax_on_save
|
|
and not self._context.get("skip_second_write", False)
|
|
):
|
|
move.with_context(skip_second_write=True).write(
|
|
{"calculate_tax_on_save": False}
|
|
)
|
|
move.avatax_compute_taxes()
|
|
return moves
|
|
|
|
|
|
class AccountMoveLine(models.Model):
|
|
_inherit = "account.move.line"
|
|
|
|
avatax_amt_line = fields.Float(string="AvaTax Line", copy=False)
|
|
|
|
def _get_avatax_amount(self, qty=None):
|
|
"""
|
|
Return the company currency line amount, after discounts,
|
|
to use for Tax calculation.
|
|
|
|
Can be used to compute unit price only, using qty=1.
|
|
|
|
Code extracted from account/models/account_move.py,
|
|
from the compute_base_line_taxes() nested function,
|
|
adjusted to compute line amount instead of unit price.
|
|
"""
|
|
self.ensure_one()
|
|
base_line = self
|
|
move = base_line.move_id
|
|
sign = -1 if move.is_inbound() else 1
|
|
quantity = qty or base_line.quantity
|
|
base_amount = base_line.price_unit * quantity
|
|
if base_line.currency_id:
|
|
price_unit_foreign_curr = (
|
|
sign * base_amount * (1 - (base_line.discount / 100.0))
|
|
)
|
|
price_unit_comp_curr = base_line.currency_id._convert(
|
|
price_unit_foreign_curr,
|
|
move.company_id.currency_id,
|
|
move.company_id,
|
|
move.date,
|
|
)
|
|
else:
|
|
price_unit_comp_curr = (
|
|
sign * base_amount * (1 - (base_line.discount / 100.0))
|
|
)
|
|
return -price_unit_comp_curr
|
|
|
|
# Same in v12
|
|
def _avatax_prepare_line(self, sign=1, doc_type=None):
|
|
"""
|
|
Prepare a line to use for Avatax computation.
|
|
Returns a dict
|
|
"""
|
|
line = self
|
|
res = {}
|
|
# Add UPC to product item code
|
|
avatax_config = line.company_id.get_avatax_config_company()
|
|
product = line.product_id
|
|
if product.barcode and avatax_config.upc_enable:
|
|
item_code = "UPC:%d" % product.barcode
|
|
else:
|
|
item_code = product.default_code or ("ID:%d" % product.id)
|
|
tax_code = line.product_id.applicable_tax_code_id.name
|
|
amount = sign * line._get_avatax_amount()
|
|
if line.quantity < 0:
|
|
amount = -amount
|
|
res = {
|
|
"qty": line.quantity,
|
|
"itemcode": item_code,
|
|
"description": line.name,
|
|
"amount": amount,
|
|
"tax_code": tax_code,
|
|
"id": line,
|
|
"account_id": line.account_id.id,
|
|
"tax_id": line.tax_ids,
|
|
}
|
|
return res
|
|
|
|
@api.onchange("price_unit", "discount", "quantity")
|
|
def onchange_reset_tax_amt(self):
|
|
"""
|
|
When changing quantities or prices, reset the Avatax computed amount.
|
|
The Odoo computed tax amount will then be shown, as a reference.
|
|
The Avatax amount will be recomputed upon document validation.
|
|
"""
|
|
for line in self:
|
|
line.avatax_amt_line = 0.0
|
|
line.move_id.avatax_amount = 0.0
|