new_avatax
This commit is contained in:
parent
938230b402
commit
c7fe69f83d
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "تمت إزالة المدفوعات من الدفعة. "
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
|
@ -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)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
from . import test_batch_payment
|
||||
from . import test_bank_rec_widget
|
||||
from . import test_bank_rec_widget_tour
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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})
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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/"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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.",
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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}'
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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},
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from . import test_avatax
|
||||
|
||||
|
||||
from . import (
|
||||
test_address_validation,
|
||||
|
||||
test_refunds,
|
||||
test_use_tax,
|
||||
test_vat,
|
||||
test_avatax_unique_code,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)])
|
||||
|
|
@ -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')
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import avatax_validate_address
|
||||
from . import avatax_connection_test_result
|
||||
|
|
@ -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')
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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 "
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import res_partner
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import avatax_validate_address
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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. "
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import sale_order
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_avatax
|
||||
|
|
@ -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}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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 "أمر البيع"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import sale_order
|
||||
from . import account_move
|
||||
from . import account_external_tax_mixin
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_avatax
|
||||
|
|
@ -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.")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -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'
|
||||
|
|
@ -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 "سجل تواصل الموقع الإلكتروني "
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import account_bank_statement
|
||||
from . import account_journal
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
|
|
@ -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
Loading…
Reference in New Issue