Merge pull request #85 from expsa/new_avatax

new_avatax
This commit is contained in:
esam-sermah 2026-01-18 09:57:36 +03:00 committed by GitHub
commit cae66f6bc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
275 changed files with 42452 additions and 0 deletions

View File

@ -0,0 +1,2 @@
from . import models

View File

@ -0,0 +1,24 @@
{
'name': 'Account Batch Payment Reconciliation',
'version': '1.0',
'category': 'Accounting',
'summary': 'Allows using Reconciliation with the Batch Payment feature.',
'depends': ['odex30_account_accountant', 'odex30_account_batch_payment'],
'data': [
'security/ir.model.access.csv',
'views/bank_rec_widget_views.xml',
'views/account_batch_payment_rejection_views.xml',
],
'auto_install': True,
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'odex30_account_accountant_batch_payment/static/src/components/**/*',
],
'web.assets_tests': [
'odex30_account_accountant_batch_payment/static/tests/tours/*.js',
],
}
}

View File

@ -0,0 +1,223 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_accountant_batch_payment
#
# Translators:
# Malaz Abuidris <msea@odoo.com>, 2024
# Wil Odoo, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-27 13:54+0000\n"
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
"Last-Translator: Wil Odoo, 2025\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid ""
"<br/>\n"
" <span>Do you want to cancel payments to retry them later or keep the batch open with unprocess payments, if you expect them later.</span>"
msgstr ""
"<br/>\n"
" <span>هل ترغب في إلغاء عمليات الدفع لإعادة المحاولة لاحقاً أو ترك الدفعة مفتوحة مع عمليات دفع غير معالَجة، إذا كنت تتوقع أن تتم معالجتهم لاحقاً.</span>"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
msgid "Amount Due"
msgstr "المبلغ المستحق"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
msgid "Amount Due (in currency)"
msgstr "المبلغ المستحق (بالعملة) "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_bank_rec_widget
msgid "Bank reconciliation widget for a single statement line"
msgstr "أداة التسوية البنكية لبند كشف حساب واحد "
#. module: odex30_account_accountant_batch_payment
#. odoo-python
#: code:addons/odex30_account_accountant_batch_payment/models/account_batch_payment.py:0
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
msgid "Batch Payment"
msgstr "دفعة مجمعة "
#. module: odex30_account_accountant_batch_payment
#. odoo-javascript
#: code:addons/odex30_account_accountant_batch_payment/static/src/components/bank_reconciliation/bank_rec_form.xml:0
msgid "Batch Payments"
msgstr "الدفعات المجمعة "
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "Cancel"
msgstr "إلغاء"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "Cancel Payments"
msgstr "إلغاء المدفوعات "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__create_uid
msgid "Created by"
msgstr "أنشئ بواسطة"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__create_date
msgid "Created on"
msgstr "أنشئ في"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
msgid "Date"
msgstr "التاريخ"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__display_name
msgid "Display Name"
msgstr "اسم العرض "
#. module: odex30_account_accountant_batch_payment
#. odoo-python
#: code:addons/odex30_account_accountant_batch_payment/models/bank_rec_widget.py:0
msgid "Exchange Difference: %(batch_name)s - %(currency)s"
msgstr "فرق سعر الصرف: %(batch_name)s - %(currency)s "
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "Expect Payments Later"
msgstr "توقع المدفوعات في وقت لاحق"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__flag
msgid "Flag"
msgstr "إبلاغ"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__id
msgid "ID"
msgstr "المُعرف"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__in_reconcile_payment_ids
msgid "In Reconcile Payment"
msgstr "في تسوية الدفع "
#. module: odex30_account_accountant_batch_payment
#. odoo-python
#: code:addons/odex30_account_accountant_batch_payment/models/bank_rec_widget.py:0
msgid "Includes %(count)s payment(s)"
msgstr "يتضمن %(count)s مدفوعات "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__write_uid
msgid "Last Updated by"
msgstr "آخر تحديث بواسطة"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__write_date
msgid "Last Updated on"
msgstr "آخر تحديث في"
#. module: account_accountant_batch_payment
#: model:ir.model,name:account_accountant_batch_payment.model_bank_rec_widget_line
msgid "Line of the bank reconciliation widget"
msgstr "بند أداة التسوية البنكية "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_batch_payment_rejection
msgid "Manage the payment rejection from batch payments"
msgstr "قم بإدارة حالات رفض الدفع من المدفوعات المجمعة "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__nb_batch_payment_ids
msgid "Nb Batch Payment"
msgstr "رقم الدفعة المجمعة "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__nb_rejected_payment_ids
msgid "Nb Rejected Payment"
msgstr "ملاحظة الدفعة مرفوضة"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
msgid "Paid"
msgstr "مدفوع"
#. module: odex30_account_accountant_batch_payment
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_reconcile_model
msgid ""
"Preset to create journal entries during a invoices and payments matching"
msgstr "الإعداد المسبق لإنشاء قيود يومية خلال مطابقة الفواتير والدفعات"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
msgid "Received"
msgstr "تم الاستلام "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__rejected_payment_ids
msgid "Rejected Payment"
msgstr "عمليات الدفع المرفوضة "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget__selected_batch_payment_ids
msgid "Selected Batch Payment"
msgstr "الدفعة المجمعة المحددة "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__source_batch_payment_id
msgid "Source Batch Payment"
msgstr "الدفعة المجمعة المصدرية "
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__source_batch_payment_name
msgid "Source Batch Payment Name"
msgstr "اسم الدفعة المجمعة المصدرية "
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
msgid "Suggestions"
msgstr "الاقتراحات "
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
msgid "Unreconciled"
msgstr "غير المسواة"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
msgid "View"
msgstr "أداة العرض"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "batches have been removed."
msgstr "تمت إزالة الدفعات."
#. module: odex30_account_accountant_batch_payment
#: model:ir.model.fields.selection,name:odex30_account_accountant_batch_payment.selection__bank_rec_widget_line__flag__new_batch
msgid "new_batch"
msgstr "new_batch"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "payments from"
msgstr "المدفوعات من"
#. module: odex30_account_accountant_batch_payment
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
msgid "payments from the batch have been removed."
msgstr "تمت إزالة المدفوعات من الدفعة. "

View File

@ -0,0 +1,6 @@
from . import account_batch_payment
from . import account_reconcile_model
from . import bank_rec_widget
from . import bank_rec_widget_line
from . import account_batch_payment_rejection

View File

@ -0,0 +1,22 @@
from odoo import models, _
class AccountBatchPayment(models.Model):
_inherit = 'account.batch.payment'
def action_open_batch_payment(self):
self.ensure_one()
return {
'name': _("Batch Payment"),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'view_id': self.env.ref('account_batch_payment.view_batch_payment_form').id,
'res_model': self._name,
'res_id': self.id,
'context': {
'create': False,
'delete': False,
},
'target': 'current',
}

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, Command
class AccountBatchPaymentRejection(models.TransientModel):
_name = 'account.batch.payment.rejection'
_description = "Manage the payment rejection from batch payments"
in_reconcile_payment_ids = fields.Many2many(comodel_name='account.payment')
rejected_payment_ids = fields.Many2many(
comodel_name='account.payment',
compute='_compute_rejected_payment_ids',
)
nb_rejected_payment_ids = fields.Integer(compute='_compute_rejected_payment_ids')
nb_batch_payment_ids = fields.Integer(compute='_compute_rejected_payment_ids')
@api.model
def _fetch_rejected_payment_ids(self, in_reconcile_payments):
batch_ids = in_reconcile_payments.batch_payment_id.ids
if batch_ids:
return self.env['account.payment'].search([
('is_matched', '=', False),
('batch_payment_id', 'in', batch_ids),
('id', 'not in', in_reconcile_payments.ids),
])
else:
return self.env['account.payment']
@api.depends('in_reconcile_payment_ids')
def _compute_rejected_payment_ids(self):
for wizard in self:
rejected_payments = wizard._fetch_rejected_payment_ids(wizard.in_reconcile_payment_ids)
wizard.rejected_payment_ids = [Command.set(rejected_payments.ids)]
wizard.nb_rejected_payment_ids = len(wizard.rejected_payment_ids)
wizard.nb_batch_payment_ids = len(rejected_payments.batch_payment_id)
def button_cancel_payments(self):
self.rejected_payment_ids.batch_payment_id = False
to_unlink = self.rejected_payment_ids.move_id.filtered(lambda x: not x._get_violated_lock_dates(x.date, False))
to_reject = self.rejected_payment_ids.move_id - to_unlink
if to_unlink:
to_unlink.button_draft()
to_unlink.button_cancel()
if to_reject:
to_reject._reverse_moves(cancel=True)
return {'type': 'ir.actions.act_window_close', 'infos': 'validate'}
def button_continue(self):
return {'type': 'ir.actions.act_window_close', 'infos': 'validate'}
def button_cancel(self):
return True

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from odoo import models
from odoo.tools import SQL
class AccountReconcileModel(models.Model):
_inherit = 'account.reconcile.model'
def _get_invoice_matching_batch_payments_candidates(self, st_line, partner):
assert self.rule_type == 'invoice_matching'
self.env['account.batch.payment'].flush_model()
_numerical_tokens, exact_tokens, _text_tokens = self._get_invoice_matching_st_line_tokens(st_line)
if not exact_tokens:
return
batches = self.env['account.batch.payment'].search([('state', '!=', 'reconciled'), ('name', 'in', exact_tokens)])
if not batches:
return
aml_domain = self._get_invoice_matching_amls_domain(st_line, partner)
query = self.env['account.move.line']._where_calc(aml_domain)
candidate_ids = [r[0] for r in self.env.execute_query(SQL(
'''
SELECT DISTINCT account_move_line.id
FROM %s
JOIN account_payment pay ON pay.id = account_move_line.payment_id
JOIN account_batch_payment batch
ON batch.id = pay.batch_payment_id
AND batch.id = ANY(%s)
AND batch.state != 'reconciled'
WHERE %s
''',
query.from_clause,
[batches.ids],
query.where_clause or SQL("TRUE"),
))]
if candidate_ids:
return {
'allow_auto_reconcile': True,
'amls': self.env['account.move.line'].browse(candidate_ids),
}
def _get_invoice_matching_rules_map(self):
# EXTENDS account
res = super()._get_invoice_matching_rules_map()
res[0].append(self._get_invoice_matching_batch_payments_candidates)
return res

View File

@ -0,0 +1,392 @@
from collections import defaultdict
import json
from odoo import _, api, fields, models, Command
from odoo.tools import SQL
from odoo.addons.web.controllers.utils import clean_action
class BankRecWidget(models.Model):
_inherit = 'bank.rec.widget'
selected_batch_payment_ids = fields.Many2many(
comodel_name='account.batch.payment',
compute='_compute_selected_batch_payment_ids',
)
def _fetch_available_amls_in_batch_payments(self, batch_payments=None):
self.ensure_one()
st_line = self.st_line_id
amls_domain = st_line._get_default_amls_matching_domain()
query = self.env['account.move.line']._where_calc(amls_domain)
rows = self.env.execute_query(SQL(
'''
SELECT
pay.batch_payment_id,
ARRAY_AGG(account_move_line.id) AS aml_ids
FROM %s
JOIN account_payment pay ON pay.id = account_move_line.payment_id
JOIN account_batch_payment batch ON batch.id = pay.batch_payment_id
WHERE %s
AND %s
AND pay.batch_payment_id IS NOT NULL
AND batch.state != 'reconciled'
GROUP BY pay.batch_payment_id
''',
query.from_clause,
query.where_clause or SQL("TRUE"),
SQL("pay.batch_payment_id IN %s", tuple(batch_payments.ids)) if batch_payments else SQL("TRUE")
))
return {r[0]: r[1] for r in rows}
@api.depends('company_id', 'line_ids.source_batch_payment_id')
def _compute_selected_batch_payment_ids(self):
for wizard in self:
batch_payment_x_amls = defaultdict(set)
new_batches = wizard.line_ids.filtered(lambda x: x.flag == 'new_batch')
new_batch_payments = new_batches.source_batch_payment_id
new_amls = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml')
for new_aml in new_amls:
if new_aml.source_batch_payment_id:
batch_payment_x_amls[new_aml.source_batch_payment_id].add(new_aml.source_aml_id.id)
selected_batch_payment_ids = []
if batch_payment_x_amls:
batch_payments = wizard.line_ids.source_batch_payment_id
available_amls_in_batch_payments = wizard._fetch_available_amls_in_batch_payments(batch_payments=batch_payments)
selected_batch_payment_ids = [
x.id
for x in batch_payments
if batch_payment_x_amls[x] == set(available_amls_in_batch_payments.get(x.id, []))
]
if new_batch_payments:
selected_batch_payment_ids += new_batch_payments.ids
wizard.selected_batch_payment_ids = [Command.set(selected_batch_payment_ids)]
@api.depends('company_id', 'line_ids.source_aml_id', 'line_ids.source_batch_payment_id')
def _compute_selected_aml_ids(self):
super()._compute_selected_aml_ids()
for wizard in self:
new_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch')
for batch in new_batches.source_batch_payment_id:
wizard.selected_aml_ids += self._get_amls_from_batch_payments(batch, include_invoice_only=True)
def _prepare_embedded_views_data(self):
results = super()._prepare_embedded_views_data()
st_line = self.st_line_id
context = {
'search_view_ref': 'odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget',
'list_view_ref': 'odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget',
}
dynamic_filters = []
journal = st_line.journal_id
dynamic_filters.append({
'name': 'same_journal',
'description': journal.display_name,
'domain': [('journal_id', '=', journal.id)],
})
context['search_default_same_journal'] = True
context['search_default_unreconciled'] = True
if self.transaction_currency_id != self.company_currency_id:
context['search_default_currency_id'] = self.transaction_currency_id.id
for dynamic_filter in dynamic_filters:
dynamic_filter['domain'] = str(dynamic_filter['domain'])
results['batch_payments'] = {
'domain': [],
'dynamic_filters': dynamic_filters,
'context': context,
}
return results
def _lines_prepare_new_aml_line(self, aml, **kwargs):
return super()._lines_prepare_new_aml_line(
aml,
source_batch_payment_id=aml.payment_id.batch_payment_id.id or aml.move_id.matched_payment_ids.batch_payment_id[:1].id,
**kwargs,
)
def _get_amls_from_batch_payments(self, batch_payments, include_invoice_only=False):
amls_domain = self.st_line_id._get_default_amls_matching_domain()
amls = self.env['account.move.line']
for batch in batch_payments:
for payment in batch.payment_ids:
if payment.move_id:
liquidity_lines, _counterpart_lines, _writeoff_lines = payment._seek_for_lines()
amls |= liquidity_lines.filtered_domain(amls_domain)
elif payment.invoice_ids and include_invoice_only:
amls |= payment.invoice_ids.line_ids.filtered(lambda line: line.account_id.account_type in payment._get_valid_payment_account_types())
return amls
def _lines_prepare_new_batch_line(self, batch_payment, **kwargs):
self.ensure_one()
return {
'source_batch_payment_id': batch_payment.id,
'flag': 'new_batch',
'currency_id': batch_payment.payment_ids.currency_id.id if len(batch_payment.payment_ids.currency_id) == 1 else False,
'amount_currency': -batch_payment.amount_residual_currency,
'balance': -batch_payment.amount_residual,
'source_amount_currency': -batch_payment.amount_residual_currency,
'source_balance': -batch_payment.amount_residual,
'source_batch_payment_name': _("Includes %(count)s payment(s)", count=str(len(batch_payment.payment_ids.filtered(lambda p: p.state == 'in_process')))),
'date': batch_payment.date,
'name': batch_payment.name,
**kwargs,
}
def _get_amls_vals_from_payment(self, payment):
amls_line_vals = []
amls_domain = self.st_line_id._get_default_amls_matching_domain()
if payment.move_id:
liquidity_lines, _counterpart_lines, _writeoff_lines = payment._seek_for_lines()
return [Command.create(self._lines_prepare_new_aml_line(aml)) for aml in liquidity_lines.filtered_domain(amls_domain)]
elif payment.invoice_ids:
invoices_amls = payment.invoice_ids.line_ids.filtered(lambda line: line.account_id.account_type in payment._get_valid_payment_account_types())
payment_residual = payment.amount
comp_curr = self.company_id.currency_id
for aml in invoices_amls.sorted(lambda aml: aml.date_maturity):
if payment.currency_id.compare_amounts(payment_residual, 0) <= 0:
break
if aml.company_currency_id.is_zero(aml.amount_residual):
continue
amls_line_vals.append(Command.create(self._lines_prepare_new_aml_line(aml)))
if payment.currency_id == aml.currency_id:
payment_residual -= aml.amount_residual
elif payment.currency_id == comp_curr:
payment_residual -= aml.currency_id._convert(aml.amount_residual_currency, payment.currency_id, self.company_id, self.st_line_id.date)
else:
payment_residual -= comp_curr._convert(aml.amount_residual, payment.currency_id, self.company_id, self.st_line_id.date)
return amls_line_vals
def _get_amls_vals_from_batch(self, batch_payment):
amls_line_vals = []
for payment in batch_payment.payment_ids:
amls_line_vals += self._get_amls_vals_from_payment(payment)
return amls_line_vals
def _lines_load_new_batch_payments(self, batch_payments, reco_model=None):
""" Create counterpart lines for the batch payments passed as parameter."""
line_ids_commands = []
kwargs = {'reconcile_model_id': reco_model.id} if reco_model else {}
for batch in batch_payments:
if self._check_for_epd(batch):
line_ids_commands += self._get_amls_vals_from_batch(batch)
else:
aml_line_vals = self._lines_prepare_new_batch_line(batch, **kwargs)
line_ids_commands.append(Command.create(aml_line_vals))
if not line_ids_commands:
return
self.line_ids = line_ids_commands
def _get_key_mapping_aml_and_exchange_diff(self, line):
if line.flag in ('new_batch', 'exchange_diff') and line.source_batch_payment_id:
return 'source_batch_payment_id', line.source_batch_payment_id.id
return super()._get_key_mapping_aml_and_exchange_diff(line)
def _lines_get_exchange_diff_values(self, line):
if line.flag != 'new_batch':
return super()._lines_get_exchange_diff_values(line)
exchange_diff_values = []
currency_x_exchange = {}
for currency, balance, amount_currency in [
(aml.currency_id, -aml.amount_residual, -aml.amount_residual_currency)
for aml in self._get_amls_from_batch_payments(line.source_batch_payment_id)
] + [
(payment.currency_id, -payment.amount_company_currency_signed, -payment.amount_signed)
for payment in line.source_batch_payment_id.payment_ids.filtered(lambda p: not p.move_id)
]:
account, exchange_diff_balance = self._lines_get_account_balance_exchange_diff(
currency,
balance,
amount_currency,
)
if exchange_diff_balance != 0.0:
currency_exch_amounts = currency_x_exchange.get((currency, account), {
'amount_currency': 0.0,
'balance': 0.0,
})
currency_exch_amounts['amount_currency'] += exchange_diff_balance if currency == self.company_currency_id else 0.0
currency_exch_amounts['balance'] += exchange_diff_balance
currency_x_exchange[currency, account] = currency_exch_amounts
for (currency, account), exch_amounts in currency_x_exchange.items():
if not currency.is_zero(exch_amounts['balance']):
exchange_diff_values.append({
'flag': 'exchange_diff',
'source_batch_payment_id': line.source_batch_payment_id.id,
'name': _("Exchange Difference: %(batch_name)s - %(currency)s", batch_name=line.source_batch_payment_id.name, currency=currency.name),
'account_id': account.id,
'currency_id': currency.id,
'amount_currency': exch_amounts['amount_currency'],
'balance': exch_amounts['balance'],
})
return exchange_diff_values
def _validation_lines_vals(self, line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile):
source2exchange = self.line_ids.filtered(lambda l: l.flag == 'exchange_diff').grouped('source_batch_payment_id')
batch_lines = self.line_ids.filtered(lambda x: x.flag == 'new_batch')
valid_payment_states = batch_lines.source_batch_payment_id._valid_payment_states()
for line in batch_lines:
for payment in line.source_batch_payment_id.payment_ids.filtered(lambda p: p.state in valid_payment_states):
account2amount = defaultdict(float)
account2lines = defaultdict(list)
term_lines = iter(payment.invoice_ids.line_ids.filtered(lambda l: l.display_type == 'payment_term' and not l.reconciled).sorted('date'))
remaining = payment.amount_signed
select_amount_func = min if payment.payment_type == 'inbound' else max
while remaining and (term_line := next(term_lines, None)):
current = select_amount_func(remaining, term_line.currency_id._convert(
from_amount=term_line.amount_currency,
to_currency=payment.currency_id,
))
remaining -= current
account2amount[term_line.account_id] -= current
account2lines[term_line.account_id].append(term_line.id)
if remaining:
partner_account = (
payment.partner_id.property_account_payable_id
if payment.payment_type == "outbound"
else payment.partner_id.property_account_receivable_id
)
account2amount[partner_account] -= remaining
for account, amount in account2amount.items():
line_ids_create_command_list.append(Command.create(line._get_aml_values(
sequence=len(line_ids_create_command_list) + 1,
partner_id=payment.partner_id.id,
account_id=account.id,
currency_id=payment.currency_id.id,
amount_currency=amount,
balance=payment.currency_id._convert(from_amount=amount, to_currency=self.env.company.currency_id, date=payment.date),
)))
if lines := self.env['account.move.line'].browse(account2lines[account]):
to_reconcile.append((len(line_ids_create_command_list), lines))
exchange_diff_vals = source2exchange.get(line.source_batch_payment_id, [])
for exchange_diff in exchange_diff_vals:
aml_to_exchange_diff_vals[len(line_ids_create_command_list) + 1] = {
'amount_residual': exchange_diff.balance,
'amount_residual_currency': exchange_diff.amount_currency,
'analytic_distribution': exchange_diff.analytic_distribution,
}
line_ids_create_command_list.append(Command.create(exchange_diff._get_aml_values(
sequence=len(line_ids_create_command_list) + 1,
)))
batch_lines.source_batch_payment_id.payment_ids.filtered(lambda p: not p.move_id and p.state in valid_payment_states).action_validate()
self.line_ids -= batch_lines
super()._validation_lines_vals(line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile)
def _check_for_epd(self, batch_payment):
valid_payment_states = batch_payment._valid_payment_states()
no_move_payments = batch_payment.payment_ids.filtered(lambda payment: not payment.move_id)
if no_move_payments.invoice_ids.currency_id == self.transaction_currency_id:
for payment in no_move_payments:
if (
len(payment.invoice_ids) == 1
and payment.state in valid_payment_states
and payment.invoice_ids._is_eligible_for_early_payment_discount(self.transaction_currency_id, self.st_line_id.date)
):
return True
return False
def _process_restore_lines_ids(self, initial_commands):
commands = []
for command in super()._process_restore_lines_ids(initial_commands):
match command:
case (Command.CREATE, _, values) if values.get('flag') == 'new_batch':
batch = self.env['account.batch.payment'].browse(values['source_batch_payment_id'])
commands.append(Command.create(self._lines_prepare_new_batch_line(batch)))
case _:
commands.append(command)
return commands
def _action_validate(self):
self.ensure_one()
batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
batches_to_expand = batches.filtered('payment_ids.move_id')
self._action_expand_batch_payments(batches_to_expand)
super()._action_validate()
def _action_add_new_batched_amls(self, batch_payments, reco_model=None, allow_partial=True):
self.ensure_one()
existing_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
batch_payments = batch_payments - existing_batches
if not batch_payments:
return
existing_batch_new_amls = self.line_ids.filtered(lambda x: x.flag == 'new_aml' and x.source_batch_payment_id in batch_payments)
self._action_remove_lines(existing_batch_new_amls)
self._lines_load_new_batch_payments(batch_payments, reco_model=reco_model)
added_lines = self.line_ids.filtered(lambda x: x.flag in ('new_batch', 'new_aml') and x.source_batch_payment_id in batch_payments)
self._lines_recompute_exchange_diff(added_lines)
if not self._lines_check_apply_early_payment_discount():
self._lines_check_apply_partial_matching()
self._lines_add_auto_balance_line()
self._action_clear_manual_operations_form()
def _action_add_new_batch_payments(self, batch_payments):
self.ensure_one()
mounted_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
self._action_add_new_batched_amls(batch_payments - mounted_batches, allow_partial=False)
def _js_action_add_new_batch_payment(self, batch_payment_id):
self.ensure_one()
batch_payment = self.env['account.batch.payment'].browse(batch_payment_id)
self._action_add_new_batch_payments(batch_payment)
def _action_remove_new_batch_payments(self, batch_payments):
self.ensure_one()
lines = self.line_ids.filtered(lambda x: x.flag in ('new_aml', 'new_batch') and x.source_batch_payment_id in batch_payments)
self._action_remove_lines(lines)
def _js_action_remove_new_batch_payment(self, batch_payment_id):
self.ensure_one()
batch_payment = self.env['account.batch.payment'].browse(batch_payment_id)
self._action_remove_new_batch_payments(batch_payment)
def _action_remove_lines(self, lines):
self.ensure_one()
if not lines:
return
has_new_batch = any(line.flag == 'new_batch' for line in lines)
has_new_aml = any(line.flag == 'new_aml' for line in lines)
super()._action_remove_lines(lines)
if has_new_batch and not has_new_aml:
self._lines_check_apply_partial_matching()
self._lines_add_auto_balance_line()
def _action_expand_batch_payments(self, batch_payments):
self.ensure_one()
if not batch_payments:
return
batch_lines = self.line_ids.filtered(lambda x: x.flag == 'new_batch' and x.source_batch_payment_id in batch_payments)
if not batch_lines:
return
batch_unlink_commands = []
for batch_line in batch_lines:
batch_unlink_commands.append(Command.unlink(batch_line.id))
self.line_ids = batch_unlink_commands
self._remove_related_exchange_diff_lines(batch_lines)
self._action_add_new_amls(self._get_amls_from_batch_payments(batch_payments), allow_partial=False)
def _js_action_redirect_to_move(self, form_index):
self.ensure_one()
line = self.line_ids.filtered(lambda x: x.index == form_index)
if line.source_batch_payment_id:
self.return_todo_command = clean_action(line.source_batch_payment_id._get_records_action(), self.env)
else:
return super()._js_action_redirect_to_move(form_index)

View File

@ -0,0 +1,10 @@
from odoo import fields, models
class BankRecWidgetLine(models.Model):
_inherit = 'bank.rec.widget.line'
source_batch_payment_id = fields.Many2one(comodel_name='account.batch.payment')
flag = fields.Selection(selection_add=[('new_batch', 'new_batch')])
source_batch_payment_name = fields.Char()

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_batch_payment_rejection,account.batch.payment.rejection,model_account_batch_payment_rejection,account.group_account_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_batch_payment_rejection account.batch.payment.rejection model_account_batch_payment_rejection account.group_account_user 1 1 1 0

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordNotebookBatchPayments">
<div class="bank_rec_widget_form_batch_payments_list_anchor" t-if="this.state.bankRecEmbeddedViewsData">
<BankRecViewEmbedder viewProps="this.notebookBatchPaymentsListViewProps()" t-key="data.st_line_id[0]"/>
</div>
</t>
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordForm"
t-inherit="account_accountant.BankRecRecordForm"
t-inherit-mode="extension"
>
<xpath expr="//t[@t-set-slot='amls_tab']" position="after">
<t t-set-slot="batch_payments_tab"
name="'batch_payments_tab'"
title.translate="Batch Payments"
isVisible="['valid', 'invalid'].includes(data.state)">
<t t-call="odex30_account_accountant_batch_payment.BankRecRecordNotebookBatchPayments"/>
</t>
</xpath>
</t>
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordFormLineIds"
t-inherit="account_accountant.BankRecRecordFormLineIds"
t-inherit-mode="extension"
>
<xpath expr="//td[@field='account_id']" position="replace">
<t t-if="line.data.flag === 'new_batch'">
<td field="source_batch_payment_name">
<span t-out="line.data.source_batch_payment_name"/>
</td>
</t>
<t t-else="">$0</t>
</xpath>
<xpath expr="//td[@field='name']" position="replace">
<t t-if="line.data.flag === 'new_batch'">
<td field="name" t-att-colspan="line_ids_columns.length - (display_currency_columns ? 5 : 3) + (hasGroupReadOnly ? 0 : 1)">
<span class="o_form_uri fst-italic"
t-out="line.data.name"
t-on-click="() => this.actionRedirectToSourceMove(line)"/>
</td>
</t>
<t t-else="">$0</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,47 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { EmbeddedListView } from "@odex30_account_accountant/components/bank_reconciliation/embedded_list_view";
import { ListRenderer } from "@web/views/list/list_renderer";
import { useState, onWillUnmount } from "@odoo/owl";
export class BankRecBatchPaymentsRenderer extends ListRenderer {
setup() {
super.setup();
this.globalState = useState(this.env.methods.getState());
onWillUnmount(this.saveSearchState);
}
getRowClass(record) {
const classes = super.getRowClass(record);
const batchId = this.globalState.bankRecRecordData.selected_batch_payment_ids.currentIds.find((x) => x === record.resId);
if (batchId){
return `${classes} o_rec_widget_list_selected_item table-info`;
}
return classes;
}
async onCellClicked(record, column, ev) {
const batchId = this.globalState.bankRecRecordData.selected_batch_payment_ids.currentIds.find((x) => x === record.resId);
if (batchId) {
this.env.config.actionRemoveNewBatchPayment(record.resId);
} else {
this.env.config.actionAddNewBatchPayment(record.resId);
}
}
saveSearchState() {
const initParams = this.globalState.bankRecEmbeddedViewsData.batch_payments;
const searchModel = this.env.searchModel;
initParams.exportState = {searchModel: JSON.stringify(searchModel.exportState())};
}
}
export const BankRecBatchPayments = {
...EmbeddedListView,
Renderer: BankRecBatchPaymentsRenderer,
};
registry.category("views").add("bank_rec_batch_payments_list_view", BankRecBatchPayments);

View File

@ -0,0 +1,92 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { BankRecKanbanController } from "@odex30_account_accountant/components/bank_reconciliation/kanban";
patch(BankRecKanbanController.prototype, {
getChildSubEnv(){
const env = super.getChildSubEnv(...arguments);
env.methods.actionAddNewBatchPayment = this.actionAddNewBatchPayment.bind(this);
env.methods.actionRemoveNewBatchPayment = this.actionRemoveNewBatchPayment.bind(this);
return env;
},
notebookBatchPaymentsListViewProps(){
const initParams = this.state.bankRecEmbeddedViewsData.batch_payments;
return {
type: "list",
noBreadcrumbs: true,
resModel: "account.batch.payment",
searchMenuTypes: ["filter"],
domain: initParams.domain,
dynamicFilters: initParams.dynamic_filters,
context: initParams.context,
allowSelectors: false,
searchViewId: false,
globalState: initParams.exportState,
};
},
getBankRecLineInvalidFields(line){
if (line.data.flag === 'new_batch') {
return [];
}
return super.getBankRecLineInvalidFields(line);
},
async actionAddNewBatchPayment(batchId){
await this.execProtectedBankRecAction(async () => {
await this.withNewState(async (newState) => {
await this.onchange(newState, "add_new_batch_payment", [batchId]);
});
});
},
async actionRemoveNewBatchPayment(batchId){
await this.execProtectedBankRecAction(async () => {
await this.withNewState(async (newState) => {
await this.onchange(newState, "remove_new_batch_payment", [batchId]);
});
});
},
async actionValidateOnCloseWizard(){
await this.execProtectedBankRecAction(async () => {
await this.withNewState(async (newState) => {
const { return_todo_command: result } = await this.onchange(newState, "validate_no_batch_payment_check");
if(result.done){
await this.moveToNextLine(newState);
}
});
});
},
async _actionValidate(newState){
const result = await super._actionValidate(...arguments);
if(!result){
return;
}
if(result.open_batch_rejection_wizard){
const validateFunc = this.actionValidateOnCloseWizard.bind(this);
this.action.doAction(
result.open_batch_rejection_wizard,
{
onClose: async (nextAction) => {
if(nextAction === "validate"){
await validateFunc();
}
},
}
);
}
},
});

View File

@ -0,0 +1,119 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import { accountTourSteps } from "@account/js/tours/account";
registry.category("web_tour.tours").add("account_accountant_batch_payment_bank_rec_widget", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
...accountTourSteps.goToAccountMenu("Open the accounting module"),
{
trigger: ".o_breadcrumb",
},
{
content: "Open the bank reconciliation widget",
trigger: "button.btn-secondary[name='action_open_reconcile']",
run: "click",
},
{
content: "The 'line1' should be selected by default",
trigger: "div[name='line_ids'] td[field='name']:contains('line1')",
},
{
content: "Click on the 'batch_payments_tab'",
trigger: "a[name='batch_payments_tab']",
run: "click",
},
{
content: "Mount BATCH0001",
trigger:
"div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table td[name='name']:contains('BATCH0001')",
run: "click",
},
{
content: "The batch should be selected",
trigger:
"div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table tr.o_rec_widget_list_selected_item",
},
{
content: "Open the batch",
trigger: "div[name='line_ids'] .o_bank_rec_second_line .o_form_uri",
run: "click",
},
{
content: "Open the payment of 100.0",
trigger: "div[name='payment_ids'] tbody tr.o_data_row:last .o_list_record_open_form_view button",
run: "click",
},
{
content: "Reject it",
trigger: "button[name='action_reject']",
run: "click",
},
{
content: "Go back to the reconciliation widget",
trigger: "a[href$='/reconciliation']",
run: "click",
},
{
trigger: "div[name='line_ids'] td[field='name']:contains('line1')",
},
{
trigger: "button.btn-primary:contains('Validate')",
},
{
content: "Validate",
trigger: "button:contains('Validate')",
run: "click",
},
...stepUtils.toggleHomeMenu(),
...accountTourSteps.goToAccountMenu("Reset back to accounting module"),
{
content: "check that we're back on the dashboard",
trigger: 'a:contains("Customer Invoices")',
},
],
});
registry.category("web_tour.tours").add("account_accountant_batch_payment_bank_rec_widget_batch_line_clickable", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
...accountTourSteps.goToAccountMenu("Open the accounting module"),
{
trigger: ".o_breadcrumb",
},
{
content: "Open the bank reconciliation widget",
trigger: "button.btn-secondary[name='action_open_reconcile']",
run: "click",
},
{
content: "Click on the 'batch_payments_tab'",
trigger: "a[name='batch_payments_tab']",
run: "click",
},
{
content: "Mount BATCH0001",
trigger: "div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table td[name='name']:contains('BATCH0001')",
run: "click",
},
{
content: "The batch should be selected",
trigger: "div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table tr.o_rec_widget_list_selected_item",
},
{
content: "Click batch row for BATCH0001",
trigger: ".o_data_row.o_selected_row.o_list_no_open.o_bank_rec_second_line:contains('BATCH0001')",
run: "click",
},
{
content: "Wait for Manual Operations tab to open",
trigger: "div[name='analytic_distribution']:not(:visible)",
},
],
});

View File

@ -0,0 +1,4 @@
from . import test_batch_payment
from . import test_bank_rec_widget
from . import test_bank_rec_widget_tour

View File

@ -0,0 +1,77 @@
from odoo import Command
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestMockOnlineSyncCommon
from odoo.addons.odex30_account_accountant.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon
@tagged('post_install', '-at_install')
class TestBankRecWidgetTour(TestBankRecWidgetCommon, AccountTestMockOnlineSyncCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['account.reconcile.model']\
.search([('company_id', '=', cls.company.id)])\
.write({'past_months_limit': None})
def test_tour_bank_rec_widget(self):
self._create_st_line(500.0, payment_ref="line1", sequence=1)
self._create_st_line(100.0, payment_ref="line2", sequence=2)
self._create_st_line(100.0, payment_ref="line3", sequence=3)
self._create_st_line(1000.0, payment_ref="line_credit", sequence=4, journal_id=self.company_data['default_journal_credit'].id)
payment_method_line = self.company_data['default_journal_bank'].inbound_payment_method_line_ids\
.filtered(lambda l: l.code == 'batch_payment')
payment_method_line.payment_account_id = self.inbound_payment_method_line.payment_account_id
payments = self.env['account.payment'].create([
{
'date': '2020-01-01',
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'payment_method_line_id': payment_method_line.id,
'amount': i * 100.0,
}
for i in range(1, 4)
])
payments.action_post()
batch = self.env['account.batch.payment'].create({
'name': "BATCH0001",
'date': '2020-01-01',
'journal_id': self.company_data['default_journal_bank'].id,
'payment_ids': [Command.set(payments.ids)],
'payment_method_id': payment_method_line.payment_method_id.id,
})
batch.validate_batch()
self.start_tour('/odoo', 'account_accountant_batch_payment_bank_rec_widget', login=self.env.user.login)
def test_batch_line_clickable(self):
self._create_st_line(500.0, payment_ref="line1", sequence=1)
payments = self.env['account.payment'].create([
{
'date': '2020-01-01',
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'amount': i * 100.0,
}
for i in range(1, 3)
])
payments.action_post()
batch = self.env['account.batch.payment'].create({
'name': "BATCH0001",
'date': '2020-01-01',
'journal_id': self.company_data['default_journal_bank'].id,
'payment_ids': [Command.set(payments.ids)],
})
self.env.user.write({'groups_id': [Command.link(self.env.ref('analytic.group_analytic_accounting').id)]})
batch.validate_batch()
self.start_tour('/odoo', 'account_accountant_batch_payment_bank_rec_widget_batch_line_clickable', login=self.env.user.login)

View File

@ -0,0 +1,41 @@
import time
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestBatchPayment(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.journal = cls.company_data['default_journal_bank']
cls.batch_deposit_method = cls.env.ref('odex30_account_batch_payment.account_payment_method_batch_deposit')
cls.batch_deposit = cls.journal.inbound_payment_method_line_ids.filtered(lambda l: l.code == 'batch_payment')
@classmethod
def createPayment(cls, partner, amount):
payment = cls.env['account.payment'].create({
'journal_id': cls.journal.id,
'payment_method_line_id': cls.batch_deposit.id,
'payment_type': 'inbound',
'date': time.strftime('%Y') + '-07-15',
'amount': amount,
'partner_id': partner.id,
'partner_type': 'customer',
})
payment.action_post()
return payment
def test_zero_amount_payment(self):
zero_payment = self.createPayment(self.partner_a, 0)
batch_vals = {
'journal_id': self.journal.id,
'payment_ids': [(4, zero_payment.id, None)],
'payment_method_id': self.batch_deposit_method.id,
}
self.assertRaises(ValidationError, self.env['account.batch.payment'].create, batch_vals)

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_batch_payment_rejection_form" model="ir.ui.view">
<field name="name">account.batch.payment.rejection.form</field>
<field name="model">account.batch.payment.rejection</field>
<field name="arch" type="xml">
<form string="Batch Payment">
<field name="in_reconcile_payment_ids" invisible="1"/>
<field name="rejected_payment_ids" invisible="1"/>
<div>
<span invisible="1 not in nb_batch_payment_ids"><field name="nb_rejected_payment_ids"/> payments from the batch have been removed.</span>
<span invisible="1 in nb_batch_payment_ids"><field name="nb_rejected_payment_ids"/> payments from <field name="nb_batch_payment_ids"/> batches have been removed.</span>
<br/>
<span>Do you want to cancel payments to retry them later or keep the batch open with unprocess payments, if you expect them later.</span>
</div>
<footer>
<button string="Cancel Payments"
name="button_cancel_payments"
type="object"
class="btn btn-primary"
close="1"
data-hotkey="q"/>
<button string="Expect Payments Later"
name="button_continue"
type="object"
class="btn btn-secondary"
close="1"
data-hotkey="l"/>
<button string="Cancel"
name="button_cancel"
type="object"
class="btn btn-secondary"
close="1"
data-hotkey="x"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_batch_payment_search_bank_rec_widget" model="ir.ui.view">
<field name="name">account.batch.payment.search.bank_rec_widget</field>
<field name="model">account.batch.payment</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<search>
<field name="name"
string="Batch Payment"
filter_domain="[('name', 'ilike', self)]"/>
<field name="date"/>
<field name="journal_id"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<separator/>
<filter name="amount_received" string="Received" domain="[('batch_type', '=', 'inbound')]"/>
<filter name="amount_paid" string="Paid" domain="[('batch_type', '=', 'outbound')]"/>
<separator/>
<filter name="unreconciled" string="Unreconciled" domain="[('state', '!=', 'reconciled')]"/>
<separator name="inject_after"/>
<filter name="date" string="Date" date="date"/>
</search>
</field>
</record>
<record id="view_account_batch_payment_list_bank_rec_widget" model="ir.ui.view">
<field name="name">account.batch.payment.list.bank_rec_widget</field>
<field name="model">account.batch.payment</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<list string="Suggestions"
create="false"
edit="false"
limit="40"
js_class="bank_rec_batch_payments_list_view">
<!-- Invisible fields -->
<field name="currency_id" column_invisible="True"/>
<field name="company_currency_id" column_invisible="True"/>
<field name="state" column_invisible="True"/>
<field name="date" readonly="state != 'draft'"/>
<field name="name" readonly="state != 'draft'"/>
<field name="journal_id"
optional="hidden" readonly="state != 'draft'"/>
<field name="amount_residual_currency"
string="Amount Due (in currency)"/>
<field name="amount_residual"
string="Amount Due"
groups="base.group_multi_currency"
optional="hidden"/>
<button name="action_open_batch_payment"
type="object"
string="View"
class="btn btn-sm btn-secondary"/>
</list>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,10 @@
from . import models
from . import wizard
def _post_init_hook(env):
if companies := env['res.company'].search([('chart_template', '=', 'generic_coa')], order="parent_path"):
avatax_fiscal_position = env['account.chart.template']._get_us_avatax_fiscal_position()
for company in companies:
Template = env['account.chart.template'].with_company(company)
Template._load_data({'account.fiscal.position': avatax_fiscal_position})

View File

@ -0,0 +1,27 @@
{
'name': 'Avatax',
'version': '1.0',
'category': 'Accounting/Accounting',
'website': 'http://exp-sa.com',
'author': 'Expert Co. Ltd.',
'countries': ['us', 'ca'],
'depends': ['payment', 'odex30_account_external_tax'],
'auto_install': ['payment'],
'data': [
'security/ir.model.access.csv',
'data/product.avatax.category.csv',
'data/fiscal_position.xml',
'views/account_fiscal_position_views.xml',
'views/account_move_views.xml',
'views/avatax_category_views.xml',
'views/avatax_exemption_views.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/product_views.xml',
'wizard/avatax_validate_address_views.xml',
'wizard/avatax_connection_test_result_views.xml',
'reports/account_invoice.xml',
],
'license': 'OEEL-1',
'post_init_hook': '_post_init_hook',
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="account_fiscal_position_avatax_us" model="account.fiscal.position">
<field name="name">Automatic Tax Mapping (AvaTax)</field>
<field name="is_avatax" eval="True"/>
<field name="auto_apply" eval="False"/>
<field name="country_id" ref="base.us"/>
</record>
</odoo>

View File

@ -0,0 +1,5 @@
-- disable AvaTax
UPDATE res_company
SET avalara_environment = 'sandbox';
UPDATE account_fiscal_position
SET is_avatax = false;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,724 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_avatax
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-15 21:10+0000\n"
"PO-Revision-Date: 2026-01-15 21:10+0000\n"
"Last-Translator: Odoo ERP Developer\n"
"Language-Team: Arabic (Saudi Arabia)\n"
"Language: ar_SA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid "- %(partner_name)s (ID: %(partner_id)s) on %(record_list)s"
msgstr "- %(partner_name)s (المعرف: %(partner_id)s) على %(record_list)s"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i class=\"oi oi-fw oi-arrow-right\"/>\n"
" How to Get Credentials"
msgstr ""
"<i class=\"oi oi-fw oi-arrow-right\"/>\n"
" كيفية الحصول على بيانات الاعتماد"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i title=\"Go to Avatax portal\" role=\"img\" aria-label=\"Go to Avatax portal\" class=\"fa fa-external-link-square fa-fw\"/>\n"
" Avatax portal"
msgstr ""
"<i title=\"الانتقال إلى بوابة Avatax\" role=\"img\" aria-label=\"الانتقال إلى بوابة Avatax\" class=\"fa fa-external-link-square fa-fw\"/>\n"
" بوابة Avatax"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i title=\"Show logs\" role=\"img\" aria-label=\"Show logs\" class=\"fa fa-file-text-o\"/>\n"
" Show logs"
msgstr ""
"<i title=\"عرض السجلات\" role=\"img\" aria-label=\"عرض السجلات\" class=\"fa fa-file-text-o\"/>\n"
" عرض السجلات"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i title=\"Start logging for 30 minutes\" role=\"img\" aria-label=\"Start logging for 30 minutes\" class=\"fa fa-file-text-o\"/>\n"
" Start logging for 30 minutes"
msgstr ""
"<i title=\"بدء التسجيل لمدة 30 دقيقة\" role=\"img\" aria-label=\"بدء التسجيل لمدة 30 دقيقة\" class=\"fa fa-file-text-o\"/>\n"
" بدء التسجيل لمدة 30 دقيقة"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i title=\"Sync Parameters\" role=\"img\" aria-label=\"Sync Parameters\" class=\"fa fa-refresh\"/>\n"
" Sync Parameters"
msgstr ""
"<i title=\"مزامنة المعلمات\" role=\"img\" aria-label=\"مزامنة المعلمات\" class=\"fa fa-refresh\"/>\n"
" مزامنة المعلمات"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid ""
"<i title=\"Test connection\" role=\"img\" aria-label=\"Test connection\" class=\"fa fa-plug fa-fw\"/>\n"
" Test connection"
msgstr ""
"<i title=\"اختبار الاتصال\" role=\"img\" aria-label=\"اختبار الاتصال\" class=\"fa fa-plug fa-fw\"/>\n"
" اختبار الاتصال"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "API ID"
msgstr "معرّف واجهة البرمجة (API ID)"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "API KEY"
msgstr "مفتاح واجهة البرمجة (API Key)"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_account_chart_template
msgid "Account Chart Template"
msgstr "قالب شجرة الحسابات"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_account_fiscal_position__avatax_invoice_account_id
msgid "Account that will be used by Avatax taxes for invoices."
msgstr "الحساب الذي سيتم استخدامه من قبل ضرائب Avatax للفواتير."
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_account_fiscal_position__avatax_refund_account_id
msgid "Account that will be used by Avatax taxes for refunds."
msgstr "الحساب الذي سيتم استخدامه من قبل ضرائب Avatax للمردودات."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Address Validation"
msgstr "التحقق من العنوان"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/wizard/avatax_validate_address.py:0
msgid "Address validation is only supported for North American addresses."
msgstr "التحقق من العنوان مدعوم فقط للعناوين في أمريكا الشمالية."
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/res_company.py:0
msgid "Authentication failed."
msgstr "فشل في المصادقة."
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/res_company.py:0
msgid "Authentication success."
msgstr "تمت المصادقة بنجاح."
#. module: odex30_account_avatax
#: model:account.fiscal.position,name:odex30_account_avatax.account_fiscal_position_avatax_us
msgid "Automatic Tax Mapping (AvaTax)"
msgstr "تعيين الضرائب تلقائياً (AvaTax)"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Automatically compute tax rates in the US and Canada."
msgstr "حساب معدلات الضرائب تلقائياً في الولايات المتحدة وكندا."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "AvaTax"
msgstr "AvaTax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_api_id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_api_id
msgid "Avalara API ID"
msgstr "معرّف واجهة Avalara API"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_api_key
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_api_key
msgid "Avalara API KEY"
msgstr "مفتاح واجهة Avalara API"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_address_validation
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_address_validation
msgid "Avalara Address Validation"
msgstr "التحقق من عنوان Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_avatax_unique_code__avatax_unique_code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__avatax_unique_code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__avatax_unique_code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avatax_unique_code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avatax_unique_code
msgid "Avalara Code"
msgstr "رمز Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_partner_code
msgid "Avalara Company Code"
msgstr "رمز الشركة في Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_environment
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_environment
msgid "Avalara Environment"
msgstr "بيئة Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_exemption_id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_exemption_id
msgid "Avalara Exemption"
msgstr "إعفاء Avalara"
#. module: odex30_account_avatax
#: model:ir.actions.act_window,name:odex30_account_avatax.ir_logging_avalara_action
msgid "Avalara Logging"
msgstr "سجلات Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_partner_code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_partner_code
msgid "Avalara Partner Code"
msgstr "رمز الشريك في Avalara"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_show_address_validation
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_show_address_validation
msgid "Avalara Show Address Validation"
msgstr "إظهار التحقق من عنوان Avalara"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.account_fiscal_position_form_inherit
msgid "Avatax"
msgstr "Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_category__avatax_category_id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_product__avatax_category_id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_template__avatax_category_id
msgid "Avatax Category"
msgstr "فئة Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__avatax_tax_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__avatax_tax_date
msgid "Avatax Date"
msgstr "تاريخ Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__avatax_invoice_account_id
msgid "Avatax Invoice Account"
msgstr "حساب فاتورة Avatax"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_avatax_exemption
msgid "Avatax Partner Exemption Codes"
msgstr "رموز إعفاء شركاء Avatax"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_product_avatax_category
msgid "Avatax Product Category"
msgstr "فئة منتجات Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__avatax_refund_account_id
msgid "Avatax Refund Account"
msgstr "حساب مردودات Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_account_bank_statement_line__avatax_tax_date
#: model:ir.model.fields,help:odex30_account_avatax.field_account_move__avatax_tax_date
msgid ""
"Avatax will use this date to calculate the tax on this invoice. If not "
"specified it will use the Invoice Date."
msgstr ""
"سيستخدم Avatax هذا التاريخ لحساب الضريبة على هذه الفاتورة. إذا لم يتم "
"تحديده، سيستخدم تاريخ الفاتورة."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Cancel"
msgstr "إلغاء"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__city
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "City"
msgstr "المدينة"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_connection_test_result_view_form
msgid "Close"
msgstr "إغلاق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__code
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__code
msgid "Code"
msgstr "الرمز"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Commit Transactions"
msgstr "تأكيد المعاملات"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_commit
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_commit
msgid "Commit in Avatax"
msgstr "تأكيد في Avatax"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_res_company
msgid "Companies"
msgstr "الشركات"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__company_id
msgid "Company"
msgstr "الشركة"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Company Code"
msgstr "رمز الشركة"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_res_config_settings
msgid "Config Settings"
msgstr "إعدادات التكوين"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_res_partner
msgid "Contact"
msgstr "الاتصال"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__country_id
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Country"
msgstr "البلد"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__create_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__create_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__create_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__create_uid
msgid "Created by"
msgstr "أُنشئ بواسطة"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__create_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__create_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__create_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__create_date
msgid "Created on"
msgstr "تاريخ الإنشاء"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_res_partner__avalara_partner_code
#: model:ir.model.fields,help:odex30_account_avatax.field_res_users__avalara_partner_code
msgid "Customer Code set in Avalara for this partner."
msgstr "رمز العميل المُعيَّن في Avalara لهذا الشريك."
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__description
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__description
msgid "Description"
msgstr "الوصف"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__display_name
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__display_name
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__display_name
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__display_name
msgid "Display Name"
msgstr "اسم العرض"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid ""
"EXP could not change the state of the transaction related to %(document)s in AvaTax\n"
"Please check the status of `%(technical)s` in the AvaTax portal."
msgstr ""
"لم يتمكن EXP من تغيير حالة المعاملة المتعلقة بـ %(document)s في AvaTax\n"
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid ""
"EXP could not fetch the taxes related to %(document)s.\n"
"Please check the status of `%(technical)s` in the AvaTax portal."
msgstr ""
"لم يتمكن EXP من جلب الضرائب المتعلقة بـ %(document)s.\n"
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid ""
"EXP could not void the transaction related to %(document)s in AvaTax\n"
"Please check the status of `%(technical)s` in the AvaTax portal."
msgstr ""
"لم يتمكن EXP من إلغاء المعاملة المتعلقة بـ %(document)s في AvaTax\n"
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Environment"
msgstr "البيئة"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/wizard/avatax_validate_address.py:0
msgid "Exp could not validate the address of %(partner)s with Avalara."
msgstr "لم يتمكن Exp من التحقق من عنوان %(partner)s مع Avalara."
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_account_fiscal_position
msgid "Fiscal Position"
msgstr "المركز الضريبي"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_latitude
msgid "Geo Latitude"
msgstr "خط العرض الجغرافي"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_longitude
msgid "Geo Longitude"
msgstr "خط الطول الجغرافي"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid "Go to the configuration panel"
msgstr "الانتقال إلى لوحة الإعدادات"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__id
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__id
msgid "ID"
msgstr "المعرف"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__is_already_valid
msgid "Is Already Valid"
msgstr "صالح مسبقاً"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__is_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_external_tax_mixin__is_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__is_avatax
msgid "Is Avatax"
msgstr "هو Avatax"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__write_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__write_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__write_uid
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__write_uid
msgid "Last Updated by"
msgstr "آخر تحديث بواسطة"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__write_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__write_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__write_date
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__write_date
msgid "Last Updated on"
msgstr "تاريخ آخر تحديث"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Latitude"
msgstr "خط العرض"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Longitude"
msgstr "خط الطول"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_account_avatax_unique_code
msgid "Mixin to generate unique ids for Avatax"
msgstr "مزيج لتوليد معرفات فريدة لـ Avatax"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_account_external_tax_mixin
msgid "Mixin to manage common parts of external tax calculation"
msgstr "مزيج لإدارة الأجزاء المشتركة لحساب الضرائب الخارجية"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__name
msgid "Name"
msgstr "الاسم"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/res_company.py:0
msgid "Odoo could not fetch the exemption codes of %(company)s"
msgstr "لم يتمكن Odoo من جلب رموز الإعفاء لـ %(company)s"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Original Address"
msgstr "العنوان الأصلي"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__partner_id
msgid "Partner"
msgstr "الشريك"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid "Please add your AvaTax credentials"
msgstr "يرجى إضافة بيانات اعتماد AvaTax الخاصة بك"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_product_template
msgid "Product"
msgstr "المنتج"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_product_category
msgid "Product Category"
msgstr "فئة المنتج"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_product_product
msgid "Product Variant"
msgstr "متغير المنتج"
#. module: odex30_account_avatax
#: model:ir.model.fields.selection,name:odex30_account_avatax.selection__res_company__avalara_environment__production
msgid "Production"
msgstr "الإنتاج"
#. module: odex30_account_avatax
#: model:ir.model.fields.selection,name:odex30_account_avatax.selection__res_company__avalara_environment__sandbox
msgid "Sandbox"
msgstr "بيئة الاختبار"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Save Validated"
msgstr "حفظ المُصَدَّق"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_avatax_unique_code.py:0
msgid "Search operation not supported"
msgstr "عملية البحث غير مدعومة"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__server_response
msgid "Server Response"
msgstr "استجابة الخادم"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__state_id
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "State"
msgstr "الولاية"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__street
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Street"
msgstr "الشارع"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__street2
msgid "Street2"
msgstr "الشارع 2"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_avatax_validate_address
msgid "Suggests validated addresses from Avatax"
msgstr "يقترح عناوين مُصَدَّقة من Avatax"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Synchronize the exemption codes from Avatax"
msgstr "مزامنة رموز الإعفاء من Avatax"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/res_company.py:0
msgid "Test Result"
msgstr "نتيجة الاختبار"
#. module: odex30_account_avatax
#: model:ir.model,name:odex30_account_avatax.model_avatax_connection_test_result
msgid "Test connection with avatax"
msgstr "اختبار الاتصال مع Avatax"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_partner_code
msgid ""
"The Avalara Company Code for this company. Avalara will interpret as DEFAULT"
" if it is not set."
msgstr ""
"رمز الشركة في Avalara لهذه الشركة. سيتعامل Avalara معه كـ 'DEFAULT' إذا لم "
"يتم تعيينه."
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid ""
"The Avalara Tax Code is required for %(name)s (#%(id)s)\n"
"See https://taxcode.avatax.avalara.com/"
msgstr ""
"رمز ضريبة Avalara مطلوب لـ %(name)s (#%(id)s)\n"
"انظر https://taxcode.avatax.avalara.com/"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
msgid ""
"The following customer(s) need to have a zip, state and country when using "
"Avatax:"
msgstr ""
"يحتاج العملاء التاليون إلى رمز بريدي وولاية وبلد عند استخدام Avatax:"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_commit
msgid "The transactions will be committed for reporting in Avatax."
msgstr "ستُؤَكَّد المعاملات للإبلاغ في Avatax."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "This is already a valid address."
msgstr "هذا عنوان صالح بالفعل."
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__setting_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__setting_account_avatax
msgid "Use AvaTax"
msgstr "استخدام AvaTax"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__is_avatax
msgid "Use AvaTax API"
msgstr "استخدام واجهة AvaTax API"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_use_upc
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_use_upc
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
msgid "Use UPC"
msgstr "استخدام UPC"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_use_upc
msgid "Use Universal Product Code instead of custom defined codes in Avalara."
msgstr "استخدام رمز المنتج العالمي بدلاً من الرموز المخصصة في Avalara."
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_account_avatax_unique_code__avatax_unique_code
#: model:ir.model.fields,help:odex30_account_avatax.field_account_bank_statement_line__avatax_unique_code
#: model:ir.model.fields,help:odex30_account_avatax.field_account_move__avatax_unique_code
#: model:ir.model.fields,help:odex30_account_avatax.field_res_partner__avatax_unique_code
#: model:ir.model.fields,help:odex30_account_avatax.field_res_users__avatax_unique_code
msgid "Use this code to cross-reference in the Avalara portal."
msgstr "استخدم هذا الرمز للإحالة المتبادلة في بوابة Avalara."
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__valid_country_ids
msgid "Valid Country"
msgstr "البلد الصالح"
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_partner_form_inherit
msgid "Validate"
msgstr "التحقق"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/res_partner.py:0
msgid "Validate address of %s"
msgstr "التحقق من عنوان %s"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_address_validation
msgid ""
"Validate and correct the addresses of partners in North America with "
"Avalara."
msgstr ""
"التحقق من عناوين الشركاء في أمريكا الشمالية وتصحيحها مع Avalara."
#. module: odex30_account_avatax
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Validated Address"
msgstr "العنوان المُصَدَّق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_city
msgid "Validated City"
msgstr "المدينة المُصَدَّقة"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_country_id
msgid "Validated Country"
msgstr "البلد المُصَدَّق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_state_id
msgid "Validated State"
msgstr "الولاية المُصَدَّقة"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_street
msgid "Validated Street"
msgstr "الشارع المُصَدَّق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_street2
msgid "Validated Street2"
msgstr "الشارع 2 المُصَدَّق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_zip
msgid "Validated Zip Code"
msgstr "رمز البريد المُصَدَّق"
#. module: odex30_account_avatax
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__zip
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
msgid "Zip Code"
msgstr "رمز البريد"
#. module: odex30_account_avatax
#. odoo-python
#: code:addons/odex30_account_avatax/models/product.py:0
msgid "[%(code)s] %(description)s"
msgstr "[%(code)s] %(description)s"
#. module: odex30_account_avatax
#: model:ir.model.fields,help:odex30_account_avatax.field_product_category__avatax_category_id
#: model:ir.model.fields,help:odex30_account_avatax.field_product_product__avatax_category_id
#: model:ir.model.fields,help:odex30_account_avatax.field_product_template__avatax_category_id
msgid "https://taxcode.avatax.avalara.com/"
msgstr "https://taxcode.avatax.avalara.com/"

View File

@ -0,0 +1,80 @@
from requests.auth import HTTPBasicAuth
from datetime import datetime
from pprint import pformat
import requests
import logging
str_type = (str, type(None))
_logger = logging.getLogger(__name__)
class AvataxClient:
def __init__(self, app_name=None, app_version=None, machine_name=None,
environment=None, timeout_limit=None):
if not all(isinstance(i, str_type) for i in [app_name,
machine_name,
environment]):
raise ValueError('Input(s) must be string or none type object')
self.base_url = 'https://sandbox-rest.avatax.com'
self.is_production = environment and environment.lower() == 'production'
if self.is_production:
self.base_url = 'https://rest.avatax.com'
self.auth = None
self.app_name = app_name
self.app_version = app_version
self.machine_name = machine_name
self.client_id = '{}; {}; Python SDK; 18.5; {};'.format(app_name,
app_version,
machine_name)
self.client_header = {'X-Avalara-Client': self.client_id}
self.timeout_limit = timeout_limit
def add_credentials(self, username=None, password=None):
if not all(isinstance(i, str_type) for i in [username, password]):
raise ValueError('Input(s) must be string or none type object')
if username and not password:
self.client_header['Authorization'] = 'Bearer ' + username
else:
self.auth = HTTPBasicAuth(username, password)
return self
def request(self, method, endpoint, params, json):
start = str(datetime.utcnow())
url = '{}/api/v2/{}'.format(self.base_url, endpoint)
response = requests.request(
method, url,
auth=self.auth,
headers=self.client_header,
timeout=self.timeout_limit if self.timeout_limit else 1200,
params=params,
json=json
).json()
end = str(datetime.utcnow())
if hasattr(self, 'logger'):
self.logger(
f"{method}\nstart={start}\nend={end}\nargs={pformat(url)}\nparams={pformat(params)}\njson={pformat(json)}\n"
f"response={pformat(response)}"
)
return response
def create_transaction(self, model, include=None):
return self.request('POST', 'transactions/createoradjust', params=include, json={'createTransactionModel': model})
def uncommit_transaction(self, companyCode, transactionCode, include=None):
return self.request('POST', 'companies/{}/transactions/{}/uncommit'.format(companyCode, transactionCode),
params=include, json=None)
def void_transaction(self, companyCode, transactionCode, model, include=None):
return self.request('POST', 'companies/{}/transactions/{}/void'.format(companyCode, transactionCode),
params=include, json=model)
def ping(self):
return self.request('GET', 'utilities/ping', params=None, json=None)
def resolve_address(self, model=None):
return self.request('POST', 'addresses/resolve', params=None, json=model)
def list_entity_use_codes(self, include=None):
return self.request('GET', 'definitions/entityusecodes', params=include, json=None)

View File

@ -0,0 +1,9 @@
from . import account_avatax_unique_code
from . import account_chart_template
from . import product
from . import avatax_exemption
from . import res_partner
from . import res_company
from . import account_move
from . import account_fiscal_position
from . import account_external_tax_mixin

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
import logging
from odoo import models, fields, _
from odoo.exceptions import UserError
from odoo.osv import expression
logger = logging.getLogger(__name__)
class AccountAvataxUniqueCode(models.AbstractModel):
_name = 'account.avatax.unique.code'
_description = 'Mixin to generate unique ids for Avatax'
avatax_unique_code = fields.Char(
"Avalara Code",
compute="_compute_avatax_unique_code",
search="_search_avatax_unique_code",
store=False,
help="Use this code to cross-reference in the Avalara portal."
)
def _get_avatax_description(self):
raise NotImplementedError()
def _compute_avatax_unique_code(self):
for record in self:
record.avatax_unique_code = '%s %s' % (record._get_avatax_description(), record.id)
def _search_avatax_unique_code(self, operator, value):
unsupported_operators = ('in', 'not in', '<', '<=', '>', '>=')
if operator in unsupported_operators or not isinstance(value, str):
raise UserError(_("Search operation not supported"))
value = value.lower()
prefix = self._get_avatax_description().lower() + " "
if value.startswith(prefix):
value = value[len(prefix):]
if operator in ('=', '!=') and not value.isdigit():
return expression.FALSE_DOMAIN
if not value:
return expression.FALSE_DOMAIN
return [('id', operator, value)]

View File

@ -0,0 +1,17 @@
from odoo import models
from odoo.addons.account.models.chart_template import template
class AccountChartTemplate(models.AbstractModel):
_inherit = 'account.chart.template'
@template('generic_coa', 'account.fiscal.position')
def _get_us_avatax_fiscal_position(self):
return {
'account_fiscal_position_avatax_us': {
'name': 'Automatic Tax Mapping (AvaTax)',
'is_avatax': True,
'auto_apply': False,
'country_id': self.env.ref('base.us').id,
},
}

View File

@ -0,0 +1,357 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from collections import defaultdict
from pprint import pformat
from odoo import models, api, fields, _
from odoo.addons.odex30_account_avatax.lib.avatax_client import AvataxClient
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.release import version
from odoo.tools import float_round, format_list
_logger = logging.getLogger(__name__)
class AccountExternalTaxMixin(models.AbstractModel):
_inherit = 'account.external.tax.mixin'
is_avatax = fields.Boolean(compute='_compute_is_avatax')
@api.depends('fiscal_position_id')
def _compute_is_avatax(self):
for record in self:
record.is_avatax = record.fiscal_position_id.is_avatax
def _compute_is_tax_computed_externally(self):
super()._compute_is_tax_computed_externally()
self.filtered('is_avatax').is_tax_computed_externally = True
def _get_external_taxes(self):
""" Override. """
def find_or_create_tax(doc, detail):
def repartition_line(repartition_type, account=None):
return (0, 0, {
'repartition_type': repartition_type,
'tag_ids': [],
'company_id': doc.company_id.id,
'account_id': account and account.id,
})
fixed = detail.get('unitOfBasis') == 'FlatAmount'
rate = detail['rate'] if fixed else detail['rate'] * 100
name_precision = 4
rounded_rate = float_round(rate, name_precision)
tax_group_name = detail['taxName'].removesuffix(' TAX')
tax_name = '%s %s' % (
tax_group_name,
("$ %.4g" if fixed else "%.4g%%") % rounded_rate,
)
group_key = (tax_group_name, doc.company_id)
if group_key not in tax_group_cache:
tax_group_cache[group_key] = self.env['account.tax.group'].search([
*self.env['account.tax.group']._check_company_domain(doc.company_id),
('name', '=', tax_group_name),
], limit=1) or self.env['account.tax.group'].sudo().with_company(doc.company_id).create({
'name': tax_group_name,
})
key = (tax_name, doc.company_id)
if key not in tax_cache:
tax_cache[key] = self.env['account.tax'].search([
*self.env['account.tax']._check_company_domain(doc.company_id),
('name', '=', tax_name),
], limit=1) or self.env['account.tax'].sudo().with_company(self._find_avatax_credentials_company(doc.company_id)).create({
'name': tax_name,
'tax_group_id': tax_group_cache[group_key].id,
'amount': rate,
'amount_type': 'fixed' if fixed else 'percent',
'refund_repartition_line_ids': [
repartition_line('base'),
repartition_line('tax', doc.fiscal_position_id.avatax_refund_account_id),
],
'invoice_repartition_line_ids': [
repartition_line('base'),
repartition_line('tax', doc.fiscal_position_id.avatax_invoice_account_id),
],
})
return tax_cache[key]
details, summary = super()._get_external_taxes()
tax_cache = {}
tax_group_cache = {}
query_results = self.filtered('is_avatax')._query_avatax_taxes()
errors = []
for document, query_result in query_results.items():
error = self._handle_response(query_result, _(
'EXP could not fetch the taxes related to %(document)s.\n'
'Please check the status of `%(technical)s` in the AvaTax portal.',
document=document.display_name,
technical=document.avatax_unique_code,
))
if error:
errors.append(error)
if errors:
raise UserError('\n\n'.join(errors))
for document, query_result in query_results.items():
is_return = document._get_avatax_document_type() == 'ReturnInvoice'
line_amounts_sign = -1 if is_return else 1
for line_result in query_result['lines']:
record_id = line_result['lineNumber'].split(',')
record = self.env[record_id[0]].browse(int(record_id[1]))
details.setdefault(record, {})
details[record]['total'] = line_amounts_sign * line_result['lineAmount']
details[record]['tax_amount'] = line_amounts_sign * line_result['tax']
for detail in line_result['details']:
tax = find_or_create_tax(document, detail)
details[record].setdefault('tax_ids', self.env['account.tax'])
details[record]['tax_ids'] += tax
summary[document] = defaultdict(float)
for summary_line in query_result['summary']:
tax = find_or_create_tax(document, summary_line)
summary[document][tax] += -summary_line['tax']
return details, summary
@api.constrains('partner_id', 'fiscal_position_id')
def _check_address(self):
incomplete_partner_to_records = self._get_partners_with_incomplete_information()
if incomplete_partner_to_records:
error = _("The following customer(s) need to have a zip, state and country when using Avatax:")
partner_errors = [
_(
"- %(partner_name)s (ID: %(partner_id)s) on %(record_list)s",
partner_name=partner.display_name,
partner_id=partner.id,
record_list=format_list(self.env, [record.display_name for record in records]),
)
for partner, records in incomplete_partner_to_records.items()
]
raise ValidationError(error + "\n" + "\n".join(partner_errors))
def _get_partners_with_incomplete_information(self, partner=None):
incomplete_partner_to_records = {}
for record in self.filtered(lambda r: r._perform_address_validation()):
partner = partner or record.partner_id
country = partner.country_id
if (
partner and partner != self.env.ref('base.public_partner')
and (
not country
or (country.zip_required and not partner.zip)
or (country.state_required and not partner.state_id)
)
):
incomplete_partner_to_records.setdefault(partner, []).append(record)
return incomplete_partner_to_records
def _get_avatax_dates(self):
raise NotImplementedError()
def _get_avatax_document_type(self):
raise NotImplementedError()
def _get_avatax_ship_to_partner(self):
return self.partner_shipping_id or self.partner_id
def _perform_address_validation(self):
return self.fiscal_position_id.is_avatax
def _get_avatax_invoice_line(self, line_data):
product = line_data['product_id']
if not product._get_avatax_category_id():
raise UserError(_(
'The Avalara Tax Code is required for %(name)s (#%(id)s)\n'
'See https://taxcode.avatax.avalara.com/',
name=product.display_name,
id=product.id,
))
item_code = product.code or ""
if self.env.company.avalara_use_upc and product.barcode:
item_code = f'UPC:{product.barcode}'
return {
'amount': -line_data["price_subtotal"] if line_data["is_refund"] else line_data["price_subtotal"],
'description': product.display_name,
'quantity': abs(line_data["qty"]),
'taxCode': product._get_avatax_category_id().code,
'itemCode': item_code,
'number': "%s,%s" % (line_data["model_name"], line_data["id"]),
}
@api.model
def _find_avatax_credentials_company(self, company):
has_avatax_credentials = bool(company.sudo().avalara_api_id and company.sudo().avalara_api_key)
if has_avatax_credentials:
return company
elif company.parent_id:
return self._find_avatax_credentials_company(company.parent_id)
def _get_avatax_ref(self):
return self.name or ''
def _get_avatax_address_from_partner(self, partner):
incomplete_partner = self._get_partners_with_incomplete_information(partner)
if incomplete_partner.get(partner):
res = {
'latitude': partner.partner_latitude,
'longitude': partner.partner_longitude,
}
else:
res = {
'city': partner.city,
'country': partner.country_id.code,
'region': partner.state_id.code,
'postalCode': partner.zip,
'line1': partner.street,
}
return res
def _get_avatax_addresses(self, partner):
res = {
'shipFrom': self._get_avatax_address_from_partner(self.company_id.partner_id),
'shipTo': self._get_avatax_address_from_partner(partner),
}
return res
def _get_avatax_invoice_lines(self):
return [self._get_avatax_invoice_line(line_data) for line_data in self._get_line_data_for_external_taxes()]
def _get_avatax_taxes(self, commit):
self.ensure_one()
partner = self.partner_id.commercial_partner_id
document_date, tax_date = self._get_avatax_dates()
taxes = {
'addresses': self._get_avatax_addresses(self._get_avatax_ship_to_partner()),
'companyCode': self.company_id.partner_id.avalara_partner_code or '',
'customerCode': partner.avalara_partner_code or partner.avatax_unique_code,
'entityUseCode': partner.with_company(self.company_id).avalara_exemption_id.code or '',
'businessIdentificationNo': partner.vat or '',
'date': (document_date or fields.Date.today()).isoformat(),
'lines': self._get_avatax_invoice_lines(),
'type': self._get_avatax_document_type(),
'code': self.avatax_unique_code,
'referenceCode': self._get_avatax_ref(),
'currencyCode': self.currency_id.name or '',
'commit': commit and self.company_id.avalara_commit,
}
if tax_date:
taxes['taxOverride'] = {
'type': 'taxDate',
'reason': 'Manually changed the tax calculation date',
'taxDate': tax_date.isoformat(),
}
return taxes
def _commit_avatax_taxes(self):
self._query_avatax_taxes(commit=True)
def _query_avatax_taxes(self, commit=False):
if not self:
return {}
client = self._get_client(self.company_id)
transactions = {record: record._get_avatax_taxes(commit) for record in self}
return {
record: client.create_transaction(transaction, include='Lines')
for record, transaction in transactions.items()
}
def _uncommit_external_taxes(self):
for record in self.filtered('is_avatax'):
if not record.company_id.avalara_commit:
continue
client = self._get_client(record.company_id)
query_result = client.uncommit_transaction(
companyCode=record.company_id.partner_id.avalara_partner_code,
transactionCode=record.avatax_unique_code,
)
error = self._handle_response(query_result, _(
'EXP could not change the state of the transaction related to %(document)s in'
' AvaTax\nPlease check the status of `%(technical)s` in the AvaTax portal.',
document=record.display_name,
technical=record.avatax_unique_code,
))
if error:
raise UserError(error)
return super()._uncommit_external_taxes()
def _void_external_taxes(self):
for record in self.filtered('is_avatax'):
if not record.company_id.avalara_commit:
continue
client = self._get_client(record.company_id)
query_result = client.void_transaction(
companyCode=record.company_id.partner_id.avalara_partner_code,
transactionCode=record.avatax_unique_code,
model={"code": "DocVoided"},
)
if query_result.get('error', {}).get('code') == 'EntityNotFoundError':
_logger.info(pformat(query_result))
continue
error = self._handle_response(query_result, _(
'EXP could not void the transaction related to %(document)s in AvaTax\nPlease '
'check the status of `%(technical)s` in the AvaTax portal.',
document=record.display_name,
technical=record.avatax_unique_code,
))
if error:
raise UserError(error)
return super()._void_external_taxes()
def _handle_response(self, response, title):
if response.get('errors'):
_logger.warning(pformat(response), stack_info=True)
return '%s\n%s' % (title, response.get('title', ''))
if response.get('error'):
_logger.warning(pformat(response), stack_info=True)
messages = '\n'.join(detail['message'] for detail in response['error']['details'])
return '%s\n%s' % (title, messages)
def _get_client(self, company):
company = self._find_avatax_credentials_company(company)
if not company:
raise RedirectWarning(
_('Please add your AvaTax credentials'),
self.env.ref('base_setup.action_general_configuration').id,
_("Go to the configuration panel"),
)
client = AvataxClient(
app_name='Odoo',
app_version=version,
environment=company.avalara_environment,
)
client.add_credentials(
company.sudo().avalara_api_id or '',
company.sudo().avalara_api_key or '',
)
client.logger = lambda message: self._log_external_tax_request(
'Avatax US', 'odex30_account_avatax.log.end.date', message
)
return client

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class AccountFiscalPosition(models.Model):
_inherit = 'account.fiscal.position'
def _default_avatax_invoice_account_id(self):
return self.env.company.account_sale_tax_id.invoice_repartition_line_ids.account_id
def _default_avatax_refund_account_id(self):
return self.env.company.account_sale_tax_id.refund_repartition_line_ids.account_id
is_avatax = fields.Boolean(string="Use AvaTax API")
avatax_invoice_account_id = fields.Many2one(
comodel_name='account.account',
default=_default_avatax_invoice_account_id,
help="Account that will be used by Avatax taxes for invoices.",
)
avatax_refund_account_id = fields.Many2one(
comodel_name='account.account',
default=_default_avatax_refund_account_id,
help="Account that will be used by Avatax taxes for refunds.",
)

View File

@ -0,0 +1,43 @@
from odoo import models, fields
class AccountMove(models.Model):
_name = 'account.move'
_inherit = ['account.avatax.unique.code', 'account.move']
avatax_tax_date = fields.Date(
string="Avatax Date",
help="Avatax will use this date to calculate the tax on this invoice. "
"If not specified it will use the Invoice Date.",
)
def _post(self, soft=True):
res = super()._post(soft=soft)
self.filtered(
lambda move: move.is_avatax and move.move_type in ('out_invoice', 'out_refund') and not move._is_downpayment()
)._commit_avatax_taxes()
return res
def _get_avatax_dates(self):
external_tax_date = self._get_date_for_external_taxes()
if self.reversed_entry_id:
reversed_override_date = self.reversed_entry_id.avatax_tax_date or self.reversed_entry_id._get_date_for_external_taxes()
return external_tax_date, reversed_override_date
return external_tax_date, self.avatax_tax_date
def _get_avatax_document_type(self):
return {
'out_invoice': 'SalesInvoice',
'out_refund': 'ReturnInvoice',
'in_invoice': 'PurchaseInvoice',
'in_refund': 'ReturnInvoice',
'entry': 'Any',
}[self.move_type]
def _get_avatax_description(self):
return 'Journal Entry'
def _perform_address_validation(self):
moves = self.filtered(lambda m: m.move_type in ('out_invoice', 'out_refund'))
return super(AccountMove, moves)._perform_address_validation() and not moves.origin_payment_id

View File

@ -0,0 +1,18 @@
from odoo import api, models, fields
class AvataxExemption(models.Model):
_name = 'avatax.exemption'
_description = "Avatax Partner Exemption Codes"
_rec_names_search = ['name', 'code']
name = fields.Char(required=True)
code = fields.Char(required=True)
description = fields.Char()
valid_country_ids = fields.Many2many('res.country')
company_id = fields.Many2one('res.company', required=True)
@api.depends('code')
def _compute_display_name(self):
for record in self:
record.display_name = f'[{record.code}] {record.name}'

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class ProductAvataxCategory(models.Model):
_name = 'product.avatax.category'
_description = "Avatax Product Category"
_rec_name = 'code'
_rec_names_search = ['description', 'code']
code = fields.Char(required=True)
description = fields.Char(required=True)
@api.depends('code', 'description')
def _compute_display_name(self):
for category in self:
category.display_name = _('[%(code)s] %(description)s', code=category.code, description=(category.description or '')[:50])
class ProductCategory(models.Model):
_inherit = 'product.category'
avatax_category_id = fields.Many2one(
'product.avatax.category',
help="https://taxcode.avatax.avalara.com/",
)
def _get_avatax_category_id(self):
categ = self
while categ and not categ.avatax_category_id:
categ = categ.parent_id
return categ.avatax_category_id
class ProductTemplate(models.Model):
_inherit = 'product.template'
avatax_category_id = fields.Many2one(
'product.avatax.category',
help="https://taxcode.avatax.avalara.com/",
)
def _get_avatax_category_id(self):
return self.avatax_category_id or self.categ_id._get_avatax_category_id()
class ProductProduct(models.Model):
_inherit = 'product.product'
avatax_category_id = fields.Many2one(
'product.avatax.category',
help="https://taxcode.avatax.avalara.com/",
)
def _get_avatax_category_id(self):
return self.avatax_category_id or self.product_tmpl_id._get_avatax_category_id()

View File

@ -0,0 +1,146 @@
import json
import logging
from datetime import timedelta
from odoo import fields, models, _
from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
class ResCompany(models.Model):
_inherit = 'res.company'
avalara_api_id = fields.Char(string='Avalara API ID', groups='base.group_system')
avalara_api_key = fields.Char(string='Avalara API KEY', groups='base.group_system')
avalara_environment = fields.Selection(
string="Avalara Environment",
selection=[
('sandbox', 'Sandbox'),
('production', 'Production'),
],
required=True,
default='sandbox',
)
avalara_commit = fields.Boolean(string="Commit in Avatax")
avalara_address_validation = fields.Boolean(string="Avalara Address Validation")
avalara_use_upc = fields.Boolean(string="Use UPC", default=True)
setting_account_avatax = fields.Boolean(string='Use AvaTax', store=True)
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
avalara_api_id = fields.Char(
related='company_id.avalara_api_id',
readonly=False,
string='Avalara API ID',
)
avalara_api_key = fields.Char(
related='company_id.avalara_api_key',
readonly=False,
string='Avalara API KEY',
)
avalara_partner_code = fields.Char(
related='company_id.partner_id.avalara_partner_code',
readonly=False,
string='Avalara Company Code',
help="The Avalara Company Code for this company. Avalara will interpret as DEFAULT if it"
" is not set.",
)
avalara_environment = fields.Selection(
related='company_id.avalara_environment',
readonly=False,
string="Avalara Environment",
required=True,
)
avalara_commit = fields.Boolean(
related='company_id.avalara_commit',
readonly=False,
string='Commit in Avatax',
help="The transactions will be committed for reporting in Avatax.",
)
avalara_address_validation = fields.Boolean(
related='company_id.avalara_address_validation',
string='Avalara Address Validation',
readonly=False,
help="Validate and correct the addresses of partners in North America with Avalara.",
)
avalara_use_upc = fields.Boolean(
related='company_id.avalara_use_upc',
readonly=False,
string="Use UPC",
help="Use Universal Product Code instead of custom defined codes in Avalara.",
)
setting_account_avatax = fields.Boolean(
related='company_id.setting_account_avatax',
readonly=False,
)
def avatax_sync_company_params(self):
def get_countries(code_list):
uncached = set(code_list) - set(country_cache)
if uncached:
country_cache.update({
country.code: country.id
for country in self.env['res.country'].search([('code', 'in', tuple(uncached))])
})
return self.env['res.country'].browse([country_cache[code] for code in code_list])
country_cache = {'*': False}
existing = {
exempt['code'] for exempt in self.env['avatax.exemption'].search_read(
domain=[('company_id', '=', self.company_id.id)],
fields=['code'],
)
}
client = self.env['account.external.tax.mixin']._get_client(self.company_id)
response = client.list_entity_use_codes()
error = self.env['account.external.tax.mixin']._handle_response(response, _(
"Odoo could not fetch the exemption codes of %(company)s",
company=self.company_id.display_name,
))
if error:
raise UserError(error)
self.env['avatax.exemption'].create([
{
'code': vals['code'],
'description': vals['description'],
'name': vals['name'],
'valid_country_ids': [(6, 0, get_countries(vals['validCountries']).ids)],
'company_id': self.company_id.id,
}
for vals in response['value']
if vals['code'] not in existing
])
return True
def avatax_ping(self):
client = self.env['account.external.tax.mixin']._get_client(self.company_id)
query_result = client.ping()
html_content = self._format_response(query_result)
return {
'name': _('Test Result'),
'type': 'ir.actions.act_window',
'res_model': 'avatax.connection.test.result',
'res_id': self.env['avatax.connection.test.result'].create({'server_response': html_content}).id,
'target': 'new',
'views': [(False, 'form')],
}
def _format_response(self, query_result):
html_content = _("Authentication success.") if query_result['authenticated'] else _("Authentication failed.")
html_content += '<ul>'
for key, value in query_result.items():
html_content += f'<li><span class="fw-bold">{key.capitalize()}:</span> {value}</li>'
html_content += '</ul>'
return html_content
def avatax_log(self):
self.env['account.external.tax.mixin']._enable_external_tax_logging('odex30_account_avatax.log.end.date')
return True

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api, _
import logging
_logger = logging.getLogger(__name__)
ADDRESS_FIELDS = ('street', 'street2', 'city', 'state_id', 'zip', 'country_id')
class ResPartner(models.Model):
_name = 'res.partner'
_inherit = ['res.partner', 'account.avatax.unique.code']
avalara_partner_code = fields.Char(
string='Avalara Partner Code',
help="Customer Code set in Avalara for this partner.",
)
avalara_exemption_id = fields.Many2one(
comodel_name='avatax.exemption',
company_dependent=True,
domain="['|', ('valid_country_ids', 'in', country_id), ('valid_country_ids', '=', False)]",
)
avalara_show_address_validation = fields.Boolean(
compute='_compute_avalara_show_address_validation',
store=False,
string='Avalara Show Address Validation',
)
@api.depends('country_id')
def _compute_avalara_show_address_validation(self):
for partner in self:
company = partner.company_id or self.env.company
partner.avalara_show_address_validation = company.avalara_address_validation and partner.street and (not partner.country_id or partner.fiscal_country_codes in ('US', 'CA'))
def _get_avatax_description(self):
return 'Contact'
def action_open_validation_wizard(self):
self.ensure_one()
return {
'name': _('Validate address of %s', self.display_name),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'avatax.validate.address',
'target': 'new',
'context': {'default_partner_id': self.id},
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_invoice_document" inherit_id="account.report_invoice_document">
<!-- TODO not the best thing to inherit... -->
<xpath expr="//span[@id='line_tax_ids']/.." position="attributes">
<attribute name="t-if">not o.is_avatax</attribute>
</xpath>
<xpath expr="//th[@name='th_taxes']" position="attributes">
<attribute name="t-if">not o.is_avatax</attribute>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_avatax_category,access_product_avatax_category,model_product_avatax_category,base.group_user,1,0,0,0
access_avatax_exemption,access_avatax_exemption,model_avatax_exemption,base.group_user,1,0,0,0
access_avatax_exemption_admin,access_avatax_exemption_admin,model_avatax_exemption,base.group_system,1,1,1,1
access_avatax_validate_address,access_avatax_validate_address,model_avatax_validate_address,base.group_user,1,1,1,1
access_avatax_connection_test_result,access_avatax_connection_test_result,model_avatax_connection_test_result,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_avatax_category access_product_avatax_category model_product_avatax_category base.group_user 1 0 0 0
3 access_avatax_exemption access_avatax_exemption model_avatax_exemption base.group_user 1 0 0 0
4 access_avatax_exemption_admin access_avatax_exemption_admin model_avatax_exemption base.group_system 1 1 1 1
5 access_avatax_validate_address access_avatax_validate_address model_avatax_validate_address base.group_user 1 1 1 1
6 access_avatax_connection_test_result access_avatax_connection_test_result model_avatax_connection_test_result base.group_user 1 1 1 1

View File

@ -0,0 +1,11 @@
from . import test_avatax
from . import (
test_address_validation,
test_refunds,
test_use_tax,
test_vat,
test_avatax_unique_code,
)

View File

@ -0,0 +1,271 @@
import os
from contextlib import contextmanager, ExitStack
from unittest import SkipTest
from unittest.mock import patch
from odoo import Command
from odoo.addons.account_avatax.lib.avatax_client import AvataxClient
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests.common import TransactionCase
from .mocked_invoice_1_response import generate_response as generate_response_invoice_1
from .mocked_invoice_2_response import generate_response as generate_response_invoice_2
from .mocked_invoice_3_response import generate_response as generate_response_invoice_3
NOTHING = object()
class TestAvataxCommon(TransactionCase):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.env.company.avalara_api_id = os.getenv("AVALARA_LOGIN_ID") or "AVALARA_LOGIN_ID"
cls.env.company.avalara_api_key = os.getenv("AVALARA_API_KEY") or "AVALARA_API_KEY"
cls.env.company.avalara_environment = 'sandbox'
cls.env.company.avalara_commit = True
company = cls.env.user.company_id
company.write({
'street': "250 Executive Park Blvd",
'city': "San Francisco",
'state_id': cls.env.ref("base.state_us_5").id,
'country_id': cls.env.ref("base.us").id,
'zip': "94134",
})
company.partner_id.avalara_partner_code = os.getenv("AVALARA_COMPANY_CODE") or "DEFAULT"
cls.fp_avatax = cls.env['account.fiscal.position'].create({
'name': 'Avatax',
'is_avatax': True,
})
cls.partner = cls.env["res.partner"].create({
'name': "Sale Partner",
'street': "2280 Market St",
'city': "San Francisco",
'state_id': cls.env.ref("base.state_us_5").id,
'country_id': cls.env.ref("base.us").id,
'zip': "94114",
'avalara_partner_code': 'CUST123456',
'property_account_position_id': cls.fp_avatax.id,
})
return res
@classmethod
@contextmanager
def _client_patched(cls, create_transaction_details=None, **kwargs):
if kwargs.get('create_transaction') is None and create_transaction_details is not None:
def create_transaction(self, transaction, include=None):
return {
'lines': [{
'lineNumber': line['number'],
'details': create_transaction_details,
} for line in transaction['lines']],
'summary': create_transaction_details,
}
if kwargs.get('uncommit_transaction') is None:
def uncommit_transaction(self, companyCode, transactionCode, include=None):
return {}
def request(self, method, *args, **kwargs):
assert False, "Request not authorized in mock"
fnames = {fname for fname in dir(AvataxClient) if not fname.startswith('_')} - {
'add_credentials',
}
methods = {**{fname: None for fname in fnames}, **kwargs, **locals()}
with ExitStack() as stack:
for _patch in [
patch(f'{AvataxClient.__module__}.AvataxClient.{fname}', methods[fname])
for fname in fnames
]:
stack.enter_context(_patch)
yield
@classmethod
@contextmanager
def _capture_request(cls, return_value=NOTHING, return_func=NOTHING):
class Capture:
val = None
def capture_request(self, method, *args, **kwargs):
self.val = kwargs
if return_value is NOTHING:
return return_func(method, *args, **kwargs)
return return_value
capture = Capture()
with patch(f'{AvataxClient.__module__}.AvataxClient.request', capture.capture_request):
yield capture
@classmethod
@contextmanager
def _skip_no_credentials(cls):
if not os.getenv("AVALARA_LOGIN_ID") or not os.getenv("AVALARA_API_KEY") or not os.getenv("AVALARA_COMPANY_CODE"):
raise SkipTest("no Avalara credentials")
yield
class TestAccountAvataxCommon(TestAvataxCommon, AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.product = cls.env["product.product"].create({
'name': "Product",
'default_code': 'PROD1',
'barcode': '123456789',
'list_price': 15.00,
'standard_price': 15.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.product_user = cls.env["product.product"].create({
'name': "Odoo User",
'list_price': 35.00,
'standard_price': 35.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.product_user_discound = cls.env["product.product"].create({
'name': "Odoo User Initial Discount",
'list_price': -5.00,
'standard_price': -5.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.product_accounting = cls.env["product.product"].create({
'name': "Accounting",
'list_price': 30.00,
'standard_price': 30.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.product_expenses = cls.env["product.product"].create({
'name': "Expenses",
'list_price': 15.00,
'standard_price': 15.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.product_invoicing = cls.env["product.product"].create({
'name': "Invoicing",
'list_price': 15.00,
'standard_price': 15.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
})
cls.example_tax = cls.env["account.tax"].create({
'name': 'CA STATE 6%',
'company_id': cls.env.user.company_id.id,
'amount': 1,
'amount_type': 'percent',
})
return res
@classmethod
def _create_invoice(cls, post=True, **kwargs):
invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_date': '2020-01-01',
'invoice_line_ids': [
(0, 0, {'product_id': cls.product.id, 'price_unit': 100}),
],
**kwargs,
})
if post:
invoice.action_post()
return invoice
@classmethod
def _create_invoice_01_and_expected_response(cls):
invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
(0, 0, {
'product_id': cls.product_user.id,
'tax_ids': None,
'price_unit': cls.product_user.list_price,
}),
(0, 0, {
'product_id': cls.product_user_discound.id,
'tax_ids': None,
'price_unit': cls.product_user_discound.list_price,
}),
(0, 0, {
'product_id': cls.product_accounting.id,
'tax_ids': None,
'price_unit': cls.product_accounting.list_price,
}),
(0, 0, {
'product_id': cls.product_expenses.id,
'tax_ids': None,
'price_unit': cls.product_expenses.list_price,
}),
(0, 0, {
'product_id': cls.product_invoicing.id,
'tax_ids': None,
'price_unit': cls.product_invoicing.list_price,
}),
]
})
response = generate_response_invoice_1(invoice.invoice_line_ids)
return invoice, response
@classmethod
def _create_invoice_02_and_expected_response(cls):
invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_line_ids': [
(0, 0, {
'product_id': cls.product_user.id,
'tax_ids': None,
'price_unit': cls.product_user.list_price,
'discount': 1 / 7 * 100,
}),
(0, 0, {
'product_id': cls.product_accounting.id,
'tax_ids': None,
'price_unit': cls.product_accounting.list_price,
}),
(0, 0, {
'product_id': cls.product_expenses.id,
'tax_ids': None,
'price_unit': cls.product_expenses.list_price,
}),
(0, 0, {
'product_id': cls.product_invoicing.id,
'tax_ids': None,
'price_unit': cls.product_invoicing.list_price,
}),
]
})
response = generate_response_invoice_2(invoice.invoice_line_ids)
return invoice, response
@classmethod
def _create_invoice_03_and_expected_response(cls):
invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_line_ids': [
Command.create({
'product_id': cls.product_accounting.id,
'tax_ids': None,
'price_unit': cls.product_accounting.list_price,
}),
]
})
response = generate_response_invoice_3(invoice.invoice_line_ids)
return invoice, response

View File

@ -0,0 +1,44 @@
response = {'address': {'city': '',
'country': 'US',
'line1': '250 executiv prk blvd',
'line2': '3400',
'postalCode': '94134',
'region': '',
'textCase': 'Mixed'},
'coordinates': {'latitude': 37.71116, 'longitude': -122.391717},
'resolutionQuality': 'Intersection',
'taxAuthorities': [{'avalaraId': '275',
'jurisdictionName': 'SAN FRANCISCO',
'jurisdictionType': 'County',
'signatureCode': 'AIUQ'},
{'avalaraId': '5000531',
'jurisdictionName': 'CALIFORNIA',
'jurisdictionType': 'State',
'signatureCode': 'AGAM'},
{'avalaraId': '2001061430',
'jurisdictionName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisdictionType': 'Special',
'signatureCode': 'EMBE'},
{'avalaraId': '2001061792',
'jurisdictionName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisdictionType': 'Special',
'signatureCode': 'EMTV'},
{'avalaraId': '2001067344',
'jurisdictionName': 'MOSCONE EXPANSION DISTRICT ZONE 2',
'jurisdictionType': 'Special',
'signatureCode': 'MHZT'},
{'avalaraId': '2001077295',
'jurisdictionName': 'SAN FRANCISCO TOURISM IMPROVEMENT '
'DISTRICT (ZONE 2)',
'jurisdictionType': 'Special',
'signatureCode': 'NQPB'}],
'validatedAddresses': [{'addressType': 'HighRiseOrBusinessComplex',
'city': 'San Francisco',
'country': 'US',
'latitude': 37.71116,
'line1': '250 Executive Park Blvd Ste 3400',
'line2': '',
'line3': '',
'longitude': -122.391717,
'postalCode': '94134-3349',
'region': 'CA'}]}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,934 @@
def generate_response(invoice_line_ids):
assert len(invoice_line_ids) == 4, "the mocked response is for 4 lines"
for i, line in enumerate(response['lines']):
line['lineNumber'] = 'account.move.line,%s' % invoice_line_ids[i].id
return response
response = {'addresses': [{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 12063164280,
'latitude': '37.764754',
'line1': '2280 Market St',
'line2': '',
'line3': '',
'longitude': '-122.432634',
'postalCode': '94114-1506',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 7119936253},
{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 5000306545659,
'latitude': '37.71116',
'line1': '250 Executive Park Blvd',
'line2': '',
'line3': '',
'longitude': '-122.391717',
'postalCode': '94134-3394',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 7119936253}],
'adjustmentDescription': '',
'adjustmentReason': 'NotAdjusted',
'batchCode': '',
'businessIdentificationNo': '',
'code': 'Journal Entry 164',
'companyId': 281741,
'country': 'US',
'currencyCode': 'USD',
'customerCode': 'CUST123456',
'customerUsageType': '',
'customerVendorCode': 'CUST123456',
'date': '2021-01-01',
'description': '',
'destinationAddressId': 12063164280,
'email': '',
'entityUseCode': '',
'exchangeRate': 1.0,
'exchangeRateCurrencyCode': 'USD',
'exchangeRateEffectiveDate': '2021-01-01',
'exemptNo': '',
'id': 7119936253,
'lines': [{'boundaryOverrideId': 0,
'businessIdentificationNo': '',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Odoo User',
'destinationAddressId': 12063164280,
'details': [{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 11000404539513,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionId': 5000531,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1525706,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 1.8,
'reportingTaxCalculated': 1.8,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'AGAM',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 1.8,
'taxAuthorityTypeId': 45,
'taxCalculated': 1.8,
'taxName': 'CA STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 12614586591,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 12000404539516,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionId': 275,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0025,
'rateRuleId': 1525710,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.08,
'reportingTaxCalculated': 0.08,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'AIUQ',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.08,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.08,
'taxName': 'CA COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 12614586591,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 1000517141040,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionId': 2001061792,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1525730,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'EMTV',
'sourcing': 'Origin',
'stateAssignedNo': '38',
'stateFIPS': '',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 12614586591,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 12614586591,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': 'false',
'lineAmount': 30.0,
'lineLocationTypes': [{'documentAddressId': 5000306545659,
'documentLineId': 12614586591,
'documentLineLocationTypeId': 9000387947821,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 12063164280,
'documentLineId': 12614586591,
'documentLineLocationTypeId': 10000387947820,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,440',
'nonPassthroughDetails': [],
'originAddressId': 5000306545659,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'revAccount': '',
'sourcing': 'Origin',
'tax': 2.18,
'taxCalculated': 2.18,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': 30.0,
'transactionId': 7119936253,
'vatCode': '',
'vatNumberTypeId': 0},
{'boundaryOverrideId': 0,
'businessIdentificationNo': '',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Accounting',
'destinationAddressId': 12063164280,
'details': [{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 2000517141038,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionId': 5000531,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1525706,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 1.8,
'reportingTaxCalculated': 1.8,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'AGAM',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 1.8,
'taxAuthorityTypeId': 45,
'taxCalculated': 1.8,
'taxName': 'CA STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 5000312772562,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 3000517141037,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionId': 275,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0025,
'rateRuleId': 1525710,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.08,
'reportingTaxCalculated': 0.08,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'AIUQ',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.08,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.08,
'taxName': 'CA COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 5000312772562,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 4000517141035,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionId': 2001061792,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1525730,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'EMTV',
'sourcing': 'Origin',
'stateAssignedNo': '38',
'stateFIPS': '',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 7119936253,
'transactionLineId': 5000312772562,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 5000312772562,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': 'false',
'lineAmount': 30.0,
'lineLocationTypes': [{'documentAddressId': 5000306545659,
'documentLineId': 5000312772562,
'documentLineLocationTypeId': 11000387947818,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 12063164280,
'documentLineId': 5000312772562,
'documentLineLocationTypeId': 12000387947817,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,441',
'nonPassthroughDetails': [],
'originAddressId': 5000306545659,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'revAccount': '',
'sourcing': 'Origin',
'tax': 2.18,
'taxCalculated': 2.18,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': 30.0,
'transactionId': 7119936253,
'vatCode': '',
'vatNumberTypeId': 0},
{'boundaryOverrideId': 0,
'businessIdentificationNo': '',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Expenses',
'destinationAddressId': 12063164280,
'details': [{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 5000517141033,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionId': 5000531,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1525706,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.9,
'reportingTaxCalculated': 0.9,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'AGAM',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.9,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.9,
'taxName': 'CA STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6000312772560,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 6000517141032,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionId': 275,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0025,
'rateRuleId': 1525710,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.04,
'reportingTaxCalculated': 0.04,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'AIUQ',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.04,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.04,
'taxName': 'CA COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6000312772560,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 7000404539513,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionId': 2001061792,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1525730,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.15,
'reportingTaxCalculated': 0.15,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'EMTV',
'sourcing': 'Origin',
'stateAssignedNo': '38',
'stateFIPS': '',
'tax': 0.15,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.15,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6000312772560,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 6000312772560,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': 'false',
'lineAmount': 15.0,
'lineLocationTypes': [{'documentAddressId': 5000306545659,
'documentLineId': 6000312772560,
'documentLineLocationTypeId': 7338538069,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 12063164280,
'documentLineId': 6000312772560,
'documentLineLocationTypeId': 9338538066,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,442',
'nonPassthroughDetails': [],
'originAddressId': 5000306545659,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'revAccount': '',
'sourcing': 'Origin',
'tax': 1.09,
'taxCalculated': 1.09,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': 15.0,
'transactionId': 7119936253,
'vatCode': '',
'vatNumberTypeId': 0},
{'boundaryOverrideId': 0,
'businessIdentificationNo': '',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Invoicing',
'destinationAddressId': 12063164280,
'details': [{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 8000404539512,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionId': 5000531,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1525706,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.9,
'reportingTaxCalculated': 0.9,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'AGAM',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.9,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.9,
'taxName': 'CA STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6406250349,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 9000404539514,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionId': 275,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0025,
'rateRuleId': 1525710,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.04,
'reportingTaxCalculated': 0.04,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'AIUQ',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': 0.04,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.04,
'taxName': 'CA COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6406250349,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 5000306545659,
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 10000404539516,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionId': 2001061792,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1525730,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.15,
'reportingTaxCalculated': 0.15,
'reportingTaxableUnits': 15.0,
'serCode': '',
'signatureCode': 'EMTV',
'sourcing': 'Origin',
'stateAssignedNo': '38',
'stateFIPS': '',
'tax': 0.15,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.15,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 15.0,
'taxableUnits': 15.0,
'transactionId': 7119936253,
'transactionLineId': 6406250349,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 6406250349,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': 'false',
'lineAmount': 15.0,
'lineLocationTypes': [{'documentAddressId': 5000306545659,
'documentLineId': 6406250349,
'documentLineLocationTypeId': 11338538066,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 12063164280,
'documentLineId': 6406250349,
'documentLineLocationTypeId': 13555596207,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,443',
'nonPassthroughDetails': [],
'originAddressId': 5000306545659,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'revAccount': '',
'sourcing': 'Origin',
'tax': 1.09,
'taxCalculated': 1.09,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': 15.0,
'transactionId': 7119936253,
'vatCode': '',
'vatNumberTypeId': 0}],
'locationCode': '',
'locationTypes': [{'documentAddressId': 5000306545659,
'documentId': 7119936253,
'documentLocationTypeId': 7051203082,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 12063164280,
'documentId': 7119936253,
'documentLocationTypeId': 9051203083,
'locationTypeCode': 'ShipTo'}],
'locked': False,
'modifiedDate': '2021-09-27T16:19:38.6053604Z',
'modifiedUserId': 212768,
'originAddressId': 5000306545659,
'purchaseOrderNo': '',
'reconciled': False,
'referenceCode': 'INV/2021/01/0001',
'region': 'CA',
'reportingLocationCode': '',
'salespersonCode': '',
'softwareVersion': '21.8.1.0',
'status': 'Saved',
'summary': [{'country': 'US',
'exemption': 0.0,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'State',
'nonTaxable': 0.0,
'rate': 0.06,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': 5.4,
'taxAuthorityType': 45,
'taxCalculated': 5.4,
'taxName': 'CA STATE TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'County',
'nonTaxable': 0.0,
'rate': 0.0025,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': 0.24,
'taxAuthorityType': 45,
'taxCalculated': 0.24,
'taxName': 'CA COUNTY TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '38',
'tax': 0.9,
'taxAuthorityType': 45,
'taxCalculated': 0.9,
'taxName': 'CA SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0}],
'taxDate': '2021-01-01',
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'totalAmount': 90.0,
'totalDiscount': 0.0,
'totalExempt': 0.0,
'totalTax': 6.54,
'totalTaxCalculated': 6.54,
'totalTaxable': 90.0,
'type': 'SalesInvoice',
'version': 1}

View File

@ -0,0 +1,482 @@
def generate_response(invoice_line_ids):
assert len(invoice_line_ids) == 1, "the mocked response is for 1 lines"
for i, line in enumerate(response['lines']):
line['lineNumber'] = 'account.move.line,%s' % invoice_line_ids[i].id
return response
response = {'addresses': [{'boundaryLevel': 'Address',
'city': 'Charleston',
'country': 'US',
'id': 85073906405601,
'latitude': '0',
'line1': 'XXXX',
'line2': '',
'line3': '',
'longitude': '0',
'postalCode': '0',
'region': 'SC',
'taxRegionId': 2113524,
'transactionId': 85073906405600},
{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 85073906405602,
'latitude': '37.71116',
'line1': '250 Executive Park Blvd Ste 3400',
'line2': '',
'line3': '',
'longitude': '-122.391717',
'postalCode': '94134-3349',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 85073906405600}],
'adjustmentDescription': 'Create or adjust transaction',
'adjustmentReason': 'Other',
'batchCode': '',
'businessIdentificationNo': '',
'code': 'Journal Entry 109',
'companyId': 2765828,
'country': 'US',
'currencyCode': 'USD',
'customerCode': 'Contact 45',
'customerUsageType': '',
'customerVendorCode': 'Contact 45',
'date': '2024-12-10',
'destinationAddressId': 0,
'entityUseCode': '',
'exchangeRate': 1.0,
'exchangeRateCurrencyCode': 'USD',
'exchangeRateEffectiveDate': '2024-12-10',
'exemptNo': '',
'id': 85073906405600,
'isSellerImporterOfRecord': False,
'lines': [{'boundaryOverrideId': 0,
'businessIdentificationNo': '',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': '[E-COM07] Large Cabinet',
'destinationAddressId': 85073906405601,
'details': [{'addressId': 85073906405601,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptRuleId': 0,
'exemptUnits': 0.0,
'id': 85073906405613,
'inState': False,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '45',
'jurisName': 'SOUTH CAROLINA',
'jurisType': 'STA',
'jurisdictionId': 52,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1109432,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'SC',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 1.8,
'reportingTaxCalculated': 1.8,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'BNPB',
'sourcing': 'Destination',
'stateAssignedNo': '',
'stateFIPS': '45',
'tax': 1.8,
'taxAuthorityTypeId': 45,
'taxCalculated': 1.8,
'taxName': 'SC STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 2113524,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 85073906405600,
'transactionLineId': 85073906405606,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85073906405601,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptRuleId': 0,
'exemptUnits': 0.0,
'id': 85073906405614,
'inState': False,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '019',
'jurisName': 'CHARLESTON',
'jurisType': 'CTY',
'jurisdictionId': 2342,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0,
'rateRuleId': 1109119,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'SC',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.0,
'reportingTaxCalculated': 0.0,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'BNSO',
'sourcing': 'Destination',
'stateAssignedNo': '1010',
'stateFIPS': '45',
'tax': 0.0,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.0,
'taxName': 'SC COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 2113524,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 85073906405600,
'transactionLineId': 85073906405606,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85073906405601,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptRuleId': 0,
'exemptUnits': 0.0,
'id': 85073906405615,
'inState': False,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '13330',
'jurisName': 'CHARLESTON',
'jurisType': 'CIT',
'jurisdictionId': 140842,
'jurisdictionType': 'City',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1110408,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'SC',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'BNSR',
'sourcing': 'Destination',
'stateAssignedNo': '2130',
'stateFIPS': '45',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'SC CITY TAX',
'taxOverride': 0.0,
'taxRegionId': 2113524,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 85073906405600,
'transactionLineId': 85073906405606,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85073906405601,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptRuleId': 0,
'exemptUnits': 0.0,
'id': 85073906405616,
'inState': False,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '1245019001',
'jurisName': 'CHARLESTON CO EDUCATIONAL CAPITAL '
'IMPROVEMENTS TAX',
'jurisType': 'STJ',
'jurisdictionId': 2001059753,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1109507,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'SC',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'EILX',
'sourcing': 'Destination',
'stateAssignedNo': '2130',
'stateFIPS': '45',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'SC SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 2113524,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 85073906405600,
'transactionLineId': 85073906405606,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85073906405601,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptRuleId': 0,
'exemptUnits': 0.0,
'id': 85073906405617,
'inState': False,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '5003794',
'jurisName': 'CHARLESTON CO TT',
'jurisType': 'STJ',
'jurisdictionId': 5003794,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1358516,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'SC',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'serCode': '',
'signatureCode': 'EETL',
'sourcing': 'Destination',
'stateAssignedNo': '2130',
'stateFIPS': '45',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'SC SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 2113524,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': 30.0,
'taxableUnits': 30.0,
'transactionId': 85073906405600,
'transactionLineId': 85073906405606,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 85073906405606,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': 'E-COM07',
'lineAmount': 30.0,
'lineLocationTypes': [{'documentAddressId': 85073906405602,
'documentLineId': 85073906405606,
'documentLineLocationTypeId': 85073906405608,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 85073906405601,
'documentLineId': 85073906405606,
'documentLineLocationTypeId': 85073906405609,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,344',
'nonPassthroughDetails': [],
'originAddressId': 85073906405602,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2024-12-10',
'revAccount': '',
'sourcing': 'Destination',
'tax': 2.7,
'taxCalculated': 2.7,
'taxCode': 'D0000000',
'taxCodeId': 8570,
'taxDate': '2024-12-10',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': 30.0,
'transactionId': 85073906405600,
'vatCode': '',
'vatNumberTypeId': 0}],
'locationCode': '',
'locationTypes': [{'documentAddressId': 85073906405602,
'documentId': 85073906405600,
'documentLocationTypeId': 85073906405604,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 85073906405601,
'documentId': 85073906405600,
'documentLocationTypeId': 85073906405605,
'locationTypeCode': 'ShipTo'}],
'locked': False,
'modifiedDate': '2024-12-10T17:42:45.1649967Z',
'modifiedUserId': 1452151,
'originAddressId': 0,
'paymentDate': '1900-01-01',
'purchaseOrderNo': '',
'reconciled': False,
'referenceCode': '/',
'region': 'SC',
'reportingLocationCode': '',
'salespersonCode': '',
'softwareVersion': '24.12.0.0',
'status': 'Saved',
'summary': [{'country': 'US',
'exemption': 0.0,
'jurisCode': '45',
'jurisName': 'SOUTH CAROLINA',
'jurisType': 'State',
'nonTaxable': 0.0,
'rate': 0.06,
'rateType': 'General',
'region': 'SC',
'stateAssignedNo': '',
'tax': 1.8,
'taxAuthorityType': 45,
'taxCalculated': 1.8,
'taxName': 'SC STATE TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 30.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '019',
'jurisName': 'CHARLESTON',
'jurisType': 'County',
'nonTaxable': 0.0,
'rate': 0.0,
'rateType': 'General',
'region': 'SC',
'stateAssignedNo': '1010',
'tax': 0.0,
'taxAuthorityType': 45,
'taxCalculated': 0.0,
'taxName': 'SC COUNTY TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 30.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '13330',
'jurisName': 'CHARLESTON',
'jurisType': 'City',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'SC',
'stateAssignedNo': '2130',
'tax': 0.3,
'taxAuthorityType': 45,
'taxCalculated': 0.3,
'taxName': 'SC CITY TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 30.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '1245019001',
'jurisName': 'CHARLESTON CO EDUCATIONAL CAPITAL IMPROVEMENTS TAX',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'SC',
'stateAssignedNo': '2130',
'tax': 0.3,
'taxAuthorityType': 45,
'taxCalculated': 0.3,
'taxName': 'SC SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 30.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '5003794',
'jurisName': 'CHARLESTON CO TT',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'SC',
'stateAssignedNo': '2130',
'tax': 0.3,
'taxAuthorityType': 45,
'taxCalculated': 0.3,
'taxName': 'SC SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 30.0}],
'taxDate': '2024-12-10',
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'totalAmount': 30.0,
'totalDiscount': 0.0,
'totalExempt': 0.0,
'totalTax': 2.7,
'totalTaxCalculated': 2.7,
'totalTaxable': 30.0,
'type': 'SalesInvoice',
'version': 15}

View File

@ -0,0 +1,408 @@
def generate_response(invoice_line_ids):
assert len(invoice_line_ids) == 1, "the mocked response is for 1 line"
RESPONSE['lines'][0]['lineNumber'] = 'account.move.line,%s' % invoice_line_ids[0].id
return RESPONSE
RESPONSE = {'addresses': [{'boundaryLevel': 'Address',
'city': 'Fremont',
'country': 'US',
'id': 85047347585913,
'latitude': '37.530253',
'line1': '4557 De Silva St',
'line2': '',
'line3': '',
'longitude': '-121.974682',
'postalCode': '94538-2506',
'region': 'CA',
'taxRegionId': 4003562,
'transactionId': 85047347585912},
{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 85047347585914,
'latitude': '37.71116',
'line1': '250 Executive Park Blvd Ste 3400',
'line2': '',
'line3': '',
'longitude': '-122.391717',
'postalCode': '94134-3349',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 85047347585912}],
'adjustmentDescription': 'Create or adjust transaction',
'adjustmentReason': 'Other',
'batchCode': '',
'businessIdentificationNo': 'US12345677',
'code': 'Journal Entry 204',
'companyId': 2765828,
'country': 'US',
'currencyCode': 'USD',
'customerCode': 'Contact 14',
'customerUsageType': '',
'customerVendorCode': 'Contact 14',
'date': '2024-01-24',
'description': '',
'destinationAddressId': 0,
'entityUseCode': '',
'exchangeRate': 1.0,
'exchangeRateCurrencyCode': 'USD',
'exchangeRateEffectiveDate': '2024-01-24',
'exemptNo': '',
'id': 85047347585912,
'isSellerImporterOfRecord': False,
'lines': [{'boundaryOverrideId': 0,
'businessIdentificationNo': 'US12345677',
'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Office Cleaning Service (SUB) (digital)',
'destinationAddressId': 85047347585913,
'details': [{'addressId': 85047347585914,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 85047347585925,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionId': 5000531,
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.06,
'rateRuleId': 1525706,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -2.1,
'reportingTaxCalculated': -2.1,
'reportingTaxableUnits': -35.0,
'serCode': '',
'signatureCode': 'AGAM',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': -2.1,
'taxAuthorityTypeId': 45,
'taxCalculated': -2.1,
'taxName': 'CA STATE TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': -35.0,
'taxableUnits': -35.0,
'transactionId': 85047347585912,
'transactionLineId': 85047347585918,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85047347585914,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 85047347585926,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionId': 275,
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.0025,
'rateRuleId': 1525710,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.09,
'reportingTaxCalculated': -0.09,
'reportingTaxableUnits': -35.0,
'serCode': '',
'signatureCode': 'AIUQ',
'sourcing': 'Origin',
'stateAssignedNo': '',
'stateFIPS': '',
'tax': -0.09,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.09,
'taxName': 'CA COUNTY TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': -35.0,
'taxableUnits': -35.0,
'transactionId': 85047347585912,
'transactionLineId': 85047347585918,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85047347585913,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 85047347585927,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMAK0',
'jurisName': 'ALAMEDA COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionId': 2001061409,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.03,
'rateRuleId': 2443976,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -1.05,
'reportingTaxCalculated': -1.05,
'reportingTaxableUnits': -35.0,
'serCode': '',
'signatureCode': 'EMAK',
'sourcing': 'Destination',
'stateAssignedNo': '966',
'stateFIPS': '',
'tax': -1.05,
'taxAuthorityTypeId': 45,
'taxCalculated': -1.05,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4003562,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': -35.0,
'taxableUnits': -35.0,
'transactionId': 85047347585912,
'transactionLineId': 85047347585918,
'unitOfBasis': 'PerCurrencyUnit'},
{'addressId': 85047347585914,
'chargedTo': 'Buyer',
'country': 'US',
'countyFIPS': '',
'exemptAmount': 0.0,
'exemptReasonId': 4,
'exemptUnits': 0.0,
'id': 85047347585928,
'inState': True,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionId': 2001061792,
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'nonTaxableRuleId': 0,
'nonTaxableType': 'RateRule',
'nonTaxableUnits': 0.0,
'rate': 0.01,
'rateRuleId': 1525730,
'rateSourceId': 3,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.35,
'reportingTaxCalculated': -0.35,
'reportingTaxableUnits': -35.0,
'serCode': '',
'signatureCode': 'EMTV',
'sourcing': 'Origin',
'stateAssignedNo': '38',
'stateFIPS': '',
'tax': -0.35,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.35,
'taxName': 'CA SPECIAL TAX',
'taxOverride': 0.0,
'taxRegionId': 4016940,
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxTypeGroupId': 'SalesAndUse',
'taxableAmount': -35.0,
'taxableUnits': -35.0,
'transactionId': 85047347585912,
'transactionLineId': 85047347585918,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'discountTypeId': 0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 85047347585918,
'isItemTaxable': True,
'isSSTP': False,
'itemCode': '',
'lineAmount': -35.0,
'lineLocationTypes': [{'documentAddressId': 85047347585914,
'documentLineId': 85047347585918,
'documentLineLocationTypeId': 85047347585920,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 85047347585913,
'documentLineId': 85047347585918,
'documentLineLocationTypeId': 85047347585921,
'locationTypeCode': 'ShipTo'}],
'lineNumber': 'account.move.line,552',
'nonPassthroughDetails': [],
'originAddressId': 85047347585914,
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2024-01-24',
'revAccount': '',
'sourcing': 'Mixed',
'tax': -3.59,
'taxCalculated': -3.59,
'taxCode': 'D0000000',
'taxCodeId': 8570,
'taxDate': '2024-01-24',
'taxEngine': '',
'taxIncluded': False,
'taxOverrideAmount': 0.0,
'taxOverrideReason': '',
'taxOverrideType': 'None',
'taxableAmount': -35.0,
'transactionId': 85047347585912,
'vatCode': '',
'vatNumberTypeId': 0}],
'locationCode': '',
'locationTypes': [{'documentAddressId': 85047347585914,
'documentId': 85047347585912,
'documentLocationTypeId': 85047347585916,
'locationTypeCode': 'ShipFrom'},
{'documentAddressId': 85047347585913,
'documentId': 85047347585912,
'documentLocationTypeId': 85047347585917,
'locationTypeCode': 'ShipTo'}],
'locked': False,
'modifiedDate': '2024-01-24T18:33:27.5394227Z',
'modifiedUserId': 1452151,
'originAddressId': 0,
'paymentDate': '1900-01-01',
'purchaseOrderNo': '',
'reconciled': False,
'referenceCode': '/',
'region': 'CA',
'reportingLocationCode': '',
'salespersonCode': '',
'softwareVersion': '24.1.0.0',
'status': 'Saved',
'summary': [{'country': 'US',
'exemption': 0.0,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'State',
'nonTaxable': 0.0,
'rate': 0.06,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': -2.1,
'taxAuthorityType': 45,
'taxCalculated': -2.1,
'taxName': 'CA STATE TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': -35.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'County',
'nonTaxable': 0.0,
'rate': 0.0025,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': -0.09,
'taxAuthorityType': 45,
'taxCalculated': -0.09,
'taxName': 'CA COUNTY TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': -35.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': 'EMAK0',
'jurisName': 'ALAMEDA COUNTY DISTRICT TAX SP',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.03,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '966',
'tax': -1.05,
'taxAuthorityType': 45,
'taxCalculated': -1.05,
'taxName': 'CA SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': -35.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '38',
'tax': -0.35,
'taxAuthorityType': 45,
'taxCalculated': -0.35,
'taxName': 'CA SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': -35.0}],
'taxDate': '2024-01-24',
'taxOverrideAmount': 0.0,
'taxOverrideReason': 'Manually changed the tax calculation date',
'taxOverrideType': 'TaxDate',
'totalAmount': -35.0,
'totalDiscount': 0.0,
'totalExempt': 0.0,
'totalTax': -3.59,
'totalTaxCalculated': -3.59,
'totalTaxable': -35.0,
'type': 'ReturnInvoice',
'version': 2}

View File

@ -0,0 +1,65 @@
from odoo.tests.common import tagged
from .common import TestAvataxCommon
from .mocked_address_validation_response import response as address_validation_response
from odoo.exceptions import ValidationError
class TestAccountAvalaraAddressValidationCommon(TestAvataxCommon):
"""https://developer.avalara.com/certification/avatax/address-validation-badge/"""
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.country_us = cls.env['res.country'].search([('code', '=', 'US')])
cls.country_not_us = cls.env['res.country'].search([('code', '=', 'BE')])
cls.env.company.avalara_address_validation = True
return res
def _create_partner(self):
return self.env['res.partner'].create({
'name': 'Odoo Inc',
'street': '250 executiv prk blvd',
'street2': '3400',
'city': '',
'zip': '94134',
'country_id': self.country_us.id,
})
def _test_address_validation_flow(self):
partner = self._create_partner()
wizard = self.env['avatax.validate.address'].create({'partner_id': partner.id})
wizard.action_save_validated()
self.assertEqual(partner.name, 'Odoo Inc', 'The name should not have changed.')
self.assertEqual(partner.street, '250 Executive Park Blvd Ste 3400', 'The validated address is incorrect.')
self.assertEqual(partner.street2, '', 'The validated address is incorrect.')
self.assertEqual(partner.city, 'San Francisco', 'The validated address is incorrect.')
self.assertEqual(partner.zip, '94134-3349', 'The validated address is incorrect.')
@tagged("-at_install", "post_install")
class TestAccountAvalaraAddressValidation(TestAccountAvalaraAddressValidationCommon):
def test_address_validation_wizard(self):
with self._capture_request(return_value=address_validation_response):
self._test_address_validation_flow()
def test_address_validation_NA_only(self):
partner = self._create_partner()
partner.country_id = self.country_not_us
with self.assertRaises(ValidationError):
wizard = self.env['avatax.validate.address'].create({'partner_id': partner.id})
wizard._compute_validated_address()
def test_auto_apply_fp_on_payment(self):
self.partner.zip = False
self.fp_avatax.auto_apply = True
self.fp_avatax.state_ids = self.partner.state_id
self.env["account.payment"].create({"partner_id": self.partner.id})
@tagged("-standard", "external")
class TestAccountAvalaraAddressValidationExternal(TestAccountAvalaraAddressValidationCommon):
def test_integration_address_validation_wizard(self):
with self._skip_no_credentials():
self._test_address_validation_flow()

View File

@ -0,0 +1,547 @@
from collections import defaultdict
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.tests.common import tagged
from odoo.modules.neutralize import get_neutralization_queries
from .common import TestAccountAvataxCommon
from .mocked_refund_1_response import generate_response as generate_response_refund_1
class TestAccountAvalaraInternalCommon(TestAccountAvataxCommon):
def assertInvoice(self, invoice, test_exact_response):
self.assertEqual(
len(invoice.invoice_line_ids.tax_ids),
0,
"There should be no tax rate on the line."
)
self.assertRecordValues(invoice, [{
'amount_total': 90.0,
'amount_untaxed': 90.0,
'amount_tax': 0.0,
}])
invoice.action_post()
tax_groups = invoice.tax_totals['subtotals'][0]['tax_groups']
self.assertEqual(len(tax_groups), 1, "There should be one tax group on the invoice containing all taxes.")
self.assertEqual(tax_groups[0]['group_name'], 'Taxes')
if test_exact_response:
self.assertRecordValues(invoice, [{
'amount_total': 96.54,
'amount_untaxed': 90.0,
'amount_tax': 6.54,
}])
avatax_mapping = {avatax_line['lineNumber']: avatax_line for avatax_line in test_exact_response['lines']}
for line in invoice.invoice_line_ids:
line_number = f'account.move.line,{line.id}'
self.assertIn(line_number, avatax_mapping)
avatax_line = avatax_mapping[line_number]
self.assertEqual(
line.price_total,
avatax_line['tax'] + avatax_line['lineAmount'],
f"Tax-included price doesn't match tax returned by Avatax for line {line.id} (product: {line.product_id.display_name})."
)
self.assertEqual(
line.price_subtotal,
avatax_line['lineAmount'],
f"Wrong Avatax amount for {line.id} (product: {line.product_id.display_name}), there is probably a mismatch between the test SO and the mocked response."
)
else:
for line in invoice.invoice_line_ids:
product_name = line.product_id.display_name
self.assertGreater(len(line.tax_ids), 0, "Line with %s did not get any taxes set." % product_name)
self.assertGreater(invoice.amount_tax, 0.0, "Invoice has a tax_amount of 0.0.")
@tagged("-at_install", "post_install")
class TestAccountAvalaraInternal(TestAccountAvalaraInternalCommon):
def test_01_odoo_invoice(self):
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response):
self.assertInvoice(invoice, test_exact_response=response)
with patch('odoo.addons.odex30_account_avatax.models.account_external_tax_mixin.AccountExternalTaxMixin._uncommit_external_taxes') as mocked_uncommit:
invoice.button_draft()
mocked_uncommit.assert_called()
def test_02_odoo_invoice(self):
invoice, response = self._create_invoice_02_and_expected_response()
with self._capture_request(return_value=response):
self.assertInvoice(invoice, test_exact_response=response)
# verify transactions are uncommitted
with patch('odoo.addons.odex30_account_avatax.models.account_external_tax_mixin.AccountExternalTaxMixin._uncommit_external_taxes') as mocked_uncommit:
invoice.button_draft()
mocked_uncommit.assert_called()
def test_03_odoo_invoice(self):
invoice, response = self._create_invoice_03_and_expected_response()
self.assertRecordValues(invoice, [{
'amount_total': 30.0,
'amount_untaxed': 30.0,
'amount_tax': 0.0,
}])
with self._capture_request(return_value=response):
invoice.action_post()
self.assertEqual(invoice.amount_total, 32.7, "Wrong total amount, it should be $30.00 + $2.70 of taxes.")
def test_01_odoo_refund(self):
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response):
invoice.action_post()
move_reversal = self.env['account.move.reversal'] \
.with_context(active_model='account.move', active_ids=invoice.ids) \
.create({'journal_id': invoice.journal_id.id})
refund = self.env['account.move'].browse(move_reversal.refund_moves()['res_id'])
for line in refund._get_avatax_invoice_lines():
if 'Discount' in line['description']:
self.assertGreater(line['amount'], 0)
else:
self.assertLess(line['amount'], 0)
def test_02_odoo_refund(self):
refund = self.env['account.move'].create({
'move_type': 'out_refund',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'invoice_date': '2024-01-24',
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_user.id,
'tax_ids': None,
'price_unit': self.product_user.list_price,
}),
]
})
response = generate_response_refund_1(refund.invoice_line_ids)
with self._capture_request(return_value=response):
refund.button_external_tax_calculation()
self.assertEqual(
refund.invoice_line_ids[0].price_subtotal,
self.product_user.list_price,
"Subtotal shouldn't have changed on this refund"
)
self.assertEqual(
refund.invoice_line_ids[0].price_total,
abs(response['lines'][0]['tax'] + response['lines'][0]['lineAmount']),
"Total amount should match the absolute value of what Avatax returned (which is negative for refunds)"
)
def test_unlink(self):
invoice, _ = self._create_invoice_01_and_expected_response()
mock_response = {'error': {'code': 'EntityNotFoundError',
'details': [{'code': 'EntityNotFoundError',
'description': "The Document with code 'Journal Entry "
"2180' was not found.",
'faultCode': 'Client',
'helpLink': 'http://developer.avalara.com/avatax/errors/EntityNotFoundError',
'message': 'Document not found.',
'number': 4,
'severity': 'Error'}],
'message': 'Document not found.',
'target': 'HttpRequest'}}
with self._capture_request(return_value=mock_response) as capture:
invoice.unlink()
self.assertEqual(capture.val['json']['code'], 'DocVoided', 'Should have tried to void without raising on EntityNotFoundError.')
def test_journal_entry(self):
entry, _ = self._create_invoice_01_and_expected_response()
entry.move_type = 'entry'
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
entry.action_post()
self.assertIsNone(capture.val, "Journal entries should not be sent to Avatax.")
def test_vendor_bill(self):
vendor_bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'invoice_date': '2017-01-01',
'partner_id': self.partner.id,
'invoice_line_ids': [(0, 0, {'product_id': self.product_user.id, 'price_unit': 123.0, 'tax_ids': []})],
})
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
vendor_bill.action_post()
self.assertIsNone(capture.val, "Posting a vendor bill should not send anything to Avatax.")
vendor_bill.button_draft()
self.assertIsNone(capture.val, "Resetting a vendor bill to draft should not send anything to Avatax.")
vendor_bill.unlink()
self.assertIsNone(capture.val, "Deleting a vendor bill should not send anything to Avatax.")
def test_invoice_multi_company(self):
invoice, response = self._create_invoice_01_and_expected_response()
company_2 = self.setup_other_company()['company']
company_2.account_fiscal_country_id = self.env.ref('base.be')
self.env.user.company_id = company_2
with self._capture_request(return_value=response):
# ensure this doesn't raise:
invoice.button_external_tax_calculation()
def test_invoice_branch_company(self):
branch = self.env['res.company'].create({
'name': "Branch A",
'parent_id': self.env.company.id,
})
child_branch = self.env['res.company'].create({
'name': "Branch B",
'parent_id': branch.id,
})
self.cr.precommit.run() # load the CoA
invoice = self._create_invoice(post=False, company_id=child_branch.id)
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
self.env.company.avalara_api_id = False
with self.assertRaises(RedirectWarning, msg='Please add your AvaTax credentials'):
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
child_branch.write({
'avalara_api_id': "AVALARA_LOGIN_ID",
'avalara_api_key': "AVALARA_API_KEY",
'avalara_environment': 'sandbox',
'avalara_commit': True,
})
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
def test_posted_invoice(self):
invoice, _ = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.action_post()
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
invoice.button_external_tax_calculation()
self.assertIsNone(capture.val, "Should not update taxes of posted invoices.")
def test_check_address_constraint(self):
invoice, _ = self._create_invoice_01_and_expected_response()
partner_no_zip = self.env["res.partner"].create({
"name": "Test no zip",
"state_id": self.env.ref("base.state_us_5").id,
"country_id": self.env.ref("base.us").id,
"zip": False,
"property_account_position_id": self.fp_avatax.id,
})
with self.assertRaises(ValidationError):
invoice.partner_id = partner_no_zip
def test_negative_quantities(self):
line_data = defaultdict(lambda: False)
line_data["product_id"] = self.product_accounting
line_data["qty"] = -1
res = self.env['account.external.tax.mixin']._get_avatax_invoice_line(line_data)
self.assertEqual(res['quantity'], 1, 'Quantities sent to Avatax should always be positive.')
def test_multi_currency_exempted_tax(self):
currency = self.setup_other_currency('EUR')
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'currency_id': currency.id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_user.id,
'tax_ids': None,
'price_unit': 100.00,
}),
]
})
lines = [{
'details': [{
'jurisCode': '06',
'nonTaxableAmount': 0.0,
'rate': 0.04,
'taxableAmount': 100.0,
'taxName': 'CA STATE TAX',
}, {
'jurisCode': '075',
'nonTaxableAmount': 100.0,
'rate': 0.06,
'taxableAmount': 0.0,
'taxName': 'CA COUNTY TAX',
}],
'lineAmount': 100.0,
'lineNumber': 'account.move.line,' + str(invoice.invoice_line_ids.id),
'tax': 4.0,
}]
summary = [{
'jurisCode': '06',
'nonTaxable': 0.0,
'rate': 0.04,
'tax': 4.0,
'taxCalculated': 4.0,
'taxName': 'CA STATE TAX',
'taxable': 100.0,
}, {
'country': 'US',
'jurisCode': '075',
'nonTaxable': 100.0,
'rate': 0.06,
'tax': 0.0,
'taxCalculated': 0.0,
'taxName': 'CA COUNTY TAX',
'taxable': 0.0,
}]
with self._capture_request(return_value={'lines': lines, 'summary': summary}):
invoice.action_post()
self.assertRecordValues(invoice, [{'amount_tax': 4.0, 'amount_total': 104.0, 'amount_untaxed': 100.0}])
tax_line = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA STATE 4%')
self.assertRecordValues(tax_line, [{'amount_currency': -4.0, 'balance': -2.0, 'debit': 0.0, 'credit': 2.0}])
exempted_tax_line = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA COUNTY 6%')
self.assertRecordValues(exempted_tax_line, [{'amount_currency': 0.0, 'balance': 0.0, 'debit': 0.0, 'credit': 0.0}])
def test_invoice_multi_taxline(self):
self.env['account.fiscal.position'].search([('is_avatax', '=', True)]).write({
'avatax_invoice_account_id': False,
'avatax_refund_account_id': False,
})
default_plan = self.env['account.analytic.plan'].create({'name': 'Default'})
analytic_account_a = self.env['account.analytic.account'].create({
'name': 'analytic_account_a',
'plan_id': default_plan.id,
'company_id': False,
})
analytic_account_b = self.env['account.analytic.account'].create({
'name': 'analytic_account_b',
'plan_id': default_plan.id,
'company_id': False,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_accounting.id,
'tax_ids': None,
'price_unit': 295.00,
'analytic_distribution': {
analytic_account_a.id: 100,
},
}),
Command.create({
'product_id': self.product_accounting.id,
'tax_ids': None,
'price_unit': 295.00,
'analytic_distribution': {
analytic_account_b.id: 100,
},
}),
]
})
response = {
'lines': [{'details': [{'jurisCode': '06',
'rate': 0.06,
'taxName': 'CA STATE TAX'},
{'jurisCode': '075',
'rate': 0.0025,
'taxName': 'CA COUNTY TAX'},
{'jurisCode': 'EMAK0',
'rate': 0.03,
'taxName': 'CA SPECIAL TAX'},
{'jurisCode': 'EMTV0',
'rate': 0.01,
'taxName': 'CA SPECIAL TAX'}],
'lineAmount': 295.0,
'lineNumber': 'account.move.line,' + str(line.id),
'tax': 30.24} for line in invoice.invoice_line_ids],
'summary': [{'jurisCode': '06',
'nonTaxable': 0.0,
'rate': 0.06,
'tax': 35.4,
'taxCalculated': 35.4,
'taxName': 'CA STATE TAX',
'taxable': 590.0},
{'jurisCode': '075',
'nonTaxable': 0.0,
'rate': 0.0025,
'tax': 1.48,
'taxCalculated': 1.48,
'taxName': 'CA COUNTY TAX',
'taxable': 590.0}]}
with self._capture_request(return_value=response):
# ensure this doesn't raise:
# odoo.exceptions.ValidationError: Expected singleton:
invoice.button_external_tax_calculation()
tax_lines = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA STATE 6%')
self.assertEqual(len(tax_lines), 2, "Multiple tax lines should have been created")
self.assertRecordValues(invoice, [{'amount_tax': 60.48, 'amount_total': 650.48, 'amount_untaxed': 590.0}])
self.assertRecordValues(tax_lines, [{'amount_currency': -17.7}, {'amount_currency': -17.7}])
def test_fully_discounted_invoice(self):
invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_accounting.id,
'tax_ids': None,
'price_unit': 295.00,
}),
Command.create({
'product_id': self.product_user_discound.id,
'tax_ids': None,
'price_unit': -295.00,
}),
]
}])
self.assertEqual(invoice.amount_total, 0.00, "Invoice should be $0 before tax calculation.")
response = {
'lines': [{'details': [{'jurisCode': '06',
'rate': 0.06,
'taxName': 'CA STATE TAX'},
{'jurisCode': '075',
'rate': 0.0025,
'taxName': 'CA COUNTY TAX'},
{'jurisCode': 'EMAK0',
'rate': 0.03,
'taxName': 'CA SPECIAL TAX'},
{'jurisCode': 'EMTV0',
'rate': 0.01,
'taxName': 'CA SPECIAL TAX'}],
'lineAmount': 295.0,
'lineNumber': f'account.move.line,{invoice.invoice_line_ids[0].id}',
'tax': 30.24},
{'details': [{'jurisCode': '06',
'rate': 0.06,
'taxName': 'CA STATE TAX'},
{'jurisCode': '075',
'rate': 0.0025,
'taxName': 'CA COUNTY TAX'},
{'jurisCode': 'EMAK0',
'rate': 0.03,
'taxName': 'CA SPECIAL TAX'},
{'jurisCode': 'EMTV0',
'rate': 0.01,
'taxName': 'CA SPECIAL TAX'}],
'lineAmount': -295.0,
'lineNumber': f'account.move.line,{invoice.invoice_line_ids[1].id}',
'tax': 0.00}], # This discount is tax-exempt.
'summary': [{'jurisCode': '06',
'rate': 0.06,
'tax': 17.7,
'taxName': 'CA STATE TAX',
'taxable': 295.0},
{'jurisCode': '075',
'rate': 0.0025,
'tax': 0.74,
'taxName': 'CA COUNTY TAX',
'taxable': 295.0},
{'jurisCode': 'EMAK0',
'rate': 0.03,
'tax': 8.85,
'taxName': 'CA SPECIAL TAX',
'taxable': 295.0},
{'jurisCode': 'EMAK0',
'rate': 0.01,
'tax': 2.95,
'taxName': 'CA SPECIAL TAX',
'taxable': 295.0},
]}
with self._capture_request(return_value=response):
invoice.button_external_tax_calculation()
self.assertRecordValues(
invoice.line_ids,
[
{'name': 'Accounting', 'balance': -295.00}, # Income account
{'name': 'Odoo User Initial Discount', 'balance': 295.00}, # Income account
{'name': False, 'balance': sum(t['tax'] for t in response['summary'])}, # AR
{'name': 'CA STATE 6%', 'balance': -17.7},
{'name': 'CA COUNTY 0.25%', 'balance': -0.74},
{'name': 'CA SPECIAL 3%', 'balance': -8.85},
{'name': 'CA SPECIAL 1%', 'balance': -2.95},
]
)
@tagged("external_l10n", "external", "-at_install", "post_install", "-standard")
class TestAccountAvalaraInternalIntegration(TestAccountAvalaraInternalCommon):
def test_integration_01_odoo_invoice(self):
with self._skip_no_credentials():
invoice, _ = self._create_invoice_01_and_expected_response()
self.assertInvoice(invoice, test_exact_response=False)
invoice.button_draft()
def test_integration_02_odoo_invoice(self):
with self._skip_no_credentials():
invoice, _ = self._create_invoice_02_and_expected_response()
self.assertInvoice(invoice, test_exact_response=False)
invoice.button_draft()
@tagged("-at_install", "post_install")
class TestAccountAvalaraSalesTaxAdministration(TestAccountAvataxCommon):
"""https://developer.avalara.com/certification/avatax/sales-tax-badge/"""
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.config = cls.env['res.config.settings'].create({})
return res
def test_disable_document_recording(self):
self.env.company.avalara_commit = False
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response) as capture:
invoice.action_post()
self.assertFalse(capture.val['json']['createTransactionModel']['commit'], 'Should not have committed.')
def test_disable_avatax(self):
self.fp_avatax.is_avatax = False
with patch('odoo.addons.odex30_account_avatax.lib.avatax_client.AvataxClient.request') as mocked_request:
self._create_invoice()
mocked_request.assert_not_called()
def test_disable_avatax_neutralize(self):
"""ORM's neutralization feature works."""
self.cr.execute(next(get_neutralization_queries(['odex30_account_avatax'])))
with patch('odoo.addons.odex30_account_avatax.lib.avatax_client.AvataxClient.request') as mocked_request:
self._create_invoice()
mocked_request.assert_not_called()
def test_integration_connect_button(self):
with self._skip_no_credentials(), self.assertRaisesRegex(UserError, "'version'"):
self.config.avatax_ping()

View File

@ -0,0 +1,61 @@
from odoo.tests.common import tagged
from .common import TestAccountAvataxCommon
from odoo.exceptions import UserError
@tagged("-at_install", "post_install")
class TestAvataxUniqueCode(TestAccountAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.partner_1 = cls.env["res.partner"].create({"name": "partner bob"})
cls.partner_2 = cls.env["res.partner"].create({"name": "partner alice"})
return res
def _search_equal_and_return(self, term):
return self.env["res.partner"].search([("avatax_unique_code", "=", term)])
def _search_not_equal_and_return(self, term):
return self.env["res.partner"].search([("avatax_unique_code", "!=", term)])
def test_search_equal(self):
self.assertEqual(
self._search_equal_and_return(str(self.partner_1.id)),
self.partner_1
)
self.assertFalse(self._search_equal_and_return(f" Contact {str(self.partner_1.id)}"))
not_equal = self._search_not_equal_and_return(f"Contact {str(self.partner_1.id)}")
self.assertFalse(self.partner_1 in not_equal)
self.assertTrue(self.partner_2 in not_equal)
self.assertFalse(self._search_equal_and_return("Contact"))
self.assertFalse(self._search_equal_and_return(f"{str(self.partner_1.id)} {str(self.partner_1.id)}"))
def _search_ilike_and_return(self, term):
return self.env["res.partner"].search([("avatax_unique_code", "ilike", term)])
def _search_not_ilike_and_return(self, term):
return self.env["res.partner"].search(["!", ("avatax_unique_code", "ilike", term)])
def test_search_like(self):
self.assertEqual(
self._search_ilike_and_return(str(self.partner_1.id)),
self.partner_1
)
self.assertEqual(
self._search_ilike_and_return(f"Contact {str(self.partner_1.id)}"),
self.partner_1
)
not_like = self._search_not_ilike_and_return(f"Contact {str(self.partner_1.id)}")
self.assertFalse(self.partner_1 in not_like)
self.assertTrue(self.partner_2 in not_like)
self.assertFalse(self._search_ilike_and_return("Contact"))
self.assertFalse(self._search_ilike_and_return(f"{str(self.partner_1.id)} {str(self.partner_1.id)}"))
def test_search_set(self):
self.assertRaises(UserError, self.env["res.partner"].search, [("avatax_unique_code", "=", True)])
self.assertRaises(UserError, self.env["res.partner"].search, [("avatax_unique_code", "=", False)])

View File

@ -0,0 +1,71 @@
from odoo.tests.common import tagged
from .common import TestAccountAvataxCommon
@tagged("-at_install", "post_install")
class TestAccountAvalaraRefunds(TestAccountAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.product = cls.env["product.product"].create({
'name': "Product",
'list_price': 15.00,
'standard_price': 15.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('account_avatax.DC010000').id,
})
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
cls.invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_date': '2020-01-01',
'invoice_line_ids': [
(0, 0, {
'product_id': cls.product.id,
'price_unit': cls.product.list_price,
})
]
})
cls.invoice.button_external_tax_calculation()
cls.invoice_captured_arguments = capture.val['json']['createTransactionModel']
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
cls.invoice.action_post()
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
move_reversal = cls.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=cls.invoice.ids
).create({
'date': '2020-02-01',
'reason': 'no reason',
'journal_id': cls.invoice.journal_id.id,
})
reversal = move_reversal.refund_moves()
reverse_move = cls.env['account.move'].browse(reversal['res_id'])
reverse_move.button_external_tax_calculation()
cls.refund_captured_arguments = capture.val['json']['createTransactionModel']
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
reverse_move.action_post()
cls.refund_commit_captured_arguments = capture.val['json']['createTransactionModel']
return res
def test_post_tax_credit_memento(self):
self.assertEqual(self.refund_captured_arguments['type'], 'ReturnInvoice')
self.assertTrue(self.refund_commit_captured_arguments['commit'])
def test_original_invoice_date(self):
self.assertTrue('taxOverride' not in self.invoice_captured_arguments)
self.assertEqual(self.invoice_captured_arguments['date'], '2020-01-01')
self.assertEqual(
self.refund_captured_arguments['taxOverride']['taxDate'], '2020-01-01',
'Refund date should be overridden to match the tax calculation date of the original invoice.'
)
def test_current_transaction_date(self):
self.assertEqual(self.invoice_captured_arguments['date'], '2020-01-01')
self.assertEqual(self.refund_captured_arguments['date'], '2020-02-01')

View File

@ -0,0 +1,44 @@
from odoo.tests.common import tagged
from .common import TestAvataxCommon, TestAccountAvataxCommon
@tagged("-at_install", "post_install")
class TestAccountAvalaraUseTaxVendorManagement(TestAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.config = cls.env['res.config.settings'].create({})
return res
def test_vendor_identifier_mapping(self):
Partner = self.env['res.partner']
partner = Partner.search([], limit=1)
partner_via_code = Partner.search([('avatax_unique_code', '=', partner.avatax_unique_code)], limit=1)
self.assertEqual(partner, partner_via_code, "Couldn't find partner via unique avatax code")
@tagged("-at_install", "post_install")
class TestAccountAvalaraUseTaxProductManagement(TestAccountAvataxCommon):
def test_item_code(self):
self.env.company.avalara_use_upc = False
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
invoice = self._create_invoice()
invoice.button_external_tax_calculation()
self.assertEqual(capture.val['json']['createTransactionModel']['lines'][0]['itemCode'], 'PROD1')
self.env.company.avalara_use_upc = True
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
invoice = self._create_invoice()
invoice.button_external_tax_calculation()
self.assertEqual(capture.val['json']['createTransactionModel']['lines'][0]['itemCode'], 'UPC:123456789')
def test_item_description(self):
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
invoice = self._create_invoice()
invoice.button_external_tax_calculation()
line_description = capture.val['json']['createTransactionModel']['lines'][0]['description']
self.assertEqual(invoice.invoice_line_ids.product_id.display_name, line_description)

View File

@ -0,0 +1,70 @@
from odoo.tests.common import tagged
from .common import TestAccountAvataxCommon
@tagged("-at_install", "post_install")
class TestAccountAvalaraVAT(TestAccountAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.shipping_partner = cls.partner.copy({
'name': 'Delivery Partner',
'street': '1000 Market St',
})
cls.partner.vat = 'businessid'
cls.product = cls.env["product.product"].create({
'name': "Product",
'list_price': 15.00,
'standard_price': 15.00,
'supplier_taxes_id': None,
'avatax_category_id': cls.env.ref('account_avatax.DC010000').id,
})
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
cls.invoice = cls.env['account.move'].create({
'partner_id': cls.partner.id,
'partner_shipping_id': cls.shipping_partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'invoice_date': '2021-01-01',
'move_type': 'out_invoice',
'invoice_line_ids': [
(0, 0, {
'product_id': cls.product.id,
'price_unit': cls.product.list_price,
}),
]
})
cls.invoice.button_external_tax_calculation()
cls.captured_arguments = capture.val['json']['createTransactionModel']
return res
def test_business_id(self):
vat = self.captured_arguments['businessIdentificationNo']
self.assertEqual(vat, 'businessid')
def test_country_code(self):
self.assertTrue(all(
all(address.get('country'))
for address in self.captured_arguments['addresses'].values()
))
def test_currency_code(self):
currency_code = self.captured_arguments['currencyCode']
self.assertEqual(currency_code, 'USD')
def test_ship_to_address(self):
destination_address = self.captured_arguments['addresses']['shipTo']
self.assertEqual(destination_address, {
'city': 'San Francisco',
'country': 'US',
'line1': '1000 Market St',
'postalCode': '94114',
'region': 'CA',
})
def test_ship_from_address(self):
country_code = self.captured_arguments['addresses']['shipFrom']['country']
self.assertEqual(country_code, 'US')

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_fiscal_position_form_inherit" model="ir.ui.view">
<field name="name">account.fiscal.position.form.inherit</field>
<field name="model">account.fiscal.position</field>
<field name="inherit_id" ref="account.view_account_position_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="is_avatax" invisible="'US' not in fiscal_country_codes and 'CA' not in fiscal_country_codes"/>
</field>
<notebook position="inside">
<page name="avatax" string="Avatax" invisible="not is_avatax">
<group>
<group>
<field name="avatax_invoice_account_id"/>
<field name="avatax_refund_account_id"/>
</group>
</group>
</page>
</notebook>
</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="move_form_inherit" model="ir.ui.view">
<field name="name">account.move.form.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<field name="invoice_date" position="after">
<field name="is_avatax" invisible="1"/>
<field name="avatax_tax_date" invisible="move_type not in ('out_invoice', 'out_refund') or not is_avatax"/>
</field>
<group name="accounting_info_group" position="inside">
<field name="avatax_unique_code" invisible="not is_avatax"/>
</group>
</field>
</record>
</odoo>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="avatax_category_tree" model="ir.ui.view">
<field name="name">product.avatax.category.list</field>
<field name="model">product.avatax.category</field>
<field name="arch" type="xml">
<list>
<field name="code"/>
<field name="description"/>
</list>
</field>
</record>
<record id="avatax_category_search" model="ir.ui.view">
<field name="name">product.avatax.category.search</field>
<field name="model">product.avatax.category</field>
<field name="arch" type="xml">
<search>
<field name="description"/>
<field name="code"/>
</search>
</field>
</record>
</odoo>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="avatax_exemption_tree" model="ir.ui.view">
<field name="name">avatax.exemption.list</field>
<field name="model">avatax.exemption</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="code"/>
<field name="description"/>
<field name="valid_country_ids" widget="many2many_tags"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="avatax_exemption_search" model="ir.ui.view">
<field name="name">avatax.exemption.search</field>
<field name="model">avatax.exemption</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="code"/>
<field name="description"/>
<field name="valid_country_ids"/>
<field name="company_id"/>
</search>
</field>
</record>
</odoo>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_inherit" model="ir.ui.view">
<field name="name">product.template.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="taxes_div" position="after">
<field name="fiscal_country_codes" invisible="True"/>
<field name="avatax_category_id" invisible="fiscal_country_codes not in ('US', 'CA')"/>
</div>
</field>
</record>
<record id="product_category_form_inherit" model="ir.ui.view">
<field name="name">product.category.form.inherit</field>
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view"/>
<field name="arch" type="xml">
<field name="parent_id" position="after">
<field name="avatax_category_id" class="oe_inline"/>
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_logging_avalara_tree" model="ir.ui.view">
<field name="name">ir.logging.avalara</field>
<field name="model">ir.logging</field>
<field name="arch" type="xml">
<list create="0">
<field name="message"/>
<field name="path"/>
<field name="func"/>
<field name="line"/>
</list>
</field>
</record>
<record id="ir_logging_avalara_action" model="ir.actions.act_window">
<field name="name">Avalara Logging</field>
<field name="res_model">ir.logging</field>
<field name="view_mode">list,form</field>
<field name="domain">[('name', 'in', ['Avatax', 'Avatax US'])]</field>
</record>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.account.avatax</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<setting id="rounding_method" position="after">
<setting id="avatax_settings" string="AvaTax" help="Automatically compute tax rates in the US and Canada."
documentation="/applications/finance/accounting/taxation/taxes/avatax.html"
invisible="country_code not in ('US', 'CA')">
<field name="setting_account_avatax"/>
<div class="content-group" invisible="not setting_account_avatax">
<div class="row mt16">
<label string="Environment" for="avalara_environment" class="col-lg-3 o_light_label"/>
<field name="avalara_environment"/>
</div>
<div class="row">
<label string="API ID" for="avalara_api_id" class="col-lg-3 o_light_label"/>
<field name="avalara_api_id"/>
</div>
<div class="row">
<label string="API KEY" for="avalara_api_key" class="col-lg-3 o_light_label" />
<field name="avalara_api_key"/>
</div>
<div class="row">
<label string="Company Code" for="avalara_partner_code" class="col-lg-3 o_light_label" />
<field name="avalara_partner_code"/>
</div>
<div class="row">
<label string="Use UPC" for="avalara_use_upc" class="col-lg-3 o_light_label" />
<field name="avalara_use_upc" class="w-50"/>
</div>
<div class="row">
<label string="Commit Transactions" for="avalara_commit" class="col-lg-3 o_light_label" />
<field name="avalara_commit" class="w-50"/>
</div>
<div class="row">
<label string="Address Validation" for="avalara_address_validation" class="col-lg-3 o_light_label" />
<field name="avalara_address_validation" class="w-50"/>
</div>
<div class="mt16" invisible="avalara_api_id and avalara_api_key">
<a href="https://www.avalara.com/us/en/get-started.html" target="_new">
<i class="oi oi-fw oi-arrow-right"/>
How to Get Credentials
</a>
</div>
<div class="mt16" invisible="not (avalara_api_id and avalara_api_key)">
<a href="https://admin.avalara.com/" target="_new">
<i title="Go to Avatax portal" role="img" aria-label="Go to Avatax portal" class="fa fa-external-link-square fa-fw"/>
Avatax portal
</a>
<button name="avatax_ping" type="object" class="btn-link">
<i title="Test connection" role="img" aria-label="Test connection" class="fa fa-plug fa-fw"/>
Test connection
</button>
</div>
<div class="mt16">
<button name="avatax_sync_company_params" type="object" class="btn-link">
<i title="Sync Parameters" role="img" aria-label="Sync Parameters" class="fa fa-refresh"/>
Sync Parameters
</button>
<div class="text-muted">
Synchronize the exemption codes from Avatax
</div>
</div>
<div class="mt16">
<button name="avatax_log" type="object" class="btn-link">
<i title="Start logging for 30 minutes" role="img" aria-label="Start logging for 30 minutes" class="fa fa-file-text-o"/>
Start logging for 30 minutes
</button>
<button name="odex30_account_avatax.ir_logging_avalara_action" type="action" class="btn-link">
<i title="Show logs" role="img" aria-label="Show logs" class="fa fa-file-text-o"/>
Show logs
</button>
</div>
</div>
</setting>
</setting>
</field>
</record>
</odoo>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<group name="sale" position="inside">
<field name="avatax_unique_code" invisible="fiscal_country_codes not in ('US', 'CA')"/>
<field name="avalara_partner_code" invisible="parent_id or not is_company or fiscal_country_codes not in ('US', 'CA')"/>
<field name="avalara_exemption_id" invisible="parent_id or not is_company or fiscal_country_codes not in ('US', 'CA')"/>
</group>
<xpath expr="//div[@name='partner_address_country']" position="inside">
<field name="avalara_show_address_validation" invisible="1"/>
<span class="o_form_label o_td_label"/>
<button class="btn-link"
type="object"
name="action_open_validation_wizard"
string="Validate"
invisible="not avalara_show_address_validation"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
from . import avatax_validate_address
from . import avatax_connection_test_result

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class AvataxConnectionTestResult(models.TransientModel):
_name = "avatax.connection.test.result"
_description = 'Test connection with avatax'
server_response = fields.Html(string='Server Response')

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="avatax_connection_test_result_view_form" model="ir.ui.view">
<field name="name">avatax.connection.test.result.form</field>
<field name="model">avatax.connection.test.result</field>
<field name="arch" type="xml">
<form>
<field name="server_response" readonly="1"/>
<footer>
<button string="Close" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,94 @@
# coding: utf-8
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, UserError
class AvataxValidateAddress(models.TransientModel):
_name = 'avatax.validate.address'
_description = 'Suggests validated addresses from Avatax'
partner_id = fields.Many2one('res.partner', required=True)
street = fields.Char(related='partner_id.street', string="Street")
street2 = fields.Char(related='partner_id.street2')
zip = fields.Char(related='partner_id.zip', string="Zip Code")
city = fields.Char(related='partner_id.city', string="City")
state_id = fields.Many2one('res.country.state', related='partner_id.state_id', string="State")
country_id = fields.Many2one('res.country', related='partner_id.country_id', string="Country")
validated_street = fields.Char(compute='_compute_validated_address', string="Validated Street")
validated_street2 = fields.Char(compute='_compute_validated_address')
validated_zip = fields.Char(compute='_compute_validated_address', string="Validated Zip Code")
validated_city = fields.Char(compute='_compute_validated_address', string="Validated City")
validated_state_id = fields.Many2one('res.country.state', compute='_compute_validated_address', string="Validated State")
validated_country_id = fields.Many2one('res.country', compute='_compute_validated_address', string="Validated Country")
validated_latitude = fields.Float(compute='_compute_validated_address', string='Geo Latitude', digits=(10, 7))
validated_longitude = fields.Float(compute='_compute_validated_address', string='Geo Longitude', digits=(10, 7))
is_already_valid = fields.Boolean(string="Is Already Valid", compute='_compute_validated_address')
@api.depends('partner_id')
def _compute_validated_address(self):
for wizard in self:
company = wizard.partner_id.company_id or wizard.env.company
country = wizard.partner_id.country_id
if country.code not in ('US', 'CA', False):
raise ValidationError(_("Address validation is only supported for North American addresses."))
client = self.env['account.external.tax.mixin']._get_client(company)
response = client.resolve_address({
'line1': wizard.street or '',
'line2': wizard.street2 or '',
'postalCode': wizard.zip or '',
'city': wizard.city or '',
'region': wizard.state_id.name or '',
'country': country.code or '',
'textCase': 'Mixed',
})
error = self.env['account.external.tax.mixin']._handle_response(response, _(
"Exp could not validate the address of %(partner)s with Avalara.",
partner=wizard.partner_id.display_name,
))
if error:
raise ValidationError(error)
if response.get('messages'):
messages = response['messages']
raise ValidationError('\n\n'.join(message['details'] for message in messages))
if response.get('validatedAddresses'):
validated = response['validatedAddresses'][0]
wizard.validated_street = validated['line1']
wizard.validated_street2 = validated['line2']
wizard.validated_zip = validated['postalCode']
wizard.validated_city = validated['city']
wizard.validated_country_id = self.env['res.country'].search([
('code', '=', validated['country'])]
).id
wizard.validated_state_id = self.env['res.country.state'].search([
('code', '=', validated['region']),
('country_id', '=', wizard.validated_country_id.id),
]).id
wizard.validated_latitude = validated.get('latitude')
wizard.validated_longitude = validated.get('longitude')
wizard.is_already_valid = (
wizard.street == wizard.validated_street
and wizard.street2 == wizard.validated_street2
and wizard.zip == wizard.validated_zip
and wizard.city == wizard.validated_city
and wizard.country_id == wizard.validated_country_id
and wizard.state_id == wizard.validated_state_id
)
def action_save_validated(self):
for wizard in self:
wizard.partner_id.write({
'street': wizard.validated_street,
'street2': wizard.validated_street2,
'zip': wizard.validated_zip,
'city': wizard.validated_city,
'state_id': wizard.validated_state_id.id,
'country_id': wizard.validated_country_id.id,
'partner_latitude': wizard.validated_latitude,
'partner_longitude': wizard.validated_longitude,
})
return True

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="avatax_validate_address_view_form" model="ir.ui.view">
<field name="name">avatax.validate.address.view.form</field>
<field name="model">avatax.validate.address</field>
<field name="arch" type="xml">
<form>
<field name="partner_id" invisible="1"/>
<field name="is_already_valid" invisible="1"/>
<div class="alert alert-success" role="alert" invisible="not is_already_valid">
This is already a valid address.
</div>
<group>
<group string="Validated Address">
<field name="validated_street" string="Street"/>
<field name="validated_street2" string=""/>
<field name="validated_zip" string="Zip Code"/>
<field name="validated_city" string="City"/>
<field name="validated_state_id" string="State"/>
<field name="validated_country_id" string="Country"/>
<field name="validated_latitude" string="Latitude"/>
<field name="validated_longitude" string="Longitude"/>
</group>
<group string="Original Address">
<field name="street"/>
<field name="street2" string=""/>
<field name="zip"/>
<field name="city"/>
<field name="state_id"/>
<field name="country_id"/>
</group>
</group>
<footer>
<button name="action_save_validated" type="object" default_focus="1"
string="Save Validated" class="oe_highlight" invisible="is_already_valid"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Avatax for geo localization',
'version': '1.0',
'category': 'Accounting/Accounting',
'website': 'http://exp-sa.com',
'author': 'Expert Co. Ltd.',
'depends': ['odex30_account_avatax', 'base_geolocalize'],
'data': [
'views/res_partner_views.xml',
],
'auto_install': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1,56 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_avatax_geolocalize
#
# Translators:
# Wil Odoo, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
"Last-Translator: Wil Odoo, 2024\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex30_account_avatax_geolocalize
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax_geolocalize.res_partner_form_inherit
msgid "Compute Localization"
msgstr "احتساب الأقلمة "
#. module: odex30_account_avatax_geolocalize
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax_geolocalize.res_partner_form_inherit
msgid "Compute based on address"
msgstr "احسب بناءً على العنوان"
#. module: odex30_account_avatax_geolocalize
#: model:ir.model,name:odex30_account_avatax_geolocalize.model_res_partner
msgid "Contact"
msgstr "جهة الاتصال"
#. module: odex30_account_avatax_geolocalize
#: model:ir.model.fields,field_description:odex30_account_avatax_geolocalize.field_res_partner__is_avatax_valid
#: model:ir.model.fields,field_description:odex30_account_avatax_geolocalize.field_res_users__is_avatax_valid
msgid "Is Avatax Valid"
msgstr "Is Avatax Valid"
#. module: odex30_account_avatax_geolocalize
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax_geolocalize.res_partner_form_inherit
msgid "Refresh"
msgstr "تحديث "
#. module: odex30_account_avatax_geolocalize
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax_geolocalize.res_partner_form_inherit
msgid "Refresh Localization"
msgstr "تحديث الأقلمة "
#. module: odex30_account_avatax_geolocalize
#: model:ir.model,name:odex30_account_avatax_geolocalize.model_avatax_validate_address
msgid "Suggests validated addresses from Avatax"
msgstr "يقترح عناويناً تم التحقق منها من AvaTax "

View File

@ -0,0 +1 @@
from . import res_partner

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResPartner(models.Model):
_name = 'res.partner'
_inherit = 'res.partner'
is_avatax_valid = fields.Boolean()

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='geo_localize_button']" position="replace">
<field name="is_avatax_valid" invisible="1"/>
<button invisible="partner_latitude != 0 or partner_longitude != 0"
icon="fa-gear" string="Compute based on address" title="Compute Localization"
name="geo_localize" type="object" class="btn btn-link p-0"/>
<button invisible="is_avatax_valid or partner_latitude == 0 and partner_longitude == 0"
icon="fa-refresh" string="Refresh" title="Refresh Localization"
name="geo_localize" type="object" class="btn btn-link p-0"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import avatax_validate_address

View File

@ -0,0 +1,14 @@
from odoo import models, fields
class AvataxValidateAddress(models.TransientModel):
_inherit = 'avatax.validate.address'
def action_save_validated(self):
res = super().action_save_validated()
for wizard in self:
wizard.partner_id.write({
'date_localization': fields.Date.context_today(wizard.partner_id),
'is_avatax_valid': True,
})
return res

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,15 @@
{
'name': 'Avatax for SO',
'version': '1.0',
'category': 'Accounting/Accounting',
'website': 'http://exp-sa.com',
'author': 'Expert Co. Ltd.',
'depends': ['odex30_sale_external_tax', 'odex30_account_avatax', 'sale'],
'data': [
'views/sale_order_views.xml',
'views/sale_portal_templates.xml',
'reports/sale_order.xml',
],
'auto_install': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1,35 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_avatax_sale
#
# Translators:
# Wil Odoo, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-19 09:52+0000\n"
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
"Last-Translator: Wil Odoo, 2024\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex30_account_avatax_sale
#: model:ir.model.fields,field_description:odex30_account_avatax_sale.field_sale_order__avatax_unique_code
msgid "Avalara Code"
msgstr "رمز Alavara "
#. module: odex30_account_avatax_sale
#: model:ir.model,name:odex30_account_avatax_sale.model_sale_order
msgid "Sales Order"
msgstr "أمر البيع"
#. module: odex30_account_avatax_sale
#: model:ir.model.fields,help:odex30_account_avatax_sale.field_sale_order__avatax_unique_code
msgid "Use this code to cross-reference in the Avalara portal."
msgstr "استخدم هذا الكود للإسناد الترافقي في بوابة Avalara. "

View File

@ -0,0 +1 @@
from . import sale_order

View File

@ -0,0 +1,21 @@
from odoo import models
class SaleOrder(models.Model):
_name = 'sale.order'
_inherit = ['account.avatax.unique.code', 'sale.order']
def _get_avatax_dates(self):
return self._get_date_for_external_taxes(), self._get_date_for_external_taxes()
def _get_avatax_document_type(self):
return 'SalesOrder'
def _get_avatax_description(self):
return 'Sales Order'
def _get_invoice_grouping_keys(self):
res = super()._get_invoice_grouping_keys()
if self.filtered('fiscal_position_id.is_avatax'):
res += ['partner_shipping_id']
return res

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_saleorder_document" inherit_id="sale.report_saleorder_document">
<xpath expr="//td[@name='td_taxes']" position="attributes">
<attribute name="t-if">not doc.is_avatax</attribute>
</xpath>
<xpath expr="//th[@name='th_taxes']" position="attributes">
<attribute name="t-if">not doc.is_avatax</attribute>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1 @@
from . import test_avatax

View File

@ -0,0 +1,904 @@
def generate_response(sale_order_line_ids):
assert len(sale_order_line_ids) == 5, "the mocked response is for 5 lines"
for i, line in enumerate(response['lines']):
line['lineNumber'] = 'sale.order.line,%s' % sale_order_line_ids[i].id
return response
response = {'addresses': [{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 0,
'latitude': '37.764754',
'line1': '2280 Market St',
'line2': '',
'line3': '',
'longitude': '-122.432634',
'postalCode': '94114',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 0},
{'boundaryLevel': 'Address',
'city': 'San Francisco',
'country': 'US',
'id': 0,
'latitude': '37.71116',
'line1': '250 Executive Park Blvd',
'line2': '',
'line3': '',
'longitude': '-122.391717',
'postalCode': '94134',
'region': 'CA',
'taxRegionId': 4016940,
'transactionId': 0}],
'adjustmentReason': 'NotAdjusted',
'batchCode': '',
'code': 'Sales Order 85',
'companyId': 2765828,
'currencyCode': 'USD',
'customerCode': 'CUST123456',
'customerUsageType': '',
'customerVendorCode': 'CUST123456',
'date': '2021-01-01',
'entityUseCode': '',
'exchangeRate': 1.0,
'exchangeRateCurrencyCode': 'USD',
'exchangeRateEffectiveDate': '2021-01-01',
'exemptNo': '',
'id': 0,
'lines': [{'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Odoo User',
'details': [{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.06,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 2.1,
'reportingTaxCalculated': 2.1,
'reportingTaxableUnits': 35.0,
'stateAssignedNo': '',
'tax': 2.1,
'taxAuthorityTypeId': 45,
'taxCalculated': 2.1,
'taxName': 'CA STATE TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 35.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0025,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.09,
'reportingTaxCalculated': 0.09,
'reportingTaxableUnits': 35.0,
'stateAssignedNo': '',
'tax': 0.09,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.09,
'taxName': 'CA COUNTY TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 35.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0125,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.44,
'reportingTaxCalculated': 0.44,
'reportingTaxableUnits': 35.0,
'stateAssignedNo': '052',
'tax': 0.44,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.44,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 35.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.01,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.35,
'reportingTaxCalculated': 0.35,
'reportingTaxableUnits': 35.0,
'stateAssignedNo': '38',
'tax': 0.35,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.35,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 35.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 0,
'isItemTaxable': True,
'itemCode': '',
'lineAmount': 35.0,
'lineNumber': 'sale.order.line,156',
'nonPassthroughDetails': [],
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'tax': 2.98,
'taxCalculated': 2.98,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxIncluded': False,
'taxableAmount': 35.0,
'transactionId': 0,
'vatCode': '',
'vatNumberTypeId': 0},
{'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Odoo User Initial Discound',
'details': [{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.06,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.3,
'reportingTaxCalculated': -0.3,
'reportingTaxableUnits': -5.0,
'stateAssignedNo': '',
'tax': -0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.3,
'taxName': 'CA STATE TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': -5.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0025,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.01,
'reportingTaxCalculated': -0.01,
'reportingTaxableUnits': -5.0,
'stateAssignedNo': '',
'tax': -0.01,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.01,
'taxName': 'CA COUNTY TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': -5.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0125,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.06,
'reportingTaxCalculated': -0.06,
'reportingTaxableUnits': -5.0,
'stateAssignedNo': '052',
'tax': -0.06,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.06,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': -5.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.01,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': -0.05,
'reportingTaxCalculated': -0.05,
'reportingTaxableUnits': -5.0,
'stateAssignedNo': '38',
'tax': -0.05,
'taxAuthorityTypeId': 45,
'taxCalculated': -0.05,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': -5.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 0,
'isItemTaxable': True,
'itemCode': '',
'lineAmount': -5.0,
'lineNumber': 'sale.order.line,157',
'nonPassthroughDetails': [],
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'tax': -0.42,
'taxCalculated': -0.42,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxIncluded': False,
'taxableAmount': -5.0,
'transactionId': 0,
'vatCode': '',
'vatNumberTypeId': 0},
{'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Accounting',
'details': [{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.06,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 1.8,
'reportingTaxCalculated': 1.8,
'reportingTaxableUnits': 30.0,
'stateAssignedNo': '',
'tax': 1.8,
'taxAuthorityTypeId': 45,
'taxCalculated': 1.8,
'taxName': 'CA STATE TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 30.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0025,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.08,
'reportingTaxCalculated': 0.08,
'reportingTaxableUnits': 30.0,
'stateAssignedNo': '',
'tax': 0.08,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.08,
'taxName': 'CA COUNTY TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 30.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0125,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.38,
'reportingTaxCalculated': 0.38,
'reportingTaxableUnits': 30.0,
'stateAssignedNo': '052',
'tax': 0.38,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.38,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 30.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.01,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.3,
'reportingTaxCalculated': 0.3,
'reportingTaxableUnits': 30.0,
'stateAssignedNo': '38',
'tax': 0.3,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.3,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 30.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 0,
'isItemTaxable': True,
'itemCode': '',
'lineAmount': 30.0,
'lineNumber': 'sale.order.line,158',
'nonPassthroughDetails': [],
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'tax': 2.56,
'taxCalculated': 2.56,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxIncluded': False,
'taxableAmount': 30.0,
'transactionId': 0,
'vatCode': '',
'vatNumberTypeId': 0},
{'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Expenses',
'details': [{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.06,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.9,
'reportingTaxCalculated': 0.9,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '',
'tax': 0.9,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.9,
'taxName': 'CA STATE TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0025,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.04,
'reportingTaxCalculated': 0.04,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '',
'tax': 0.04,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.04,
'taxName': 'CA COUNTY TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0125,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.19,
'reportingTaxCalculated': 0.19,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '052',
'tax': 0.19,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.19,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.01,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.15,
'reportingTaxCalculated': 0.15,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '38',
'tax': 0.15,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.15,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 0,
'isItemTaxable': True,
'itemCode': '',
'lineAmount': 15.0,
'lineNumber': 'sale.order.line,159',
'nonPassthroughDetails': [],
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'tax': 1.28,
'taxCalculated': 1.28,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxIncluded': False,
'taxableAmount': 15.0,
'transactionId': 0,
'vatCode': '',
'vatNumberTypeId': 0},
{'costInsuranceFreight': 0.0,
'customerUsageType': '',
'description': 'Invoicing',
'details': [{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'STA',
'jurisdictionType': 'State',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.06,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.9,
'reportingTaxCalculated': 0.9,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '',
'tax': 0.9,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.9,
'taxName': 'CA STATE TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'CTY',
'jurisdictionType': 'County',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0025,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.04,
'reportingTaxCalculated': 0.04,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '',
'tax': 0.04,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.04,
'taxName': 'CA COUNTY TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.0125,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.19,
'reportingTaxCalculated': 0.19,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '052',
'tax': 0.19,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.19,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'},
{'country': 'US',
'exemptAmount': 0.0,
'id': 0,
'isFee': False,
'isNonPassThru': False,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'STJ',
'jurisdictionType': 'Special',
'liabilityType': 'Seller',
'nonTaxableAmount': 0.0,
'rate': 0.01,
'rateType': 'General',
'rateTypeCode': 'G',
'region': 'CA',
'reportingExemptUnits': 0.0,
'reportingNonTaxableUnits': 0.0,
'reportingTax': 0.15,
'reportingTaxCalculated': 0.15,
'reportingTaxableUnits': 15.0,
'stateAssignedNo': '38',
'tax': 0.15,
'taxAuthorityTypeId': 45,
'taxCalculated': 0.15,
'taxName': 'CA SPECIAL TAX',
'taxSubTypeId': 'S',
'taxType': 'Sales',
'taxableAmount': 15.0,
'transactionId': 0,
'transactionLineId': 0,
'unitOfBasis': 'PerCurrencyUnit'}],
'discountAmount': 0.0,
'entityUseCode': '',
'exemptAmount': 0.0,
'exemptCertId': 0,
'exemptNo': '',
'hsCode': '',
'id': 0,
'isItemTaxable': True,
'itemCode': '',
'lineAmount': 15.0,
'lineNumber': 'sale.order.line,160',
'nonPassthroughDetails': [],
'quantity': 1.0,
'ref1': '',
'ref2': '',
'reportingDate': '2021-01-01',
'tax': 1.28,
'taxCalculated': 1.28,
'taxCode': 'DC010000',
'taxCodeId': 8575,
'taxDate': '2021-01-01',
'taxIncluded': False,
'taxableAmount': 15.0,
'transactionId': 0,
'vatCode': '',
'vatNumberTypeId': 0}],
'locationCode': '',
'locked': False,
'modifiedDate': '2021-12-13T18:38:19.2067334Z',
'modifiedUserId': 1452151,
'paymentDate': '2021-01-01',
'purchaseOrderNo': '',
'reconciled': False,
'referenceCode': 'S00084',
'reportingLocationCode': '',
'salespersonCode': '',
'status': 'Temporary',
'summary': [{'country': 'US',
'exemption': 0.0,
'jurisCode': '06',
'jurisName': 'CALIFORNIA',
'jurisType': 'State',
'nonTaxable': 0.0,
'rate': 0.06,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': 5.4,
'taxAuthorityType': 45,
'taxCalculated': 5.4,
'taxName': 'CA STATE TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': '075',
'jurisName': 'SAN FRANCISCO',
'jurisType': 'County',
'nonTaxable': 0.0,
'rate': 0.0025,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '',
'tax': 0.24,
'taxAuthorityType': 45,
'taxCalculated': 0.24,
'taxName': 'CA COUNTY TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': 'EMTV0',
'jurisName': 'SAN FRANCISCO CO LOCAL TAX SL',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.01,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '38',
'tax': 0.9,
'taxAuthorityType': 45,
'taxCalculated': 0.9,
'taxName': 'CA SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0},
{'country': 'US',
'exemption': 0.0,
'jurisCode': 'EMBE0',
'jurisName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
'jurisType': 'Special',
'nonTaxable': 0.0,
'rate': 0.0125,
'rateType': 'General',
'region': 'CA',
'stateAssignedNo': '052',
'tax': 1.14,
'taxAuthorityType': 45,
'taxCalculated': 1.14,
'taxName': 'CA SPECIAL TAX',
'taxSubType': 'S',
'taxType': 'Sales',
'taxable': 90.0}],
'taxDate': '2021-01-01',
'taxOverrideAmount': 0.0,
'taxOverrideReason': 'Manually changed the tax calculation date',
'taxOverrideType': 'TaxDate',
'totalAmount': 90.0,
'totalDiscount': 0.0,
'totalExempt': 0.0,
'totalTax': 7.68,
'totalTaxCalculated': 7.68,
'totalTaxable': 90.0,
'type': 'SalesOrder',
'version': 1}

View File

@ -0,0 +1,361 @@
from odoo import fields
from odoo.tests.common import tagged
from odoo.tools.misc import formatLang
from odoo.addons.odex30_account_avatax.tests.common import TestAccountAvataxCommon
from .mocked_so_response import generate_response
@tagged("-at_install", "post_install")
class TestSaleAvalara(TestAccountAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.tax_with_diff_amount = cls.env["account.tax"].create({
'name': 'CA COUNTY TAX [075] (0.2500 %)',
'company_id': cls.env.user.company_id.id,
'amount': 1,
'amount_type': 'percent',
})
cls.sales_user = cls.env['res.users'].create({
'name': 'Sales user',
'login': 'sales',
'email': 'sale_user@test.com',
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id, cls.env.ref('sales_team.group_sale_salesman').id])],
})
cls.env = cls.env(user=cls.sales_user)
cls.cr = cls.env.cr
return res
def assertOrder(self, order, mocked_response=None):
if mocked_response:
self.assertRecordValues(order, [{
'amount_total': 97.68,
'amount_untaxed': 90.0,
'amount_tax': 7.68,
}])
totals = order.tax_totals
subtotals = totals['subtotals']
self.assertEqual(len(subtotals), 1)
subtotal = subtotals[0]
self.assertEqual(subtotal['base_amount_currency'], order.amount_untaxed)
self.assertEqual(subtotal['tax_amount_currency'], order.amount_tax)
self.assertEqual(totals['total_amount_currency'], order.amount_total)
tax_groups = subtotal['tax_groups']
self.assertEqual(len(tax_groups), 1, "There should be one tax group on the invoice containing all taxes.")
self.assertEqual(tax_groups[0]['group_name'], 'Taxes')
for avatax_line in mocked_response['lines']:
so_line = order.order_line.filtered(lambda l: str(l.id) == avatax_line['lineNumber'].split(',')[1])
self.assertRecordValues(so_line, [{
'price_subtotal': avatax_line['taxableAmount'],
'price_tax': avatax_line['tax'],
'price_total': avatax_line['taxableAmount'] + avatax_line['tax'],
}])
else:
for line in order.order_line:
product_name = line.product_id.display_name
self.assertGreater(len(line.tax_id), 0, "Line with %s did not get any taxes set." % product_name)
self.assertGreater(order.amount_tax, 0.0, "Invoice has a tax_amount of 0.0.")
def _create_sale_order(self):
return self.env['sale.order'].create({
'user_id': self.sales_user.id,
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'order_line': [
(0, 0, {
'product_id': self.product_user.id,
'tax_id': None,
'price_unit': self.product_user.list_price,
}),
(0, 0, {
'product_id': self.product_user_discound.id,
'tax_id': None,
'price_unit': self.product_user_discound.list_price,
}),
(0, 0, {
'product_id': self.product_accounting.id,
'tax_id': None,
'price_unit': self.product_accounting.list_price,
}),
(0, 0, {
'product_id': self.product_expenses.id,
'tax_id': None,
'price_unit': self.product_expenses.list_price,
}),
(0, 0, {
'product_id': self.product_invoicing.id,
'tax_id': None,
'price_unit': self.product_invoicing.list_price,
}),
]
})
def test_compute_on_send(self):
order = self._create_sale_order()
mocked_response = generate_response(order.order_line)
with self._capture_request(return_value=mocked_response):
order.action_quotation_send()
self.assertOrder(order, mocked_response=mocked_response)
def test_01_odoo_sale_order(self):
order = self._create_sale_order()
mocked_response = generate_response(order.order_line)
with self._capture_request(return_value=mocked_response):
order.button_external_tax_calculation()
self.assertOrder(order, mocked_response=mocked_response)
def test_integration_01_odoo_sale_order(self):
with self._skip_no_credentials():
order = self._create_sale_order()
order.button_external_tax_calculation()
self.assertOrder(order)
def test_tax_round_globally(self):
self.env.company.sudo().tax_calculation_rounding_method = 'round_globally'
order = self.env['sale.order'].create({
'user_id': self.sales_user.id,
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'order_line': [
(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 1,
'price_unit': 1.48,
'tax_id': self.tax_with_diff_amount.ids,
}),
(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 1,
'price_unit': 1.48,
'tax_id': self.tax_with_diff_amount.ids,
}),
],
})
self.assertEqual(order.amount_total, 2.98)
def test_sale_order_downpayment(self):
order = self._create_sale_order()
mocked_response = generate_response(order.order_line)
with self._capture_request(return_value=mocked_response):
order.action_confirm()
downpayment_pct = 50
payment_ctx = {
"active_model": "sale.order",
"active_ids": [order.id],
"active_id": order.id,
}
wizard = (
self.env["sale.advance.payment.inv"]
.with_context(**payment_ctx)
.create({
'advance_payment_method': 'percentage',
'amount': downpayment_pct,
})
)
wizard.sudo().create_invoices()
downpayment_invoice = order.invoice_ids
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
downpayment_invoice.sudo().action_post()
self.assertIsNone(capture.val, "Shouldn't call Avatax when posting a down payment invoice.")
self.assertEqual(len(order.order_line.filtered(lambda line: not line.display_type)), 6, "Should have generated a new down payment line.")
self.assertFalse(order.order_line.filtered('is_downpayment').tax_id, "Down payment lines on the quotation shouldn't have taxes.")
self.assertAlmostEqual(downpayment_invoice.amount_total, order.amount_total * downpayment_pct / 100, msg="Down payment has the wrong amount.")
self.assertEqual(downpayment_invoice.amount_tax, 0, "Down payment shouldn't have taxes.")
wizard = (
self.env["sale.advance.payment.inv"]
.with_context(**payment_ctx)
.create({
'advance_payment_method': 'delivered',
})
)
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
wizard.sudo().create_invoices()
sent_lines = capture.val['json']['createTransactionModel']['lines']
self.assertEqual(len(sent_lines), 5, "Should send only the regular lines.")
@tagged("-at_install", "post_install")
class TestAccountAvalaraSalesTaxItemsIntegration(TestAccountAvataxCommon):
"""https://developer.avalara.com/certification/avatax/sales-tax-badge/"""
@classmethod
def setUpClass(cls):
res = super().setUpClass()
shipping_partner = cls.env["res.partner"].create({
'name': "Shipping Partner",
'street': "234 W 18th Ave",
'city': "Columbus",
'state_id': cls.env.ref("base.state_us_30").id, # Ohio
'country_id': cls.env.ref("base.us").id,
'zip': "43210",
})
with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture:
cls.sale_order = cls.env['sale.order'].create({
'partner_id': cls.partner.id,
'partner_shipping_id': shipping_partner.id,
'fiscal_position_id': cls.fp_avatax.id,
'date_order': '2021-01-01',
'order_line': [
(0, 0, {
'product_id': cls.product.id,
'tax_id': None,
'price_unit': cls.product.list_price,
}),
]
})
cls.sale_order.button_external_tax_calculation()
cls.captured_arguments = capture.val['json']['createTransactionModel']
return res
def test_item_code(self):
"""Identify customer code (number, ID) to pass to the AvaTax service."""
line_model, line_id = self.captured_arguments['lines'][0]['number'].split(',')
self.assertEqual(self.sale_order.order_line, self.env[line_model].browse(int(line_id)))
def test_item_description(self):
"""Identify item/service/charge description to pass to the AvaTax service with a
human-readable description or item name.
"""
line_description = self.captured_arguments['lines'][0]['description']
self.assertEqual(self.sale_order.order_line.name, line_description)
def test_tax_code_mapping(self):
tax_code = self.captured_arguments['lines'][0]['taxCode']
self.assertEqual(self.product.avatax_category_id.code, tax_code)
def test_doc_code(self):
"""Values that can come across to AvaTax as the DocCode."""
code = self.captured_arguments['code']
sent_so = self.env['sale.order'].search([('avatax_unique_code', '=', code)])
self.assertEqual(self.sale_order, sent_so)
def test_customer_code(self):
"""Values that can come across to AvaTax as the Customer Code."""
customer_code = self.captured_arguments['customerCode']
self.assertEqual(self.sale_order.partner_id.avalara_partner_code, customer_code)
def test_doc_date(self):
"""Value that comes across to AvaTax as the DocDate."""
doc_date = self.captured_arguments['date'] # didn't find anything with "DocDate"
self.assertEqual(self.sale_order.date_order.date(), fields.Date.to_date(doc_date))
def test_calculation_date(self):
"""Value that is used for Tax Calculation Date in AvaTax."""
tax_date = self.captured_arguments['taxOverride']['taxDate']
self.assertEqual(self.sale_order.date_order.date(), fields.Date.to_date(tax_date))
def test_doc_type(self):
"""DocType used for varying stages of the transaction life cycle."""
doc_type = self.captured_arguments['type']
self.assertEqual('SalesOrder', doc_type)
def test_header_level_destination_address(self):
"""Value that is sent to AvaTax for Destination Address at the header level."""
destination_address = self.captured_arguments['addresses']['shipTo']
self.assertEqual(destination_address, {
'city': 'Columbus',
'country': 'US',
'line1': '234 W 18th Ave',
'postalCode': '43210',
'region': 'OH',
})
def test_header_level_origin_address(self):
"""Value that is sent to AvaTax for Origin Address at the header level."""
origin_address = self.captured_arguments['addresses']['shipFrom']
self.assertEqual(origin_address, {
'city': 'San Francisco',
'country': 'US',
'line1': '250 Executive Park Blvd',
'postalCode': '94134',
'region': 'CA',
})
def test_quantity(self):
"""Value that is sent to AvaTax for the Quantity."""
quantity = self.captured_arguments['lines'][0]['quantity']
self.assertEqual(self.sale_order.order_line.product_uom_qty, quantity)
def test_amount(self):
"""Value that is sent to AvaTax for the Amount."""
amount = self.captured_arguments['lines'][0]['amount']
self.assertEqual(self.sale_order.order_line.price_subtotal, amount)
def test_tax_code(self):
"""Value that is sent to AvaTax for the Tax Code."""
tax_code = self.captured_arguments['lines'][0]['taxCode']
self.assertEqual(self.sale_order.order_line.product_id.avatax_category_id.code, tax_code)
def test_sales_order(self):
"""Ensure that invoices are processed through a logical document lifecycle."""
self.assertEqual(self.captured_arguments['type'], 'SalesOrder')
with self._capture_request({'lines': [], 'summary': []}) as capture:
self.sale_order.action_quotation_send()
self.sale_order.action_confirm()
invoice = self.sale_order._create_invoices()
invoice.button_external_tax_calculation()
self.assertEqual(capture.val['json']['createTransactionModel']['type'], 'SalesInvoice')
with self._capture_request({'lines': [], 'summary': []}) as capture:
invoice.action_post()
self.assertTrue(capture.val['json']['createTransactionModel']['commit'])
def test_commit_tax(self):
"""Ensure that invoices are committed/posted for reporting appropriately."""
with self._capture_request({'lines': [], 'summary': []}) as capture:
self.sale_order.action_quotation_send()
self.sale_order.action_confirm()
invoice = self.sale_order._create_invoices()
invoice.action_post()
self.assertTrue(capture.val['json']['createTransactionModel']['commit'])
def test_merge_sale_orders(self):
"""Ensure sale orders with different shipping partner are not merged
in the same invoice
"""
shipping_partner_b = self.env["res.partner"].create({
'name': "Shipping Partner B",
'street': "4557 De Silva St",
'city': "Freemont",
'state_id': self.env.ref("base.state_us_13").id,
'country_id': self.env.ref("base.us").id,
'zip': "94538",
})
with self._capture_request(return_value={'lines': [], 'summary': []}):
sale_order_b = self.env['sale.order'].create({
'partner_id': self.partner.id,
'partner_shipping_id': shipping_partner_b.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'order_line': [
(0, 0, {
'product_id': self.product.id,
'tax_id': None,
'price_unit': self.product.list_price,
}),
]
})
orders = self.sale_order | sale_order_b
orders.action_confirm()
orders._create_invoices()
self.assertEqual(len(orders.invoice_ids), 2, "Different invoices should be created")

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<group name="sales_person" position="inside">
<field name="is_avatax" invisible="1"/>
<field name="avatax_unique_code" invisible="not is_avatax"/>
</group>
</field>
</record>
</odoo>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_content" inherit_id="sale.sale_order_portal_content">
<xpath expr="//th[@id='taxes_header']" position="attributes">
<attribute name="t-if">not sale_order.fiscal_position_id.is_avatax</attribute>
</xpath>
<xpath expr="//td[@id='taxes']" position="attributes">
<attribute name="t-if">not sale_order.fiscal_position_id.is_avatax</attribute>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
{
'name': 'Avatax for Inventory',
'version': '1.0',
'website': 'http://exp-sa.com',
'author': 'Expert Co. Ltd.',
'description':"""
Inventory management for Avatax
=======================================
This module allows for line-level addresses when getting taxes from avatax.
A current limitation is a single order line with more than one stock move (i.e. 10 units of
product A, 2 shipped from warehouse #1 and 8 from warehouse #2). In this case the sale orders should be
split per delivery.
""",
'category': 'Accounting/Accounting',
'depends': ['odex30_account_avatax_sale', 'stock'],
'auto_install': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1,35 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_avatax_stock
#
# Translators:
# Wil Odoo, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
"Last-Translator: Wil Odoo, 2024\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: account_avatax_stock
#: model:ir.model,name:account_avatax_stock.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: account_avatax_stock
#: model:ir.model,name:account_avatax_stock.model_account_external_tax_mixin
msgid "Mixin to manage common parts of external tax calculation"
msgstr "Mixin لإدارة الأجزاء المشتركة لحساب الضرائب الخارجية "
#. module: account_avatax_stock
#: model:ir.model,name:account_avatax_stock.model_sale_order
msgid "Sales Order"
msgstr "أمر البيع"

View File

@ -0,0 +1,3 @@
from . import sale_order
from . import account_move
from . import account_external_tax_mixin

View File

@ -0,0 +1,22 @@
from odoo import models
class AccountExternalTaxMixin(models.AbstractModel):
_inherit = 'account.external.tax.mixin'
def _get_avatax_line_addresses(self, partner, warehouse_id):
res = {
'shipFrom': self._get_avatax_address_from_partner(warehouse_id.partner_id),
'shipTo': self._get_avatax_address_from_partner(partner),
}
return res
def _get_avatax_invoice_line(self, line_data):
res = super()._get_avatax_invoice_line(line_data)
warehouse = line_data['warehouse_id']
if warehouse and warehouse.partner_id != self.company_id.partner_id:
res['addresses'] = self._get_avatax_line_addresses(self._get_avatax_ship_to_partner(), warehouse)
return res

View File

@ -0,0 +1,13 @@
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _get_line_data_for_external_taxes(self):
res = super()._get_line_data_for_external_taxes()
for i, line in enumerate(self._get_lines_eligible_for_external_taxes()):
locations = line.sale_line_ids.move_ids.filtered(lambda move: move.state != 'cancel').location_id
shipping_addresses = locations.mapped(lambda loc: loc.warehouse_id.partner_id or loc.company_id.partner_id)
res[i]['warehouse_id'] = locations.warehouse_id[:1] if len(shipping_addresses) == 1 else None
return res

View File

@ -0,0 +1,11 @@
from odoo import models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _get_line_data_for_external_taxes(self):
res = super()._get_line_data_for_external_taxes()
for i, line in enumerate(self._get_lines_eligible_for_external_taxes()):
res[i]['warehouse_id'] = line.warehouse_id
return res

View File

@ -0,0 +1 @@
from . import test_avatax

View File

@ -0,0 +1,171 @@
from odoo import Command
from odoo.tests.common import tagged
from odoo.addons.odex30_account_avatax.tests.common import TestAccountAvataxCommon
@tagged("-at_install", "post_install")
class TestAccountAvalaraStock(TestAccountAvataxCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.shipping_partner = cls.env["res.partner"].create({
'name': "Shipping Partner",
'street': "234 W 18th Ave",
'city': "Columbus",
'state_id': cls.env.ref("base.state_us_30").id, # Ohio
'country_id': cls.env.ref("base.us").id,
'zip': "43210",
})
warehouse_address_partner = cls.env["res.partner"].create({
'name': "Address for second warehouse",
'street': "100 Ravine Lane NE",
'city': "Bainbridge Island",
'state_id': cls.env.ref("base.state_us_48").id, # Washington
'country_id': cls.env.ref("base.us").id,
'zip': "98110",
})
cls.warehouse_with_different_address = cls.env['stock.warehouse'].create({
'name': "Warehouse #2",
'partner_id': warehouse_address_partner.id,
'code': "WH02"
})
cls.warehouse_with_same_address = cls.env['stock.warehouse'].create({
'name': "Warehouse #3",
'partner_id': cls.env.user.company_id.partner_id.id,
'code': "WH03"
})
return res
def test_line_level_address_sale_order_warehouse(self):
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'partner_shipping_id': self.shipping_partner.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'warehouse_id': self.warehouse_with_different_address.id,
'order_line': [
(0, 0, {
'product_id': self.product.id,
'tax_id': None,
'price_unit': self.product.list_price,
}),
]
})
sale_order.button_external_tax_calculation()
line_addresses = capture.val['json']['createTransactionModel']['lines'][0].get('addresses', False)
self.assertEqual(line_addresses['shipFrom']['region'], 'WA', 'should ship from the sales order warehouse')
self.assertEqual(line_addresses['shipTo']['region'], 'OH', 'should ship to the delivery address')
capture.val = None
sale_order.action_confirm()
line_addresses = capture.val['json']['createTransactionModel']['lines'][0].get('addresses', False)
self.assertEqual(line_addresses['shipFrom']['region'], 'WA', 'should ship from the sales order warehouse')
self.assertEqual(line_addresses['shipTo']['region'], 'OH', 'should ship to the delivery address')
capture.val = None
invoice = sale_order._create_invoices()
invoice.button_external_tax_calculation()
line_addresses = capture.val['json']['createTransactionModel']['lines'][0].get('addresses', False)
self.assertEqual(line_addresses['shipFrom']['region'], 'WA', 'should ship from the sales order warehouse')
self.assertEqual(line_addresses['shipTo']['region'], 'OH', 'should ship to the delivery address')
def test_line_level_address_with_different_warehouse_address(self):
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'partner_shipping_id': self.shipping_partner.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'order_line': [
(0, 0, {
'product_id': self.product.id,
'tax_id': None,
'price_unit': self.product.list_price,
}),
(0, 0, {
'product_id': self.product_user.id,
'tax_id': None,
'price_unit': self.product_user.list_price,
}),
(0, 0, {
'product_id': self.product_accounting.id,
'tax_id': None,
'price_unit': self.product_accounting.list_price,
}),
]
})
sale_order.action_confirm()
self.assertEqual(len(sale_order.picking_ids.move_ids), 3, "Three stock moves should be created from the sale order.")
move01 = sale_order.picking_ids.move_ids[0]
move01.location_id = self.warehouse_with_different_address.lot_stock_id
move02 = sale_order.picking_ids.move_ids[1]
move02.location_id = self.warehouse_with_same_address.lot_stock_id
invoice = sale_order._create_invoices()
invoice.button_external_tax_calculation()
line_addresses = capture.val['json']['createTransactionModel']['lines'][0].get('addresses', False)
self.assertTrue(line_addresses, "Line level addresses should be created for different warehouse addresses.")
self.assertEqual(line_addresses, {
'shipFrom': {
'city': 'Bainbridge Island',
'country': 'US',
'line1': '100 Ravine Lane NE',
'postalCode': '98110',
'region': 'WA'
},
'shipTo': {
'city': 'Columbus',
'country': 'US',
'line1': '234 W 18th Ave',
'postalCode': '43210',
'region': 'OH'
}}, "Line level address should have the correct shipForm and shipTo")
# Line 2
line_addresses = capture.val['json']['createTransactionModel']['lines'][1].get('addresses', False)
self.assertFalse(line_addresses, "Line level addresses should not be created for a warehouse with the same address as the company.")
# Line 3
line_addresses = capture.val['json']['createTransactionModel']['lines'][2].get('addresses', False)
self.assertFalse(line_addresses, "Line level addresses should not be created for a warehouse with the same address as the company.")
def test_line_level_address_with_backorders(self):
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'partner_shipping_id': self.shipping_partner.id,
'fiscal_position_id': self.fp_avatax.id,
'date_order': '2021-01-01',
'warehouse_id': self.warehouse_with_different_address.id,
'order_line': [
Command.create({
'product_id': self.product.id,
'tax_id': None,
'price_unit': self.product.list_price,
'product_uom_qty': 2,
}),
]
})
sale_order.action_confirm()
sale_order.picking_ids.move_ids.quantity = 1 # do half
# validate and create backorder
res_dict = sale_order.picking_ids.button_validate()
self.env['stock.backorder.confirmation'].with_context(res_dict['context']).process()
self.assertEqual(len(sale_order.picking_ids), 2, "There should be two pickings: the original one for qty 1 and the backorder for the remaining qty 1.")
self.assertEqual(len(sale_order.order_line.move_ids), 2, "There should be two moves associated to this single order line, one for the original picking, the other for the backorder.")
invoice = sale_order._create_invoices()
invoice.button_external_tax_calculation()
line_address = capture.val['json']['createTransactionModel']['lines'][0].get('addresses')
self.assertTrue(line_address, "Send a line level address, even though two moves are associated to the line, they both refer to the same address.")

View File

@ -0,0 +1,2 @@
from . import controllers
from . import models

View File

@ -0,0 +1,13 @@
{
'name': 'Account Bank Statement Extract',
'category': 'Accounting/Accounting',
'version': '1.0',
'depends': ['accountant', 'odex30_account_bank_statement_import', 'iap_extract', 'odex30_account_extract'],
'summary': 'Extract data from bank statement scans to fill them automatically',
'data': [
'views/res_config_settings_views.xml',
'views/account_bank_statement_views.xml',
],
'auto_install': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,14 @@
from odoo import http
from odoo.http import request
class AccountBankStatementExtractController(http.Controller):
@http.route('/odex30_account_bank_statement_extract/request_done/<string:extract_document_uuid>', type='http', auth='public', csrf=False)
def request_done(self, extract_document_uuid):
statements_to_update = request.env['account.bank.statement'].sudo().search([
('extract_document_uuid', '=', extract_document_uuid),
('extract_state', 'in', ['waiting_extraction', 'extract_not_ready']),
('is_in_extractable_state', '=', True)])
for statement in statements_to_update:
statement._check_ocr_status()
return 'OK'

View File

@ -0,0 +1,211 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_bank_statement_extract
#
# Translators:
# Wil Odoo, 2024
# Malaz Abuidris <msea@odoo.com>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-27 13:55+0000\n"
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
"Last-Translator: Malaz Abuidris <msea@odoo.com>, 2025\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_needaction
msgid "Action Needed"
msgstr "إجراء مطلوب"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_attachment_count
msgid "Attachment Count"
msgstr "عدد المرفقات"
#. module: odex30_account_bank_statement_extract
#: model:ir.model,name:odex30_account_bank_statement_extract.model_account_bank_statement
msgid "Bank Statement"
msgstr "كشف الحساب البنكي "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_res_config_settings__extract_bank_statement_digitalization_mode
msgid "Bank Statements"
msgstr "كشوفات الحساب البنكية"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_can_show_send_button
msgid "Can show the ocr send button"
msgstr "بإمكانه عرض زر إرسال ملف بتمييز الرموز ضوئياً "
#. module: odex30_account_bank_statement_extract
#: model:ir.model,name:odex30_account_bank_statement_extract.model_res_company
msgid "Companies"
msgstr "الشركات"
#. module: odex30_account_bank_statement_extract
#: model:ir.model,name:odex30_account_bank_statement_extract.model_res_config_settings
msgid "Config Settings"
msgstr "تهيئة الإعدادات "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_res_company__extract_bank_statement_digitalization_mode
msgid "Digitization mode on bank statements"
msgstr "وضع الرقمنة في كشوفات الحساب البنكية "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields.selection,name:odex30_account_bank_statement_extract.selection__res_company__extract_bank_statement_digitalization_mode__auto_send
msgid "Digitize automatically"
msgstr "الرقمنة تلقائياً "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields.selection,name:odex30_account_bank_statement_extract.selection__res_company__extract_bank_statement_digitalization_mode__no_send
msgid "Do not digitize"
msgstr "لا تقم بالرقمنة"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_error_message
msgid "Error message"
msgstr "رسالة خطأ"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_state_processed
msgid "Extract State Processed"
msgstr "تمت معالجة حالة الاستخلاص "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_state
msgid "Extract state"
msgstr "استخلاص حالة "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_status
msgid "Extract status"
msgstr "حالة الاستخلاص "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_follower_ids
msgid "Followers"
msgstr "المتابعين"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_partner_ids
msgid "Followers (Partners)"
msgstr "المتابعين (الشركاء) "
#. module: odex30_account_bank_statement_extract
#. odoo-python
#: code:addons/odex30_account_bank_statement_extract/models/account_journal.py:0
msgid "Generated Bank Statements"
msgstr "كشوفات الحسابات البنكية المنشأة "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__has_message
msgid "Has Message"
msgstr "يحتوي على رسالة "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__extract_document_uuid
msgid "ID of the request to IAP-OCR"
msgstr "مُعرف طلب IAP-OCR"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__message_needaction
msgid "If checked, new messages require your attention."
msgstr "إذا كان محددًا، فهناك رسائل جديدة عليك رؤيتها. "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_error
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr "إذا كان محددًا، فقد حدث خطأ في تسليم بعض الرسائل."
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_is_follower
msgid "Is Follower"
msgstr "متابع"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__is_in_extractable_state
msgid "Is In Extractable State"
msgstr "في حالة قابلة للاستخلاص "
#. module: odex30_account_bank_statement_extract
#: model:ir.model,name:odex30_account_bank_statement_extract.model_account_journal
msgid "Journal"
msgstr "دفتر اليومية"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_main_attachment_id
msgid "Main Attachment"
msgstr "المرفق الرئيسي"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_error
msgid "Message Delivery error"
msgstr "خطأ في تسليم الرسائل"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_ids
msgid "Messages"
msgstr "الرسائل"
#. module: odex30_account_bank_statement_extract
#. odoo-python
#: code:addons/odex30_account_bank_statement_extract/models/account_journal.py:0
msgid "Mixing PDF/Image files with other file types is not allowed."
msgstr "لا يُسمح بخلط ملفات PDF/صور مع أنواع ملفات أخرى. "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_needaction_counter
msgid "Number of Actions"
msgstr "عدد الإجراءات"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_error_counter
msgid "Number of errors"
msgstr "عدد الأخطاء "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "عدد الرسائل التي تتطلب اتخاذ إجراء"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr "عدد الرسائل الحادث بها خطأ في التسليم"
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__rating_ids
msgid "Ratings"
msgstr "التقييمات "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__message_has_sms_error
msgid "SMS Delivery error"
msgstr "خطأ في تسليم الرسائل النصية القصيرة "
#. module: odex30_account_bank_statement_extract
#. odoo-python
#: code:addons/odex30_account_bank_statement_extract/models/account_bank_statement.py:0
msgid ""
"Statement and transactions have been updated using Artificial Intelligence."
msgstr "تم تحديث كشوف الحسابات والمعاملات باستخدام الذكاء الاصطناعي. "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,field_description:odex30_account_bank_statement_extract.field_account_bank_statement__website_message_ids
msgid "Website Messages"
msgstr "رسائل الموقع الإلكتروني "
#. module: odex30_account_bank_statement_extract
#: model:ir.model.fields,help:odex30_account_bank_statement_extract.field_account_bank_statement__website_message_ids
msgid "Website communication history"
msgstr "سجل تواصل الموقع الإلكتروني "

View File

@ -0,0 +1,4 @@
from . import account_bank_statement
from . import account_journal
from . import res_company
from . import res_config_settings

View File

@ -0,0 +1,70 @@
from odoo import api, fields, models, Command, _
from odoo.addons.iap.tools import iap_tools
OCR_VERSION = 100
class AccountBankStatement(models.Model):
_name = 'account.bank.statement'
_inherit = ['extract.mixin', 'account.bank.statement']
@api.depends('line_ids')
def _compute_is_in_extractable_state(self):
self.is_in_extractable_state = not self.line_ids
def _compute_journal_id(self):
if self.line_ids:
super()._compute_journal_id()
def _get_ocr_option_can_extract(self):
ocr_option = self.env.company.extract_bank_statement_digitalization_mode
return ocr_option and ocr_option != 'no_send'
def _get_ocr_module_name(self):
return 'odex30_account_bank_statement_extract'
def _get_user_infos(self):
user_infos = super()._get_user_infos()
user_infos['journal_type'] = self.journal_id.type
return user_infos
def _contact_iap_extract(self, pathinfo, params):
params['version'] = OCR_VERSION
params['account_token'] = self._get_iap_account().account_token
endpoint = self.env['ir.config_parameter'].sudo().get_param('iap_extract_endpoint', 'https://extract.api.odoo.com')
return iap_tools.iap_jsonrpc(endpoint + '/api/extract/bank_statement/1/' + pathinfo, params=params)
def _fill_document_with_results(self, ocr_results):
self.ensure_one()
balance_start_ocr = self._get_ocr_selected_value(ocr_results, 'balance_start', 0.0)
balance_end_ocr = self._get_ocr_selected_value(ocr_results, 'balance_end', 0.0)
date_ocr = self._get_ocr_selected_value(ocr_results, 'date', "")
lines_ocr = ocr_results.get('bank_statement_lines', [])
self.balance_start = balance_start_ocr
self.balance_end = balance_end_ocr
self.date = date_ocr
self._compute_name()
self.line_ids = [Command.create({
'amount': line['amount'],
'date': line['date'],
'journal_id': self.journal_id.id,
'payment_ref': line['description'],
}) for line in lines_ocr]
odoobot = self.env.ref('base.partner_root')
self.message_post(
body=_("Statement and transactions have been updated using Artificial Intelligence."),
author_id=odoobot.id
)
self.env.ref('odex30_account_accountant.auto_reconcile_bank_statement_line')._trigger()
def _message_set_main_attachment_id(self, attachments, force=False, filter_xml=True):
res = super()._message_set_main_attachment_id(attachments, force=force, filter_xml=filter_xml)
self._autosend_for_digitization()
return res
def _autosend_for_digitization(self):
if self.env.company.extract_bank_statement_digitalization_mode == 'auto_send':
self.filtered('extract_can_show_send_button')._send_batch_for_digitization()

Some files were not shown because too many files have changed in this diff Show More