Merge remote-tracking branch 'source_origin/dev_odex30_accounting' into dev_odex30_accounting

This commit is contained in:
maltayyar2 2026-01-25 14:27:07 +03:00
commit ab76ca2aff
493 changed files with 53439 additions and 2118 deletions

View File

@ -41,11 +41,14 @@
'account_chart_of_accounts/static/src/js/account_type_selection_extend.js',
'account_chart_of_accounts/static/src/js/filters_patch.js',
'account_chart_of_accounts/static/src/js/account_report.js',
'account_chart_of_accounts/static/src/js/account_list_renderer.js',
'account_chart_of_accounts/static/src/js/search_panel_bold.js',
'account_chart_of_accounts/static/src/xml/filter_full_hierarchy.xml',
],
'web.assets_frontend': [
# CRITICAL: SCSS must be in backend, not frontend!
'account_chart_of_accounts/static/src/scss/account_hierarchy.scss',
],
},
'installable': True,

View File

@ -0,0 +1,317 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_chart_of_accounts
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-18 03:36+0000\n"
"PO-Revision-Date: 2026-01-18 03:36+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.view_account_form_inherit_hierarchy
msgid ""
"<span class=\"badge text-bg-success ms-2\" invisible=\"not automticAccountsCodes\" title=\"Code is auto-generated\">\n"
" Auto\n"
" </span>"
msgstr ""
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_account_account
msgid "Account"
msgstr "الحساب "
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Account Code Padding"
msgstr "حشو كود الحساب"
#. module: account_chart_of_accounts
#: model:ir.model.fields,help:account_chart_of_accounts.field_account_account__account_type
msgid ""
"Account Type is used for information purpose, to generate country-specific "
"legal reports, and set the rules to close a fiscal year and generate opening"
" entries."
msgstr ""
"يُستخدم نوع الحساب في إنشاء التقارير القانونية المخصصة لكل دولة، وتضع "
"القواعد اللازمة لإقفال السنة المالية وإنشاء القيود الافتتاحية للسنة المالية "
"اللاحقة. "
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_account_report
msgid "Accounting Report"
msgstr "تقرير المحاسبة "
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Assets"
msgstr "الأصول"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__auto_code
msgid "Auto Code"
msgstr "كود آلي"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Auto Generate Account Codes"
msgstr "توليد أكواد الحسابات تلقائياً"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__automticAccountsCodes
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_company__automticAccountsCodes
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__automticAccountsCodes
msgid "Automatically Generate Accounts Codes"
msgstr "توليد أكواد الحسابات تلقائياً"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Automatically generate account codes based on parent account"
msgstr "توليد أكواد الحسابات تلقائياً بناءً على الحساب الأب"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Balance Sheet"
msgstr "الميزانية العمومية"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Bank Account Prefix"
msgstr "بادئة حساب البنك"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__bank_account_code_prefix
msgid "Bank Prefix"
msgstr "بادئة البنك"
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/account_journal.py:0
msgid "Can not find account with code (%s)."
msgstr "لا يمكن إيجاد حساب بالكود (%s)."
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/account_journal.py:0
msgid ""
"Cannot generate an unused journal code. Please fill the 'Shortcode' field."
msgstr "لا يمكن توليد كود دفتر يومية غير مستخدم. يرجى ملء حقل 'الرمز المختصر'."
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Cash Account Prefix"
msgstr "بادئة حساب النقدية"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__cash_account_code_prefix
msgid "Cash Prefix"
msgstr "بادئة النقدية"
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/res_config_settings.py:0
msgid "Chart of Accounts"
msgstr "دليل الحسابات"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Chart of Accounts Hierarchy"
msgstr "التسلسل الهرمي لدليل الحسابات"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_company__chart_account_padding
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__chart_account_padding
msgid "Chart of accounts Padding"
msgstr "حشو دليل الحسابات"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_company__chart_account_length
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__chart_account_length
msgid "Chart of accounts length"
msgstr "طول دليل الحسابات"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__child_ids
msgid "Child Accounts"
msgstr "الحسابات الفرعية"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.view_account_form_inherit_hierarchy
msgid "Code"
msgstr "الكود"
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_res_company
msgid "Companies"
msgstr "الشركات"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__company_id
msgid "Company"
msgstr "الشركة"
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_res_config_settings
msgid "Config Settings"
msgstr "تهيئة الإعدادات "
#. module: account_chart_of_accounts
#: model:ir.model.fields,help:account_chart_of_accounts.field_account_report__filter_show_full_hierarchy
msgid "Enable filter to display accounts in full hierarchical structure"
msgstr "تفعيل الفلتر لعرض الحسابات في هيكل هرمي كامل"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Equity"
msgstr "حقوق الملكية"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Expense"
msgstr "المصروفات"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/xml/filter_full_hierarchy.xml:0
msgid "Full Hierarchy"
msgstr "التسلسل الهرمي الكامل"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.view_account_search_inherit
msgid "Hierarchical Chart"
msgstr "الدليل الهرمي"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Income"
msgstr "الإيرادات"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__internal_group
msgid "Internal Group"
msgstr "مجموعة داخلية"
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_account_journal
msgid "Journal"
msgstr "دفتر اليومية"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__level
msgid "Level"
msgstr "المستوى"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Liabilities"
msgstr "الخصوم"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Number of digits for account code padding"
msgstr "عدد الأرقام لحشو كود الحساب"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Other"
msgstr "أخرى"
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/account_journal.py:0
msgid "Outstanding Payments"
msgstr "مدفوعات معلقة"
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/account_journal.py:0
msgid "Outstanding Receipts"
msgstr "مقبوضات معلقة"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__parent_id
msgid "Parent Account"
msgstr "الحساب الأب"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_company__parent_bank_cash_account_id
msgid "Parent Bank Cash Account"
msgstr "حساب البنك النقدي الأب"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__parent_path
msgid "Parent Path"
msgstr "المسار الأب"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Prefix for bank account codes"
msgstr "بادئة لأكواد حسابات البنك"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Prefix for cash account codes"
msgstr "بادئة لأكواد حسابات النقدية"
#. module: account_chart_of_accounts
#. odoo-javascript
#: code:addons/account_chart_of_accounts/static/src/js/account_type_selection_extend.js:0
msgid "Profit & Loss"
msgstr "الأرباح والخسائر"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_report__filter_show_full_hierarchy
msgid "Show Full Hierarchy Filter"
msgstr "عرض فلتر التسلسل الهرمي الكامل"
#. module: account_chart_of_accounts
#. odoo-python
#: code:addons/account_chart_of_accounts/models/account_account.py:0
msgid "This account level is greater than the chart of account length."
msgstr "مستوى هذا الحساب أكبر من طول دليل الحسابات."
#. module: account_chart_of_accounts
#: model:ir.model,name:account_chart_of_accounts.model_account_trial_balance_report_handler
msgid "Trial Balance Custom Handler"
msgstr "معالج مخصص لميزان المراجعة "
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_account_account__account_type
msgid "Type"
msgstr "النوع"
#. module: account_chart_of_accounts
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_company__useFiexedTree
#: model:ir.model.fields,field_description:account_chart_of_accounts.field_res_config_settings__useFiexedTree
msgid "Use Fixed Length Chart of accounts"
msgstr "استخدام دليل حسابات بطول ثابت"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.res_config_settings_view_form_inherit
msgid "Use fixed length chart of accounts structure"
msgstr "استخدام هيكل دليل حسابات بطول ثابت"
#. module: account_chart_of_accounts
#: model:ir.model.fields.selection,name:account_chart_of_accounts.selection__account_account__account_type__view
#: model:ir.model.fields.selection,name:account_chart_of_accounts.selection__account_account__internal_group__view
msgid "View"
msgstr "إجمالي"
#. module: account_chart_of_accounts
#: model_terms:ir.ui.view,arch_db:account_chart_of_accounts.view_account_form_inherit_hierarchy
msgid "e.g. 101000"
msgstr "مثال: 101000"

View File

@ -139,49 +139,56 @@ class AccountAccount(models.Model):
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""
Override to show hierarchical accounts in search panel.
Root level: Only show view accounts with children
Sub-levels: Show all children when parent is expanded
"""
if field_name != 'parent_id':
return super().search_panel_select_range(field_name, **kwargs)
enable_counters = kwargs.get('enable_counters', False)
search_domain = kwargs.get('search_domain', [])
# Get ALL accounts for hierarchy
all_accounts = self.search([], order='code')
all_view_accounts = self.search([('account_type', '=', 'view')], order='code')
# Filter accounts to show:
# - Root level (parent_id = False): ONLY view accounts WITH children
# - Sub levels: ALL accounts (shown when parent is expanded)
accounts_to_show = all_accounts.filtered(
lambda a: (not a.parent_id and a.account_type == 'view' and a.child_ids) or a.parent_id
)
count_by_parent = {}
count_by_account = {}
if enable_counters:
all_accounts = self.search(search_domain)
for account in all_accounts:
if account.parent_id:
parent_id = account.parent_id.id
count_by_parent[parent_id] = count_by_parent.get(parent_id, 0) + 1
ancestor = account.parent_id.parent_id
while ancestor:
count_by_parent[ancestor.id] = count_by_parent.get(ancestor.id, 0) + 1
ancestor = ancestor.parent_id
# Count records that match the search domain for each account
filtered_accounts = self.search(search_domain)
for account in filtered_accounts:
# ✅ ONLY count in parent (and ancestors), not self
parent = account.parent_id
while parent:
count_by_account[parent.id] = count_by_account.get(parent.id, 0) + 1
parent = parent.parent_id
values = []
for parent in all_view_accounts:
for account in accounts_to_show:
value = {
'id': parent.id,
'display_name': f"{parent.code} {parent.name}" if parent.code else parent.name,
'parent_id': parent.parent_id.id if parent.parent_id else False,
'id': account.id,
'display_name': f"{account.code} {account.name}" if account.code else account.name,
'parent_id': account.parent_id.id if account.parent_id else False,
}
if enable_counters:
value['__count'] = count_by_parent.get(parent.id, 0)
value['__count'] = count_by_account.get(account.id, 0)
values.append(value)
result = {
'parent_field': 'parent_id',
'values': values,
}
return result

View File

@ -0,0 +1,31 @@
/** @odoo-module **/
import { ListRenderer } from "@web/views/list/list_renderer";
import { patch } from "@web/core/utils/patch";
patch(ListRenderer.prototype, {
/**
* Add custom class to rows with account_type='view' to make them bold
*/
setup() {
super.setup(...arguments);
},
async onCellClicked(record, column, ev) {
await super.onCellClicked(...arguments);
},
getRowClass(record) {
const classes = super.getRowClass(record) || "";
// Add bold class if account_type is 'view'
if (this.props.list.resModel === 'account.account') {
const accountType = record.data.account_type;
if (accountType === 'view') {
return `${classes} fw-bold account-view-type`;
}
}
return classes;
}
});

View File

@ -0,0 +1,61 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { SearchPanel } from "@web/search/search_panel/search_panel";
patch(SearchPanel.prototype, {
/**
* Override to add bold class to view accounts with children in search panel
*/
setup() {
super.setup(...arguments);
},
/**
* After rendering, add bold class to accounts with toggle button
*/
async onMounted() {
if (super.onMounted) {
await super.onMounted(...arguments);
}
this.makeBoldAccountsWithChildren();
},
/**
* Make accounts with children (that have toggle button) bold
*/
makeBoldAccountsWithChildren() {
// CRITICAL: HTML uses class "account_root" not "account_account"
const searchPanel = document.querySelector('.o_search_panel.account_root');
if (!searchPanel) return;
// Find ALL category values
const allItems = searchPanel.querySelectorAll('.o_search_panel_category_value');
allItems.forEach(item => {
// Check if this item has a toggle button (means it has children)
const toggleButton = item.querySelector('.o_toggle_fold');
// Only make bold if it has a toggle button (has children)
if (toggleButton && toggleButton.querySelector('i.fa-caret-down, i.fa-caret-right')) {
// Add bold class
item.classList.add('has-children-bold');
// Also add to header and label
const header = item.querySelector('header');
const label = item.querySelector('.o_search_panel_label_title');
if (header) header.classList.add('has-children-bold');
if (label) label.classList.add('has-children-bold');
} else {
// Remove bold class if it doesn't have children
item.classList.remove('has-children-bold');
const header = item.querySelector('header');
const label = item.querySelector('.o_search_panel_label_title');
if (header) header.classList.remove('has-children-bold');
if (label) label.classList.remove('has-children-bold');
}
});
},
});

View File

@ -1,5 +1,6 @@
.o_search_panel.account_account {
.o_search_panel_field.parent_id {
// CRITICAL: HTML uses class "account_root" not "account_account"
.o_search_panel.account_root {
.o_search_panel_field {
.o_search_panel_category_value {
.o_toggle_fold {
margin-right: 15px;
@ -11,9 +12,9 @@
align-items: center;
padding: 3px 8px;
&:hover {
color: #f0f0f0;
}
// &:hover {
// color: #f0f0f0;
// }
.o_search_panel_label_title {
flex: 1;
@ -25,15 +26,64 @@
}
}
// Levels
&[data-level="0"] {
font-weight: bold;
// ONLY use JavaScript-added class (not :has() selector)
// Only accounts with actual toggle buttons get this class
&.has-children-bold {
font-weight: 900 !important;
header,
.o_search_panel_label_title {
font-weight: 900 !important;
}
}
&[data-level="1"] header { padding-left: 20px; }
&[data-level="2"] header { padding-left: 35px; }
&[data-level="3"] header { padding-left: 50px; }
&[data-level="4"] header { padding-left: 65px; }
// Levels indentation
&[data-level="1"] header {
padding-left: 20px;
}
&[data-level="2"] header {
padding-left: 35px;
}
&[data-level="3"] header {
padding-left: 50px;
}
&[data-level="4"] header {
padding-left: 65px;
}
&[data-level="5"] header {
padding-left: 80px;
}
&[data-level="6"] header {
padding-left: 95px;
}
}
}
}
// Make view accounts bold - using custom class from JavaScript
.o_list_view {
tr.o_data_row.account-view-type,
tr.o_data_row.fw-bold {
font-weight: 900 !important;
td.o_data_cell {
font-weight: 900 !important;
// Add text shadow to make it appear even bolder
// Slightly darker color for more contrast
color: #2c3e50 !important;
}
// Optional: Add background color like Odoo 14
// Uncomment the lines below if you want a background color
// background-color: #f5f5f5 !important;
// &:hover {
// background-color: #ebebeb !important;
// }
}
}

View File

@ -75,6 +75,11 @@
<field name="code" position="after">
<field name="parent_id" optional="hide"/>
</field>
<!-- Ensure account_type is visible in DOM for CSS targeting -->
<field name="account_type" position="attributes">
<attribute name="class">account_type_field</attribute>
</field>
</field>
</record>

View File

@ -0,0 +1,67 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from . import models
_logger = logging.getLogger(__name__)
def _accounting_post_init(env):
country_code = env.company.country_id.code
if country_code:
module_list = []
if country_code in ('AU', 'CA', 'US'):
module_list.append('account_reports_cash_basis')
module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
if module_ids:
module_ids.sudo().button_install()
def uninstall_hook(env):
try:
group_user = env.ref("account.group_account_user")
group_user.write({
'name': "Show Full Accounting Features",
'implied_ids': [(3, env.ref('account.group_account_invoice').id)],
'category_id': env.ref("base.module_category_hidden").id,
})
group_readonly = env.ref("account.group_account_readonly")
group_readonly.write({
'name': "Show Full Accounting Features - Readonly",
'category_id': env.ref("base.module_category_hidden").id,
})
except ValueError as e:
_logger.warning(e)
try:
group_manager = env.ref("account.group_account_manager")
group_manager.write({'name': "Billing Manager",
'implied_ids': [(4, env.ref("account.group_account_invoice").id),
(3, env.ref("account.group_account_readonly").id),
(3, env.ref("account.group_account_user").id)]})
except ValueError as e:
_logger.warning(e)
# make the account_accountant features disappear (magic)
env.ref("account.group_account_user").write({'users': [(5, False, False)]})
env.ref("account.group_account_readonly").write({'users': [(5, False, False)]})
# this menu should always be there, as the module depends on account.
# if it's not, there is something wrong with the db that should be investigated.
invoicing_menu = env.ref("account.menu_finance")
menus_to_move = [
"account.menu_finance_receivables",
"account.menu_finance_payables",
"account.menu_finance_entries",
"account.menu_finance_reports",
"account.menu_finance_configuration",
"account.menu_board_journal_1",
]
for menu_xmlids in menus_to_move:
try:
env.ref(menu_xmlids).parent_id = invoicing_menu
except ValueError as e:
_logger.warning(e)

View File

@ -0,0 +1,37 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Accounting',
'version': '1.1',
'category': 'Accounting/Accounting',
'sequence': 30,
'summary': 'Manage financial and analytic accounting',
'description': """
Accounting Access Rights
========================
It gives the Administrator user access to all accounting features such as journal items and the chart of accounts.
It assigns manager and user access rights to the Administrator for the accounting application and only user rights to the Demo user.
""",
'website': 'https://www.odoo.com/app/accounting',
'depends': ['odex30_account_accountant'],
'data': [
'data/account_accountant_data.xml',
'security/accounting_security.xml',
'views/res_config_settings.xml',
'views/partner_views.xml',
],
'demo': ['demo/account_accountant_demo.xml'],
'installable': True,
'application': True,
'post_init_hook': '_accounting_post_init',
'uninstall_hook': "uninstall_hook",
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'accountant/static/src/js/tours/accountant.js',
],
'web.assets_tests': [
'accountant/static/tests/tours/*',
],
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Switch root menu "Invoicing" to "Accounting" -->
<!-- Top menu item -->
<menuitem name="Accounting"
id="menu_accounting"
groups="account.group_account_readonly,account.group_account_invoice"
web_icon="accountant,static/description/icon.png"
sequence="60"/>
<!-- move existing submenus to point to the new parent -->
<record id="account.menu_finance_receivables" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_payables" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_entries" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_reports" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_configuration" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_board_journal_1" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<menuitem id="account.menu_account_config" name="Settings" parent="account.menu_finance_configuration" sequence="0" groups="base.group_system"/>
</odoo>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,28 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * accountant
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-19 09:51+0000\n"
"PO-Revision-Date: 2024-12-19 09:51+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: accountant
#: model:ir.ui.menu,name:accountant.menu_accounting
#: model_terms:ir.ui.view,arch_db:accountant.res_config_settings_view_form
#: model_terms:ir.ui.view,arch_db:accountant.res_partner_view_form
msgid "Accounting"
msgstr ""
#. module: accountant
#: model:ir.model,name:accountant.model_account_move
msgid "Journal Entry"
msgstr ""

View File

@ -0,0 +1,32 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * accountant
#
# 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:51+0000\n"
"PO-Revision-Date: 2024-09-25 09:44+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: accountant
#: model:ir.ui.menu,name:accountant.menu_accounting
#: model_terms:ir.ui.view,arch_db:accountant.res_config_settings_view_form
#: model_terms:ir.ui.view,arch_db:accountant.res_partner_view_form
msgid "Accounting"
msgstr "المحاسبة "
#. module: accountant
#: model:ir.model,name:accountant.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"

View File

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

View File

@ -0,0 +1,9 @@
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _get_invoice_in_payment_state(self):
# OVERRIDE to enable the 'in_payment' state on invoices.
return 'in_payment'

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="base.user_root" model="res.users">
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
</record>
<record id="base.user_admin" model="res.users">
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
</record>
<!-- To change the categoy name from Invoicing -> Accounting-->
<record model="ir.module.category" id="base.module_category_accounting_accounting">
<field name="name">Accounting</field>
<field name="description">Helps you handle your invoices and accounting actions.
Invoicing: Invoices, payments and basic invoice reporting.
Invoicing &amp; Banks: adds the accounting dashboard, bank management and follow-up reports.
Bookkeeper: access to all Accounting features, including reporting, asset management, analytic accounting, without configuration rights.
Administrator: full access including configuration rights and accounting data management.
Readonly: access to all the accounting data but in readonly mode, no actions allowed.
</field>
</record>
<record id="account.group_account_readonly" model="res.groups">
<field name="name">Read-only</field>
<field name="category_id" ref="base.module_category_accounting_accounting"/>
</record>
<record id="account.group_account_user" model="res.groups">
<field name="name">Bookkeeper</field>
<field name="category_id" ref="base.module_category_accounting_accounting"/>
</record>
<record id="account.group_account_manager" model="res.groups">
<field name="implied_ids" eval="[(3, ref('account.group_account_invoice')), (4, ref('account.group_account_user'))]"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M40.682 41.263c-3.514 4.188-9.758 4.734-13.946 1.22-4.189-3.514-4.735-9.758-1.22-13.947 3.514-4.188 9.758-4.734 13.946-1.22 4.188 3.515 4.734 9.759 1.22 13.947Z" fill="#1AD3BB"/><path d="M25.383 21.464c-3.514 4.188-9.758 4.734-13.946 1.22-4.188-3.514-4.735-9.758-1.22-13.946 3.514-4.189 9.758-4.735 13.946-1.22 4.188 3.514 4.734 9.758 1.22 13.946Z" fill="#FBB945"/><path d="M37.09 6.545c1.407-1.406 3.36-1.732 4.364-.728L46 10.363l-33.09 33.09c-1.407 1.406-3.36 1.732-4.365.728L4 39.636 37.09 6.544Z" fill="#985184"/><path d="m31.174 25.188-7.79 7.789a9.853 9.853 0 0 1 2.13-4.442 9.858 9.858 0 0 1 5.66-3.348Z" fill="#005E7A"/><path d="M18.676 24.962a9.868 9.868 0 0 0 6.707-3.498 9.854 9.854 0 0 0 2.278-5.487l-8.985 8.985Z" fill="#953B24"/></svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@ -0,0 +1,12 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { accountTourSteps } from "@account/js/tours/account";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
patch(accountTourSteps, {
goToAccountMenu(description="Open Accounting Menu") {
return stepUtils.goToAppSteps('accountant.menu_accounting', description);
},
});

View File

@ -0,0 +1,61 @@
/** @odoo-module */
import { accountTourSteps } from "@account/js/tours/account";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("account_merge_wizard_tour", {
url: "/odoo",
steps: () => [
...accountTourSteps.goToAccountMenu("Go to Accounting"),
{
content: "Go to Configuration",
trigger: 'span:contains("Configuration")',
run: "click",
},
{
content: "Go to Chart of Accounts",
trigger: 'a:contains("Chart of Accounts")',
run: "click",
},
{
trigger: '.o_breadcrumb .text-truncate:contains("Chart of Accounts")',
},
{
content: "Select accounts",
trigger: "thead .o_list_record_selector",
run: "click",
},
{
content: "Check that exactly 4 accounts are present and selected",
trigger: ".o_list_selection_box:contains(4):contains(selected)",
},
{
content: "Open Actions menu",
trigger: ".o_cp_action_menus .dropdown-toggle",
run: "click",
},
{
content: "Open Merge accounts wizard",
trigger: 'span:contains("Merge accounts")',
run: "click",
},
{
content: "Group by name",
trigger: 'div[name="is_group_by_name"] input',
run: "click",
},
{
content: "Wait for content to be updated",
trigger: 'td:contains("Current Assets (Current Assets)")',
},
{
content: "Merge accounts",
trigger: 'button:not([disabled]) span:contains("Merge")',
run: "click",
},
{
content: "Check that there are now exactly 2 accounts",
trigger: ".o_pager_limit:contains(2)",
},
],
});

View File

@ -0,0 +1,10 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { accountTourSteps } from "@account/js/tours/account";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
patch(accountTourSteps, {
goToAccountMenu(description="Open Accounting Menu") {
return stepUtils.goToAppSteps('accountant.menu_accounting', description);
}
});

View File

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

View File

@ -0,0 +1,48 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install')
class TestAccountantTours(AccountTestInvoicingHttpCommon):
def test_account_merge_wizard_tour(self):
companies = self.env['res.company'].create([
{'name': 'tour_company_1'},
{'name': 'tour_company_2'},
])
self.env['account.account'].create([
{
'company_ids': [Command.set(companies[0].ids)],
'code': "100001",
'name': "Current Assets",
'account_type': 'asset_current',
},
{
'company_ids': [Command.set(companies[0].ids)],
'code': "100002",
'name': "Non-Current Assets",
'account_type': 'asset_non_current',
},
{
'company_ids': [Command.set(companies[1].ids)],
'code': "200001",
'name': "Current Assets",
'account_type': 'asset_current',
},
{
'company_ids': [Command.set(companies[1].ids)],
'code': "200002",
'name': "Non-Current Assets",
'account_type': 'asset_non_current',
},
])
self.env.ref('base.user_admin').write({
'company_id': companies[0].id,
'company_ids': [Command.set(companies.ids)],
})
self.start_tour("/odoo", 'account_merge_wizard_tour', login="admin", cookies={"cids": f"{companies[0].id}-{companies[1].id}"})

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_partner_view_form" model="ir.ui.view">
<field name="name">res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='accounting_disabled']" position="attributes">
<attribute name="string">Accounting</attribute>
</xpath>
<xpath expr="//page[@name='accounting']" position="attributes">
<attribute name="string">Accounting</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.accountant</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<app name="account" position="attributes">
<attribute name="data-string">Accounting</attribute>
<attribute name="string">Accounting</attribute>
<attribute name="logo">/accountant/static/description/icon.png</attribute>
</app>
</field>
</record>
</odoo>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
{
'name': 'ODEX Account: 3-Way Matching',
'category': 'Account/Accounting',
'author': 'ODEX',
'summary': 'Manage 3-way matching on bills',
'description': """
Manage 3-way matching on supplier bills
=======================================
In this system, you can manage the verification process for supplier bills against
received goods. This ensures that payments are only made when the items
have actually been delivered.
This feature allows creating the supplier bill based on ordered quantities
while keeping the payment pending until the received quantities on the purchase lines
match the recorded supplier bill.
The system introduces a "release to pay" status that marks for each bill
whether it is ready for payment.
Each bill receives one of the following three states:
- Yes (The bill can be paid)
- No (The bill cannot be paid, delivery is pending)
- Exception (Differences found between received and billed quantities)
""",
'depends': ['purchase'],
'data': [
'views/account_invoice_view.xml',
'views/account_journal_dashboard_view.xml'
],
'license': 'LGPL-3',
}

View File

@ -0,0 +1,119 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_3way_match
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-27 13:54+0000\n"
"PO-Revision-Date: 2025-01-27 13:54+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay_manual
msgid ""
" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
msgstr ""
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills in Exception"
msgstr ""
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills to Pay"
msgstr ""
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills to Validate"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__exception
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__exception
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__exception
msgid "Exception"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__force_release_to_pay
msgid "Force Status"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__force_release_to_pay
msgid ""
"Indicates whether the 'Should Be Paid' status is defined automatically or "
"manually."
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_journal
msgid "Journal"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_move_line
msgid "Journal Item"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__no
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__no
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__no
msgid "No"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay
msgid "Release To Pay"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move_line__can_be_paid
msgid "Release to Pay"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay_manual
msgid "Should Be Paid"
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay
msgid ""
"This field can take the following values :\n"
" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
msgstr ""
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__yes
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__yes
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__yes
msgid "Yes"
msgstr ""

View File

@ -0,0 +1,132 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_3way_match
#
# Translators:
# Wil ODEX, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX 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 ODEX, 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_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay_manual
msgid ""
" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
msgstr ""
"* نعم: عليك سداد قيمة الفاتورة، لقد استلمت المنتجات \n"
"* لا: ليس عليك سداد قيمة الفاتورة، لم تستلم المنتجات\n"
" * استثناء: هناك فرق بين الكمية المستلمة والكمية المدفوع قيمتها\n"
"هذه الحالة تُحدد تلقائياً، لكن يمكنك فرض الحالة من خلال تحديد اختيار 'فرض الحالة'."
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills in Exception"
msgstr "الفواتير المستثناة "
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills to Pay"
msgstr "الفواتير بانتظار السداد "
#. module: odex30_account_3way_match
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
msgid "Bills to Validate"
msgstr "الفواتير بانتظار التصديق "
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__exception
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__exception
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__exception
msgid "Exception"
msgstr "استثناء "
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__force_release_to_pay
msgid "Force Status"
msgstr "فرض الحالة"
#. module: odex30_account_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__force_release_to_pay
msgid ""
"Indicates whether the 'Should Be Paid' status is defined automatically or "
"manually."
msgstr "يحدد إذا ما كانت الحالة 'واجبة السداد' تُعين تلقائياً أم يدوياً. "
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_journal
msgid "Journal"
msgstr "دفتر اليومية"
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: odex30_account_3way_match
#: model:ir.model,name:odex30_account_3way_match.model_account_move_line
msgid "Journal Item"
msgstr "عنصر اليومية"
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__no
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__no
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__no
msgid "No"
msgstr "لا"
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay
msgid "Release To Pay"
msgstr "جاهزة للسداد"
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move_line__can_be_paid
msgid "Release to Pay"
msgstr "جاهزة للسداد"
#. module: odex30_account_3way_match
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay_manual
msgid "Should Be Paid"
msgstr "واجبة السداد"
#. module: odex30_account_3way_match
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay
msgid ""
"This field can take the following values :\n"
" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
msgstr ""
"يتحمل هذا الحقل القيم التالية:\n"
" * نعم: عليك سداد قيمة الفاتورة، لقد استلمت المنتجات\n"
" * لا: ليس عليك سداد قيمة الفاتورة، لم تستلم المنتجات\n"
" * استثناء: هناك فرق بين الكمية المستلمة والكمية المدفوع قيمتها\n"
"هذه الحالة تُحدد تلقائياً، لكن يمكنك فرض الحالة من خلال تحديد اختيار 'فرض الحالة'."
#. module: odex30_account_3way_match
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__yes
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__yes
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__yes
msgid "Yes"
msgstr "نعم"

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import account_invoice
from . import account_journal_dashboard

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.float_utils import float_compare
from odoo.tools.sql import column_exists, create_column
# Available values for the release_to_pay field.
_release_to_pay_status_list = [('yes', 'Yes'), ('no', 'No'), ('exception', 'Exception')]
class AccountMove(models.Model):
_inherit = 'account.move'
def _auto_init(self):
if not column_exists(self.env.cr, "account_move", "release_to_pay"):
# Create column manually to set default value to 'exception' on postgres level.
# This way we avoid heavy computation on module installation.
self.env.cr.execute("ALTER TABLE account_move ADD COLUMN release_to_pay VARCHAR DEFAULT 'exception'")
return super()._auto_init()
release_to_pay = fields.Selection(
_release_to_pay_status_list,
compute='_compute_release_to_pay',
copy=False,
store=True,
help="This field can take the following values :\n"
" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox.")
release_to_pay_manual = fields.Selection(
_release_to_pay_status_list,
string='Should Be Paid',
compute='_compute_release_to_pay_manual', store='True', readonly=False,
help=" * Yes: you should pay the bill, you have received the products\n"
" * No, you should not pay the bill, you have not received the products\n"
" * Exception, there is a difference between received and billed quantities\n"
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox.")
force_release_to_pay = fields.Boolean(
string="Force Status",
help="Indicates whether the 'Should Be Paid' status is defined automatically or manually.")
@api.depends('invoice_line_ids.can_be_paid', 'force_release_to_pay', 'payment_state')
def _compute_release_to_pay(self):
records = self
if self.env.context.get('module') == 'odex30_account_3way_match':
# on module installation we set 'no' for all paid bills and other non relevant records at once
records = records.filtered(lambda r: r.payment_state != 'paid' and r.move_type in ('in_invoice', 'in_refund'))
(self - records).release_to_pay = 'no'
for invoice in records:
if invoice.payment_state == 'paid' or not invoice.is_invoice(include_receipts=True):
# no need to pay, if it's already paid
invoice.release_to_pay = 'no'
elif invoice.force_release_to_pay:
#we must use the manual value contained in release_to_pay_manual
invoice.release_to_pay = invoice.release_to_pay_manual
else:
#otherwise we must compute the field
result = None
for invoice_line in invoice.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_section', 'line_note')):
line_status = invoice_line.can_be_paid
if line_status == 'exception':
#If one line is in exception, the entire bill is
result = 'exception'
break
elif not result:
result = line_status
elif line_status != result:
result = 'exception'
break
#The last two elif conditions model the fact that a
#bill will be in exception if its lines have different status.
#Otherwise, its status will be the one all its lines share.
#'result' can be None if the bill was entirely empty.
invoice.release_to_pay = result or 'no'
@api.depends('release_to_pay', 'force_release_to_pay')
def _compute_release_to_pay_manual(self):
for invoice in self:
if not (invoice.payment_state == 'paid' or not invoice.is_invoice(include_receipts=True) or invoice.force_release_to_pay):
invoice.release_to_pay_manual = invoice.release_to_pay
@api.onchange('release_to_pay_manual')
def _onchange_release_to_pay_manual(self):
if self.release_to_pay and self.release_to_pay_manual != self.release_to_pay:
self.force_release_to_pay = True
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _auto_init(self):
if not column_exists(self.env.cr, "account_move_line", "can_be_paid"):
# Create column manually to set default value to 'exception' on postgres level.
# This way we avoid heavy computation on module installation.
self.env.cr.execute("ALTER TABLE account_move_line ADD COLUMN can_be_paid VARCHAR DEFAULT 'exception'")
return super()._auto_init()
@api.depends('purchase_line_id.qty_received', 'purchase_line_id.qty_invoiced', 'purchase_line_id.product_qty', 'price_unit')
def _can_be_paid(self):
""" Computes the 'release to pay' status of an invoice line, depending on
the invoicing policy of the product linked to it, by calling the dedicated
subfunctions. This function also ensures the line is linked to a purchase
order (otherwise, can_be_paid will be set as 'exception'), and the price
between this order and the invoice did not change (otherwise, again,
the line is put in exception).
"""
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for invoice_line in self:
po_line = invoice_line.purchase_line_id
if po_line:
invoiced_qty = po_line.qty_invoiced
received_qty = po_line.qty_received
ordered_qty = po_line.product_qty
# A price difference between the original order and the invoice results in an exception
invoice_currency = invoice_line.currency_id
order_currency = po_line.currency_id
invoice_converted_price = invoice_currency._convert(
invoice_line.price_unit, order_currency, invoice_line.company_id, fields.Date.today())
if order_currency.compare_amounts(po_line.price_unit, invoice_converted_price) != 0:
invoice_line.can_be_paid = 'exception'
continue
if po_line.product_id.purchase_method == 'purchase': # 'on ordered quantities'
invoice_line._can_be_paid_ordered_qty(invoiced_qty, received_qty, ordered_qty, precision)
else: # 'on received quantities'
invoice_line._can_be_paid_received_qty(invoiced_qty, received_qty, ordered_qty, precision)
else: # Serves as default if the line is not linked to any Purchase.
invoice_line.can_be_paid = 'exception'
def _can_be_paid_ordered_qty(self, invoiced_qty, received_qty, ordered_qty, precision):
"""
Gives the release_to_pay status of an invoice line for 'on ordered
quantity' billing policy, if this line's invoice is related to a purchase order.
This function sets can_be_paid field to one of the following:
'yes': the content of the line has been ordered and can be invoiced
'no' : the content of the line hasn't been ordered at all, and cannot be invoiced
'exception' : only part of the invoice has been ordered
"""
if float_compare(invoiced_qty - self.quantity, ordered_qty, precision_digits=precision) >= 0:
self.can_be_paid = 'no'
elif float_compare(invoiced_qty, ordered_qty, precision_digits=precision) <= 0:
self.can_be_paid = 'yes'
else:
self.can_be_paid = 'exception'
def _can_be_paid_received_qty(self, invoiced_qty, received_qty, ordered_qty, precision):
"""
Gives the release_to_pay status of an invoice line for 'on received
quantity' billing policy, if this line's invoice is related to a purchase order.
This function sets can_be_paid field to one of the following:
'yes': the content of the line has been received and can be invoiced
'no' : the content of the line hasn't been received at all, and cannot be invoiced
'exception' : ordered and received quantities differ
"""
if float_compare(invoiced_qty, received_qty, precision_digits=precision) <= 0:
self.can_be_paid = 'yes'
elif received_qty == 0 and float_compare(invoiced_qty, ordered_qty, precision_digits=precision) <= 0: # "and" part to ensure a too high billed quantity results in an exception:
self.can_be_paid = 'no'
else:
self.can_be_paid = 'exception'
can_be_paid = fields.Selection(
_release_to_pay_status_list,
compute='_can_be_paid',
copy=False,
store=True,
string='Release to Pay')

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.osv import expression
from odoo.tools import SQL
class AccountJournal(models.Model):
_inherit = 'account.journal'
def open_action(self):
action = super(AccountJournal, self).open_action()
view = self.env.ref('account.action_move_in_invoice_type')
if view and action.get("id") == view.id:
action['context']['search_default_in_invoice'] = 0
account_purchase_filter = self.env.ref('odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match', False)
action['search_view_id'] = account_purchase_filter and [account_purchase_filter.id, account_purchase_filter.name] or False
return action
def _get_open_sale_purchase_query(self, journal_type):
# OVERRIDE
assert journal_type in ('sale', 'purchase')
query = self.env['account.move']._where_calc([
*self.env['account.move']._check_company_domain(self.env.companies),
('journal_id', 'in', self.ids),
('payment_state', 'in', ('not_paid', 'partial')),
('move_type', 'in', ('out_invoice', 'out_refund') if journal_type == 'sale' else ('in_invoice', 'in_refund')),
('state', '=', 'posted'),
])
selects = [
SQL("journal_id"),
SQL("company_id"),
SQL("currency_id AS currency"),
SQL("invoice_date_due < %s AS late", fields.Date.context_today(self)),
SQL("SUM(amount_residual_signed) AS amount_total_company"),
SQL("SUM((CASE WHEN move_type = 'in_invoice' THEN -1 ELSE 1 END) * amount_residual) AS amount_total"),
SQL("COUNT(*)"),
SQL("release_to_pay IN ('yes', 'exception') AS to_pay")
]
return query, selects
def _get_draft_sales_purchases_query(self):
# OVERRIDE
domain_sale = [
('journal_id', 'in', self.filtered(lambda j: j.type == 'sale').ids),
('move_type', 'in', self.env['account.move'].get_sale_types(include_receipts=True))
]
domain_purchase = [
('journal_id', 'in', self.filtered(lambda j: j.type == 'purchase').ids),
('move_type', 'in', self.env['account.move'].get_purchase_types(include_receipts=False)),
'|',
('invoice_date_due', '<', fields.Date.today()),
('release_to_pay', '=', 'yes')
]
domain = expression.AND([
[('state', '=', 'draft'), ('payment_state', 'in', ('not_paid', 'partial'))],
expression.OR([domain_sale, domain_purchase])
])
return self.env['account.move']._where_calc([
*self.env['account.move']._check_company_domain(self.env.companies),
*domain
])

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import test_release_to_pay_invoice
from . import test_account_journal_dashboard

View File

@ -0,0 +1,240 @@
from freezegun import freeze_time
from odoo.addons.account.tests.test_account_journal_dashboard_common import TestAccountJournalDashboardCommon
from odoo.tests import tagged
from odoo.tools.misc import format_amount
@tagged('post_install', '-at_install')
class AccountJournalDashboard3WayWatchTest(TestAccountJournalDashboardCommon):
@classmethod
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=None, amounts=None, taxes=None, company=False, currency=None, journal=None, invoice_date_due=None, release_to_pay=None):
move = super().init_invoice(move_type, partner, invoice_date, False, products, amounts, taxes, company, currency, journal)
if invoice_date_due:
move.invoice_date_due = invoice_date_due
if release_to_pay:
move.release_to_pay = release_to_pay
if post:
move.action_post()
return move
def test_sale_purchase_journal_for_purchase(self):
"""
Test different purchase journal setups with or without multicurrency:
1) Journal with no currency, bills in foreign currency -> dashboard data should be displayed in company currency
2) Journal in foreign currency, bills in foreign currency -> dashboard data should be displayed in foreign currency
3) Journal in foreign currency, bills in company currency -> dashboard data should be displayed in foreign currency
4) Journal in company currency, bills in company currency -> dashboard data should be displayed in company currency
5) Journal in company currency, bills in foreign currency -> dashboard data should be displayed in company currency
"""
foreign_currency = self.other_currency
company_currency = self.company_data['currency']
setup_values = [
[self.company_data['default_journal_purchase'], foreign_currency],
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
]
expected_vals_list = [
# number_draft, sum_draft, number_waiting, sum_waiting, number_late, sum_late, currency
[ 1, 100, 1, 55, 1, 55, company_currency],
[ 1, 200, 1, 110, 1, 110, foreign_currency],
[ 1, 400, 1, 220, 1, 220, foreign_currency],
[ 1, 200, 1, 110, 1, 110, company_currency],
[ 1, 100, 1, 55, 1, 55, company_currency],
]
for (purchase_journal, bill_currency), expected_vals in zip(setup_values, expected_vals_list):
with self.subTest(purchase_journal_currency=purchase_journal.currency_id, bill_currency=bill_currency, expected_vals=expected_vals):
bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=True, amounts=[200], currency=bill_currency, journal=purchase_journal)
_draft_bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=False, amounts=[200], currency=bill_currency, journal=purchase_journal)
payment = self.init_payment(-90, post=True, date='2017-01-01', currency=bill_currency)
(bill + payment.move_id).line_ids.filtered_domain([
('account_id', '=', self.company_data['default_account_payable'].id)
]).reconcile()
self.assertDashboardPurchaseSaleData(purchase_journal, *expected_vals)
@freeze_time("2023-03-15")
def test_purchase_journal_numbers_and_sums(self):
company_currency = self.company_data['currency']
journal = self.company_data['default_journal_purchase']
self._create_test_vendor_bills(journal)
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
# Expected behavior is to have three moves waiting for payment for a total amount of 4440$ one of which would be late
# for a total amount of 40$ (second move has one of three lines late but that's not enough to make the move late)
self.assertEqual(3, dashboard_data['number_waiting'])
self.assertEqual(format_amount(self.env, 4440, company_currency), dashboard_data['sum_waiting'])
self.assertEqual(1, dashboard_data['number_late'])
self.assertEqual(format_amount(self.env, 40, company_currency), dashboard_data['sum_late'])
@freeze_time("2019-01-22")
def test_customer_invoice_dashboard(self):
journal = self.company_data['default_journal_sale']
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'journal_id': journal.id,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-21',
'date': '2019-01-21',
'invoice_line_ids': [(0, 0, {
'product_id': self.product_a.id,
'quantity': 40.0,
'name': 'product test 1',
'discount': 10.00,
'price_unit': 2.27,
'tax_ids': [],
})]
})
refund = self.env['account.move'].create({
'move_type': 'out_refund',
'journal_id': journal.id,
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-21',
'date': '2019-01-21',
'invoice_line_ids': [(0, 0, {
'product_id': self.product_a.id,
'quantity': 1.0,
'name': 'product test 1',
'price_unit': 13.3,
'tax_ids': [],
})]
})
# Check Draft
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_draft'], 2)
self.assertIn('68.42', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 0)
self.assertIn('0.00', dashboard_data['sum_waiting'])
# Check Both
invoice.action_post()
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_draft'], 1)
self.assertIn('-\N{ZERO WIDTH NO-BREAK SPACE}13.30', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 1)
self.assertIn('81.72', dashboard_data['sum_waiting'])
# Check partial on invoice
partial_payment = self.env['account.payment'].create({
'amount': 13.3,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
})
partial_payment.action_post()
(invoice + partial_payment.move_id).line_ids.filtered(lambda line: line.account_type == 'asset_receivable').reconcile()
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_draft'], 1)
self.assertIn('13.3', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 1)
self.assertIn('68.42', dashboard_data['sum_waiting'])
# Check waiting payment
refund.action_post()
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_draft'], 0)
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 2)
self.assertIn('55.12', dashboard_data['sum_waiting'])
# Check partial on refund
payment = self.env['account.payment'].create({
'amount': 10.0,
'payment_type': 'outbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
})
payment.action_post()
(refund + payment.move_id).line_ids\
.filtered(lambda line: line.account_type == 'asset_receivable')\
.reconcile()
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_draft'], 0)
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEqual(dashboard_data['number_waiting'], 2)
self.assertIn('65.12', dashboard_data['sum_waiting'])
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
self.assertEqual(dashboard_data['number_late'], 2)
self.assertIn('65.12', dashboard_data['sum_late'])
def test_sale_purchase_journal_for_multi_currency_sale(self):
currency = self.other_currency
company_currency = self.company_data['currency']
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': '2017-01-01',
'date': '2017-01-01',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
'invoice_line_ids': [
(0, 0, {'name': 'test', 'price_unit': 200})
],
})
invoice.action_post()
payment = self.env['account.payment'].create({
'amount': 90.0,
'date': '2016-01-01',
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'currency_id': currency.id,
})
payment.action_post()
(invoice + payment.move_id).line_ids.filtered_domain([
('account_id', '=', self.company_data['default_account_receivable'].id)
]).reconcile()
default_journal_sale = self.company_data['default_journal_sale']
dashboard_data = default_journal_sale._get_journal_dashboard_data_batched()[default_journal_sale.id]
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_waiting'])
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_late'])
@freeze_time("2023-03-15")
def test_purchase_journal_numbers_and_sums_to_validate(self):
company_currency = self.company_data['currency']
journal = self.company_data['default_journal_purchase']
datas = [
{'invoice_date_due': '2023-04-30'},
{'invoice_date_due': '2023-04-30', 'release_to_pay': 'yes'},
{'invoice_date_due': '2023-04-30', 'release_to_pay': 'no'},
{'invoice_date_due': '2023-03-01'},
{'invoice_date_due': '2023-03-01', 'release_to_pay': 'yes'},
{'invoice_date_due': '2023-03-01', 'release_to_pay': 'no'},
]
for data in datas:
self.init_invoice('in_invoice', invoice_date='2023-03-01', post=False, amounts=[4000], journal=journal, invoice_date_due=data['invoice_date_due'], release_to_pay=data.get('release_to_pay'))
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
# Expected behavior is to have six amls waiting for payment for a total amount of 4440$
# three of which would be late for a total amount of 140$
self.assertEqual(4, dashboard_data['number_draft'])
self.assertEqual(format_amount(self.env, 16000, company_currency), dashboard_data['sum_draft'])

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import Command, fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
@tagged('post_install', '-at_install')
class TestReleaseToPayInvoice(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env['res.partner'].create({'name': 'Zizizapartner'})
cls.product = cls.env['product.product'].create({
'name': 'VR Computer',
'standard_price': 2500.0,
'list_price': 2899.0,
'type': 'service',
'default_code': 'VR-01',
'weight': 1.0,
'purchase_method': 'receive',
})
cls.other_currency = cls.setup_other_currency('HRK', rounding=0.001)
def check_release_to_pay_scenario(self, ordered_qty, scenario, invoicing_policy='receive', order_price=500.0):
""" Generic test function to check that each use scenario behaves properly.
"""
self.product.purchase_method = invoicing_policy
purchase_order = self.env['purchase.order'].create({
'partner_id': self.partner.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_qty': ordered_qty,
'product_uom': self.product.uom_po_id.id,
'price_unit': order_price,
'date_planned': fields.Datetime.now(),
})]
})
purchase_order.button_confirm()
invoices_list = []
purchase_line = purchase_order.order_line[-1]
AccountMove = self.env['account.move'].with_context(default_move_type='in_invoice')
for (action, params) in scenario:
if action == 'invoice':
# <field name="purchase_id" invisible="1"/>
move_form = Form(AccountMove.with_context(default_purchase_id=purchase_order.id))
with move_form.invoice_line_ids.edit(0) as line_form:
if 'price' in params:
line_form.price_unit = params['price']
if 'qty' in params:
line_form.quantity = params['qty']
new_invoice = move_form.save()
new_invoice.write({'invoice_line_ids': [
Command.create({'display_type': 'line_section', 'name': 'Section'}),
Command.create({'display_type': 'line_note', 'name': 'Note'}),
]})
invoices_list.append(new_invoice)
self.assertEqual(new_invoice.release_to_pay, params['rslt'], "Wrong invoice release to pay status for scenario " + str(scenario))
elif action == 'receive':
purchase_line.write({'qty_received': params['qty']}) # as the product is a service, its recieved quantity is set manually
if 'rslt' in params:
for (invoice_index, status) in params['rslt']:
self.assertEqual(invoices_list[invoice_index].release_to_pay, status, "Wrong invoice release to pay status for scenario " + str(scenario))
def test_3_way_match(self):
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'yes'})], invoicing_policy='purchase')
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 10, 'rslt': 'yes'})], invoicing_policy='purchase')
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 10, 'rslt': 'yes'})], invoicing_policy='purchase')
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'yes'}), ('receive',{'qty': 5}), ('invoice', {'qty': 6, 'rslt': 'exception'})], invoicing_policy='purchase')
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 10, 'rslt': 'yes'}), ('invoice', {'qty': 10, 'rslt': 'no'})], invoicing_policy='purchase')
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'yes'})])
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 10, 'rslt': 'exception'})])
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'})])
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 5, 'rslt': [(-1, 'yes')]})])
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 3, 'rslt': [(-1, 'exception')]})])
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 10, 'rslt': [(-1, 'yes')]})])
# Special use case : a price change between order and invoice should always put the bill in exception
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'exception', 'price':42})])
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'exception', 'price':42})], invoicing_policy='purchase')
def test_amount_currency_edit(self):
"""
Ensure that editing the `amount_currency` of a journal item on an invoice is possible.
In 17.0 changes to Binary fields and web_save were made (related to context key 'bin_size').
They led to tracebacks in the flow tested here.
"""
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
move_form.invoice_date = fields.Date.from_string('2023-01-01')
move_form.partner_id = self.partner_a
move_form.currency_id = self.other_currency
with move_form.invoice_line_ids.new() as line_form:
line_form.quantity = 1
line_form.price_unit = 10
move_form.save()
with move_form.line_ids.edit(0) as line_form:
line_form.amount_currency = -30
move_form.save()
self.assertEqual(move_form.line_ids.edit(0).amount_currency, -30)

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_invoice_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">
<xpath expr="//page[@id='other_tab']//field[@name='fiscal_position_id']" position='after'>
<label for="release_to_pay_manual" invisible="move_type not in ('in_invoice', 'in_refund')"/>
<div class="o_row" col="4" invisible="move_type not in ('in_invoice', 'in_refund')">
<field name="release_to_pay" invisible="True" force_save="1"/>
<field name="release_to_pay_manual"/>
<label class="fw-bold" for="force_release_to_pay" invisible="not force_release_to_pay"/>
<field name="force_release_to_pay" invisible="not force_release_to_pay"/>
</div>
</xpath>
</field>
</record>
<record id="account_invoice_filter_inherit_odex30_account_3way_match" model="ir.ui.view">
<field name="name">account.invoice.select.inherit.odex30_account_3way_match</field>
<field name="mode">primary</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_account_bill_filter"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='late']" position='after'>
<separator/>
<filter name="bills_to_validate" string="Bills to Validate" domain="['&amp;', '|', ('release_to_pay','=', 'yes'), ('invoice_date_due', '&lt;', time.strftime('%Y-%m-%d')), ('state', '=', 'draft')]"/>
<filter name="bills_to_pay" string="Bills to Pay" domain="['&amp;', '&amp;', ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), ('release_to_pay','in', ('yes', 'exception'))]"/>
<filter name="exception" string="Bills in Exception" domain="[('release_to_pay','=', 'exception')]"/>
<separator/>
</xpath>
</field>
</record>
<!--This action has been redefined and the account_invoice_filter_inherit_odex30_account_3way_match
created in order to only display 'bills_to_pay' and 'exception' filters
in the view related to vendor bills, as it makes no sense to propose them
in the view related to sales invoices, which share the same model.-->
<record id="account.action_move_in_invoice_type" model="ir.actions.act_window">
<field name="search_view_id" ref="account_invoice_filter_inherit_odex30_account_3way_match"/>
</record>
<record id="account.action_move_in_refund_type" model="ir.actions.act_window">
<field name="search_view_id" ref="account_invoice_filter_inherit_odex30_account_3way_match"/>
</record>
</odoo>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_journal_dashboard_kanban_view_3_way_match" model="ir.ui.view">
<field name="name">account.journal.dashboard.kanban</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/>
<field name="arch" type="xml">
<xpath expr="//a[span[@id='account_dashboard_purchase_draft']]" position="attributes">
<attribute name="context">
{'search_default_bills_to_validate': 1}
</attribute>
</xpath>
<xpath expr="//a[span[@id='account_dashboard_bills_to_pay']]" position="attributes">
<attribute name="context">
{'search_default_bills_to_pay':1}
</attribute>
</xpath>
<xpath expr="//a[span[@id='account_dashboard_bills_late']]" position="attributes">
<attribute name="context">
{'search_default_late':1}
</attribute>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -0,0 +1,26 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
{
'name': 'ODEX Account: Advanced Check Processing',
'version': '1.0',
'category': 'Account/Accounting',
'author': 'ODEX',
'summary': 'Advanced verification and reconciliation for issued checks.',
'description': """
This system provides advanced tools for managing financial check distributions.
It enables seamless verification between your internal accounting records and
issued physical checks within the financial management interface.
Key Features:
- Seamless verification of check payments.
- Real-time reconciliation within the accounting workspace.
- Enhanced audit trails for distributed checks.
""",
'depends': ['odex30_account_accountant', 'account_check_printing'],
'data': [
'views/bank_rec_widget_views.xml',
],
'installable': True,
'auto_install': True,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,34 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_accountant_check_printing
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX 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:26+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_accountant_check_printing
#: model:ir.model.fields,field_description:odex30_account_accountant_check_printing.field_account_move_line__check_number
msgid "Check Number"
msgstr ""
#. module: odex30_account_accountant_check_printing
#: model:ir.model,name:odex30_account_accountant_check_printing.model_account_move_line
msgid "Journal Item"
msgstr ""
#. module: odex30_account_accountant_check_printing
#: model:ir.model.fields,help:odex30_account_accountant_check_printing.field_account_move_line__check_number
msgid ""
"The selected journal is configured to print check numbers. If your pre-"
"printed check paper already has numbers or if the current numbering is "
"wrong, you can change it in the journal configuration page."
msgstr ""

View File

@ -0,0 +1,41 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_accountant_check_printing
#
# Translators:
# Wil ODEX, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX 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 ODEX, 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_accountant_check_printing
#: model:ir.model.fields,field_description:odex30_account_accountant_check_printing.field_account_move_line__check_number
msgid "Check Number"
msgstr "رقم الشيك "
#. module: odex30_account_accountant_check_printing
#: model:ir.model,name:odex30_account_accountant_check_printing.model_account_move_line
msgid "Journal Item"
msgstr "عنصر دفتر اليومية "
#. module: odex30_account_accountant_check_printing
#: model:ir.model.fields,help:odex30_account_accountant_check_printing.field_account_move_line__check_number
msgid ""
"The selected journal is configured to print check numbers. If your pre-"
"printed check paper already has numbers or if the current numbering is "
"wrong, you can change it in the journal configuration page."
msgstr ""
"تمت تهيئة قيد اليومية المُختار لطباعة أرقام الشيكات. إذا كان لشيكاتك "
"المطبوعة مسبقًا أرقام بالفعل أو إذا كان الترقيم الحالي خاطئًا، فيمكنك تغييره"
" في صفحة تهيئة دفتر اليومية. "

View File

@ -0,0 +1,3 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import account_move_line

View File

@ -0,0 +1,13 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountMoveLine(models.Model):
_name = "account.move.line"
_inherit = "account.move.line"
check_number = fields.Char(
string="Check Number",
related='payment_id.check_number',
)

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_move_line_list_bank_rec_widget" model="ir.ui.view">
<field name="name">account.move.line.list.bank_rec_widget</field>
<field name="model">account.move.line</field>
<field name="inherit_id" ref="odex30_account_accountant.view_account_move_line_list_bank_rec_widget"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="check_number"
optional="hidden"/>
</xpath>
</field>
</record>
<record id="view_account_move_line_search_bank_rec_widget" model="ir.ui.view">
<field name="name">account.move.line.search.bank_rec_widget</field>
<field name="model">account.move.line</field>
<field name="inherit_id" ref="odex30_account_accountant.view_account_move_line_search_bank_rec_widget"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="check_number"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="odex30_account_accountant_fleet.BankRecRecordFormLineIds" t-inherit="account_accountant.BankRecRecordFormLineIds" t-inherit-mode="extension">
<t t-name="odex30_account_accountant_fleet.BankRecRecordFormLineIds" t-inherit="odex30_account_accountant.BankRecRecordFormLineIds" t-inherit-mode="extension">
<xpath expr="//t[@name='col_taxes']" position="after">
<t t-if="column[0] === 'vehicle'" name="col_vehicle">
<td class="o_data_cell o_field_cell o_field_widget o_list_many2one"
@ -12,7 +12,7 @@
</xpath>
</t>
<t t-name="odex30_account_accountant_fleet.BankRecRecordNotebookManualOperations" t-inherit="account_accountant.BankRecRecordNotebookManualOperations" t-inherit-mode="extension">
<t t-name="odex30_account_accountant_fleet.BankRecRecordNotebookManualOperations" t-inherit="odex30_account_accountant.BankRecRecordNotebookManualOperations" t-inherit-mode="extension">
<xpath expr="//div[@name='suggestion']" position="before">
<div name="vehicle"
t-if="!['liquidity', 'new_batch'].includes(line.data.flag)"

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
{
'name': 'ODEX Account: Asset-Fleet Integration',
'category': 'Odex30-Accounting/Odex30-Accounting',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'summary': 'Advanced integration between fixed assets and vehicle fleet management.',
'description': """
This module provides a robust bridge between your fixed asset records and
the company's vehicle fleet management system.
It allows for:
- Direct linkage of assets to specific fleet vehicles.
- Unified tracking of depreciation and maintenance costs for transport assets.
- Integrated reporting across financial and operational fleet data.
""",
'version': '1.0',
'depends': ['account_fleet', 'odex30_account_asset'],
'data': [
'views/account_asset_views.xml',
'views/account_move_views.xml',
],
'auto_install': True,
}

View File

@ -0,0 +1,43 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_asset_fleet
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX 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:26+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_asset_fleet
#. odoo-python
#: code:addons/odex30_account_asset_fleet/models/account_asset.py:0
msgid "All the lines should be from the same vehicle"
msgstr ""
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_account_asset
msgid "Asset/Revenue Recognition"
msgstr ""
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_fleet_vehicle_log_services
msgid "Services for vehicles"
msgstr ""
#. module: odex30_account_asset_fleet
#: model:ir.model.fields,field_description:odex30_account_asset_fleet.field_account_asset__vehicle_id
#: model_terms:ir.ui.view,arch_db:odex30_account_asset_fleet.view_odex30_account_asset_fleet_form
msgid "Vehicle"
msgstr ""

View File

@ -0,0 +1,47 @@
# Translation of ODEX Server.
# This file contains the translation of the following modules:
# * odex30_account_asset_fleet
#
# Translators:
# Wil ODEX, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: ODEX 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 ODEX, 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_asset_fleet
#. odoo-python
#: code:addons/odex30_account_asset_fleet/models/account_asset.py:0
msgid "All the lines should be from the same vehicle"
msgstr "يجب أن تكون كافة البنود من نفس المركبة "
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_account_asset
msgid "Asset/Revenue Recognition"
msgstr "إثبات الأصل/الإيرادات "
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: odex30_account_asset_fleet
#: model:ir.model,name:odex30_account_asset_fleet.model_fleet_vehicle_log_services
msgid "Services for vehicles"
msgstr "خدمات المركبات "
#. module: odex30_account_asset_fleet
#: model:ir.model.fields,field_description:odex30_account_asset_fleet.field_account_asset__vehicle_id
#: model_terms:ir.ui.view,arch_db:odex30_account_asset_fleet.view_odex30_account_asset_fleet_form
msgid "Vehicle"
msgstr "المركبة"

View File

@ -0,0 +1,5 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from . import account_asset
from . import account_move
from . import fleet_vehicle_log_services

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountAsset(models.Model):
_inherit = 'account.asset'
vehicle_id = fields.Many2one('fleet.vehicle', compute='_compute_vehicle_id', readonly=False, store=True)
@api.depends('original_move_line_ids')
def _compute_vehicle_id(self):
for record in self:
if len(record.original_move_line_ids.vehicle_id) > 1:
raise UserError(_("All the lines should be from the same vehicle"))
record.vehicle_id = record.original_move_line_ids.vehicle_id
def action_open_vehicle(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'fleet.vehicle',
'res_id': self.vehicle_id.id,
'view_ids': [(False, 'form')],
'view_mode': 'form',
}

View File

@ -0,0 +1,15 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _prepare_move_for_asset_depreciation(self, vals):
# Overridden in order to link the depreciation entries with the vehicle_id
move_vals = super()._prepare_move_for_asset_depreciation(vals)
if vals['asset_id'].vehicle_id:
for _command, _id, line_vals in move_vals['line_ids']:
line_vals['vehicle_id'] = vals['asset_id'].vehicle_id.id
return move_vals

View File

@ -0,0 +1,20 @@
# Part of ODEX. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class FleetVehicleLogServices(models.Model):
_inherit = 'fleet.vehicle.log.services'
@api.depends('account_move_line_id.price_subtotal',
'account_move_line_id.non_deductible_tax_value',
'account_move_line_id.account_id.multiple_assets_per_line')
def _compute_amount(self):
for log_service in self:
if not log_service.account_move_line_id:
continue
account_move_line_id = log_service.account_move_line_id
quantity = 1
if account_move_line_id.account_id.multiple_assets_per_line:
quantity = account_move_line_id.quantity
log_service.amount = account_move_line_id.currency_id.round(
(account_move_line_id.debit + account_move_line_id.non_deductible_tax_value) / quantity)

View File

@ -0,0 +1,22 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="view_odex30_account_asset_fleet_form" model="ir.ui.view">
<field name="name">account.asset.fleet.form</field>
<field name="model">account.asset</field>
<field name="inherit_id" ref="odex30_account_asset.view_account_asset_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/div[@name='button_box']" position="inside">
<field name='vehicle_id' invisible="1"/>
<button class="oe_stat_button" string="Vehicle" name="action_open_vehicle" type="object" icon="fa-car" invisible="not vehicle_id"/>
</xpath>
<xpath expr="//sheet/notebook/page[@name='related_items']//field[@name='account_id']" position="after">
<field name='vehicle_id' optional='hidden'/>
</xpath>
<xpath expr="//field[@name='already_depreciated_amount_import']" position="after">
<field name='vehicle_id'/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="view_account_move_fleet_form" model="ir.ui.view">
<field name="name">account.move.fleet.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_fleet.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='line_ids']//field[@name='vehicle_id']" position="attributes">
<attribute name="column_invisible">parent.move_type not in ('entry', 'in_invoice', 'in_refund')</attribute>
<attribute name="required">need_vehicle and parent.move_type in ('in_invoice', 'in_refund')</attribute>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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