fetch report
This commit is contained in:
parent
cae66f6bc2
commit
7ff50ca726
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Binary file not shown.
|
|
@ -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')
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
all_view_accounts = self.search([('account_type', '=', 'view')], order='code')
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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/*',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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 "قيد اليومية"
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import account_move
|
||||
|
|
@ -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'
|
||||
|
|
@ -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 & 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 |
|
|
@ -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 |
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_tour
|
||||
|
|
@ -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}"})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "Online Bank Statement Synchronization",
|
||||
'summary': """
|
||||
This module is used for Online bank synchronization.""",
|
||||
|
||||
'description': """
|
||||
This module is used for Online bank synchronization. It provides basic methods to synchronize bank statement.
|
||||
""",
|
||||
'author': "Expert Co. Ltd.",
|
||||
'website': "http://www.exp-sa.com",
|
||||
'category': 'Odex30-Accounting/Odex30-Accounting',
|
||||
'version': '2.0',
|
||||
'depends': ['odex30_account_accountant'],
|
||||
|
||||
'data': [
|
||||
'data/config_parameter.xml',
|
||||
'data/ir_cron.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/sync_reminder_email_template.xml',
|
||||
|
||||
'security/ir.model.access.csv',
|
||||
'security/account_online_sync_security.xml',
|
||||
|
||||
'views/account_online_sync_views.xml',
|
||||
'views/account_bank_statement_view.xml',
|
||||
'views/account_journal_view.xml',
|
||||
'views/account_online_sync_portal_templates.xml',
|
||||
'views/account_journal_dashboard_view.xml',
|
||||
|
||||
'wizard/account_bank_selection_wizard.xml',
|
||||
'wizard/account_journal_missing_transactions.xml',
|
||||
'wizard/account_journal_duplicate_transactions.xml',
|
||||
'wizard/account_bank_statement_line.xml',
|
||||
],
|
||||
'license': 'OEEL-1',
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'odex30_account_online_sync/static/src/components/**/*',
|
||||
'odex30_account_online_sync/static/src/js/odoo_fin_connector.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'odex30_account_online_sync/static/src/js/online_sync_portal.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'odex30_account_online_sync/static/tests/helpers/*.js',
|
||||
'odex30_account_online_sync/static/tests/*.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
from . import portal
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,58 +0,0 @@
|
|||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.tools import format_amount, format_date
|
||||
from odoo.exceptions import AccessError, MissingError, UserError
|
||||
|
||||
|
||||
class OnlineSynchronizationPortal(CustomerPortal):
|
||||
|
||||
@http.route(['/renew_consent/<int:journal_id>'], type='http', auth="public", website=True, sitemap=False)
|
||||
def portal_online_sync_renew_consent(self, journal_id, access_token=None, **kw):
|
||||
# Display a page to the user allowing to renew the consent for his bank sync.
|
||||
# Requires the same rights as the button in odoo.
|
||||
try:
|
||||
journal_sudo = self._document_check_access('account.journal', journal_id, access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
values = self._prepare_portal_layout_values()
|
||||
# Ignore the route if the journal isn't one using bank sync.
|
||||
if not journal_sudo.account_online_account_id:
|
||||
raise request.not_found()
|
||||
|
||||
balance = journal_sudo.account_online_account_id.balance
|
||||
if journal_sudo.account_online_account_id.currency_id:
|
||||
formatted_balance = format_amount(request.env, balance, journal_sudo.account_online_account_id.currency_id)
|
||||
else:
|
||||
formatted_balance = format_amount(request.env, balance, journal_sudo.currency_id or journal_sudo.company_id.currency_id)
|
||||
|
||||
values.update({
|
||||
'bank': journal_sudo.bank_account_id.bank_name or journal_sudo.account_online_account_id.name,
|
||||
'bank_account': journal_sudo.bank_account_id.acc_number,
|
||||
'journal': journal_sudo.name,
|
||||
'latest_balance_formatted': formatted_balance,
|
||||
'latest_balance': balance,
|
||||
'latest_sync': format_date(request.env, journal_sudo.account_online_account_id.last_sync, date_format="MMM dd, YYYY"),
|
||||
'iframe_params': json.dumps(journal_sudo.action_extend_consent()),
|
||||
})
|
||||
return request.render("account_online_synchronization.portal_renew_consent", values)
|
||||
|
||||
|
||||
@http.route(['/renew_consent/<int:journal_id>/complete'], type='http', auth="public", methods=['POST'], website=True)
|
||||
def portal_online_sync_action_complete(self, journal_id, access_token=None, **kw):
|
||||
# Complete the consent renewal process
|
||||
try:
|
||||
journal_sudo = self._document_check_access('account.journal', journal_id, access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
# Ignore the route if the journal isn't one using bank sync.
|
||||
if not journal_sudo.account_online_link_id:
|
||||
raise request.not_found()
|
||||
try:
|
||||
journal_sudo.account_online_link_id._update_connection_status()
|
||||
journal_sudo.manual_sync()
|
||||
except UserError:
|
||||
pass
|
||||
return request.make_response(json.dumps({'status': 'done'}))
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record forcecreate="True" id="config_online_sync_proxy_mode" model="ir.config_parameter">
|
||||
<field name="key">odex30_account_online_sync.proxy_mode</field>
|
||||
<field name="value">production</field>
|
||||
</record>
|
||||
<record forcecreate="True" id="config_online_sync_request_timeout" model="ir.config_parameter">
|
||||
<field name="key">odex30_account_online_sync.request_timeout</field>
|
||||
<field name="value">60</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Cron to synchronize transaction -->
|
||||
<record id="online_sync_cron" model="ir.cron">
|
||||
<field name="name">Account: Journal online sync</field>
|
||||
<field name="model_id" ref="account.model_account_journal"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_fetch_online_transactions()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">12</field>
|
||||
<field name="interval_type">hours</field>
|
||||
</record>
|
||||
<record id="online_sync_cron_waiting_synchronization" model="ir.cron">
|
||||
<field name="name">Account: Journal online Waiting Synchronization</field>
|
||||
<field name="model_id" ref="account.model_account_journal"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_fetch_waiting_online_transactions()</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
</record>
|
||||
<!-- Cron to handle sending of reminder email -->
|
||||
<record id="online_sync_mail_cron" model="ir.cron">
|
||||
<field name="name">Account: Journal online sync reminder</field>
|
||||
<field name="model_id" ref="account.model_account_journal"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_reminder_email()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
<!-- Cron to delete unused connections -->
|
||||
<record id="online_sync_unused_connection_cron" model="ir.cron">
|
||||
<field name="name">Account: Journal online sync cleanup unused connections</field>
|
||||
<field name="model_id" ref="odex30_account_online_sync.model_account_online_link"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_delete_unused_connection()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="bank_sync_activity_update_consent" model="mail.activity.type">
|
||||
<field name="name">Bank Synchronization: Update consent</field>
|
||||
<field name="icon">fa-university</field>
|
||||
<field name="decoration_type">warning</field>
|
||||
<field name="res_model">account.journal</field>
|
||||
<field name="delay_count">0</field>
|
||||
</record>
|
||||
|
||||
<record id="bank_sync_consent_renewal" model="mail.message.subtype">
|
||||
<field name="name">Consent Renewal</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="hidden" eval="True"/>
|
||||
<field name="res_model">account.journal</field>
|
||||
<field name="sequence" eval="900"/>
|
||||
<field name="track_recipients" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-- disable bank synchronisation links
|
||||
UPDATE account_online_link
|
||||
SET provider_data = '',
|
||||
client_id = 'duplicate';
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="email_template_sync_reminder" model="mail.template">
|
||||
<field name="name">Bank connection expiration reminder</field>
|
||||
<field name="subject">Your bank connection is expiring soon</field>
|
||||
<field name="email_from">{{ object.company_id.email_formatted or user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.renewal_contact_email }}</field>
|
||||
<field name="model_id" ref="odex30_account_online_sync.model_account_journal"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td valign="top" style="font-size: 13px;">
|
||||
<div>
|
||||
Hello,<br /><br />
|
||||
The connection between <b><a t-att-href='object.get_base_url()' t-out="object.get_base_url() or ''">https://yourcompany.odoo.com</a></b> and <t t-out="object.account_online_link_id.name or ''">Belfius</t> <t t-if="not object.expiring_synchronization_due_day">expired.</t><t t-else="">expires in <t t-out="object.expiring_synchronization_due_day or ''">10</t> days.</t><br/>
|
||||
<div style="margin: 16px 0px 16px 0px;">
|
||||
<a t-attf-href="{{ website_url }}/renew_consent/{{ object.id }}?access_token={{object.access_token}}"
|
||||
style="background-color: #4caf50; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
Renew Consent
|
||||
</a>
|
||||
</div>
|
||||
Security Tip: Check that the domain name you are redirected to is: <b><a t-att-href='object.get_base_url()' t-out="object.get_base_url() or ''">https://yourcompany.odoo.com</a></b>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:center;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- POWERED BY -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td style="text-align: center; font-size: 13px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=auth" style="color: #875A7B;">Odoo</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; color: #454748; padding: 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td style="text-align: center; font-size: 11px;">
|
||||
PS: This is an automated email sent by Odoo Accounting to remind you before a bank sync consent expiration.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_bank_statement
|
||||
from . import account_journal
|
||||
from . import account_online
|
||||
from . import company
|
||||
from . import mail_activity_type
|
||||
from . import partner
|
||||
from . import bank_rec_widget
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,114 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID, tools, _
|
||||
from odoo.tools import date_utils
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
STATEMENT_LINE_CREATION_BATCH_SIZE = 500 # When importing transactions, batch the process to commit after importing batch_size
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
|
||||
online_transaction_identifier = fields.Char("Online Transaction Identifier", readonly=True)
|
||||
online_partner_information = fields.Char(readonly=True)
|
||||
online_account_id = fields.Many2one(comodel_name='account.online.account', readonly=True)
|
||||
online_link_id = fields.Many2one(
|
||||
comodel_name='account.online.link',
|
||||
related='online_account_id.account_online_link_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""
|
||||
Some transactions can be marked as "Zero Balancing",
|
||||
which is a transaction used at the end of the day to summarize all the transactions
|
||||
of the day. As we already manage the details of all the transactions, this one is not
|
||||
useful and moreover create duplicates. To deal with that, we cancel the move and so
|
||||
the bank statement line.
|
||||
"""
|
||||
# EXTEND account
|
||||
bank_statement_lines = super().create(vals_list)
|
||||
moves_to_cancel = self.env['account.move']
|
||||
for bank_statement_line in bank_statement_lines:
|
||||
transaction_details = json.loads(bank_statement_line.transaction_details) if bank_statement_line.transaction_details else {}
|
||||
if not transaction_details.get('is_zero_balancing'):
|
||||
continue
|
||||
moves_to_cancel |= bank_statement_line.move_id
|
||||
moves_to_cancel.button_cancel()
|
||||
|
||||
return bank_statement_lines
|
||||
|
||||
@api.model
|
||||
def _online_sync_bank_statement(self, transactions, online_account):
|
||||
"""
|
||||
build bank statement lines from a list of transaction and post messages is also post in the online_account of the journal.
|
||||
:param transactions: A list of transactions that will be created.
|
||||
The format is : [{
|
||||
'id': online id, (unique ID for the transaction)
|
||||
'date': transaction date, (The date of the transaction)
|
||||
'name': transaction description, (The description)
|
||||
'amount': transaction amount, (The amount of the transaction. Negative for debit, positive for credit)
|
||||
}, ...]
|
||||
:param online_account: The online account for this statement
|
||||
Return: The number of imported transaction for the journal
|
||||
"""
|
||||
start_time = time.time()
|
||||
lines_to_reconcile = self.env['account.bank.statement.line']
|
||||
try:
|
||||
for journal in online_account.journal_ids:
|
||||
# Since the synchronization succeeded, set it as the bank_statements_source of the journal
|
||||
journal.sudo().write({'bank_statements_source': 'online_sync'})
|
||||
if not transactions:
|
||||
continue
|
||||
|
||||
sorted_transactions = sorted(transactions, key=lambda transaction: transaction['date'])
|
||||
total = self.env.context.get('transactions_total') or sum([transaction['amount'] for transaction in transactions])
|
||||
|
||||
# For first synchronization, an opening line is created to fill the missing bank statement data
|
||||
any_st_line = self.search_count([('journal_id', '=', journal.id)], limit=1)
|
||||
journal_currency = journal.currency_id or journal.company_id.currency_id
|
||||
# If there are neither statement and the ending balance != 0, we create an opening bank statement at the day of the oldest transaction.
|
||||
# We set the sequence to >1 to ensure the computed internal_index will force its display before any other statement with the same date.
|
||||
if not any_st_line and not journal_currency.is_zero(online_account.balance - total):
|
||||
opening_st_line = self.with_context(skip_statement_line_cron_trigger=True).create({
|
||||
'date': sorted_transactions[0]['date'],
|
||||
'journal_id': journal.id,
|
||||
'payment_ref': _("Opening statement: first synchronization"),
|
||||
'amount': online_account.balance - total,
|
||||
'sequence': 2,
|
||||
})
|
||||
lines_to_reconcile += opening_st_line
|
||||
|
||||
filtered_transactions = online_account._get_filtered_transactions(sorted_transactions)
|
||||
|
||||
do_commit = not (hasattr(threading.current_thread(), 'testing') and threading.current_thread().testing)
|
||||
if filtered_transactions:
|
||||
# split transactions import in batch and commit after each batch except in testing mode
|
||||
for index in range(0, len(filtered_transactions), STATEMENT_LINE_CREATION_BATCH_SIZE):
|
||||
lines_to_reconcile += self.with_user(SUPERUSER_ID).with_company(journal.company_id).with_context(skip_statement_line_cron_trigger=True).create(filtered_transactions[index:index + STATEMENT_LINE_CREATION_BATCH_SIZE])
|
||||
if do_commit:
|
||||
self.env.cr.commit()
|
||||
# Set last sync date as the last transaction date
|
||||
journal.account_online_account_id.sudo().write({'last_sync': filtered_transactions[-1]['date']})
|
||||
|
||||
if lines_to_reconcile:
|
||||
# 'limit_time_real_cron' defaults to -1.
|
||||
# Manual fallback applied for non-POSIX systems where this key is disabled (set to None).
|
||||
cron_limit_time = tools.config['limit_time_real_cron'] or -1
|
||||
limit_time = (cron_limit_time if cron_limit_time > 0 else 180) - (time.time() - start_time)
|
||||
if limit_time > 0:
|
||||
lines_to_reconcile._cron_try_auto_reconcile_statement_lines(limit_time=limit_time)
|
||||
# Catch any configuration error that would prevent creating the entries, reset fetching_status flag and re-raise the error
|
||||
# Otherwise flag is never reset and user is under the impression that we are still fetching transactions
|
||||
except (UserError, ValidationError) as e:
|
||||
self.env.cr.rollback()
|
||||
online_account.account_online_link_id._log_information('error', subject=_("Error"), message=str(e))
|
||||
self.env.cr.commit()
|
||||
raise
|
||||
return lines_to_reconcile
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from requests.exceptions import RequestException, Timeout
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError, RedirectWarning
|
||||
from odoo.tools import SQL
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
def __get_bank_statements_available_sources(self):
|
||||
rslt = super(AccountJournal, self).__get_bank_statements_available_sources()
|
||||
rslt.append(("online_sync", _("Online Synchronization")))
|
||||
return rslt
|
||||
|
||||
next_link_synchronization = fields.Datetime("Online Link Next synchronization", related='account_online_link_id.next_refresh')
|
||||
expiring_synchronization_date = fields.Date(related='account_online_link_id.expiring_synchronization_date')
|
||||
expiring_synchronization_due_day = fields.Integer(compute='_compute_expiring_synchronization_due_day')
|
||||
account_online_account_id = fields.Many2one('account.online.account', copy=False, ondelete='set null')
|
||||
account_online_link_id = fields.Many2one('account.online.link', related='account_online_account_id.account_online_link_id', readonly=True, store=True)
|
||||
account_online_link_state = fields.Selection(related="account_online_link_id.state", readonly=True)
|
||||
renewal_contact_email = fields.Char(
|
||||
string='Connection Requests',
|
||||
help='Comma separated list of email addresses to send consent renewal notifications 15, 3 and 1 days before expiry',
|
||||
default=lambda self: self.env.user.email,
|
||||
)
|
||||
online_sync_fetching_status = fields.Selection(related="account_online_account_id.fetching_status", readonly=True)
|
||||
|
||||
def write(self, vals):
|
||||
# When changing the bank_statement_source, unlink the connection if there is any
|
||||
if 'bank_statements_source' in vals and vals.get('bank_statements_source') != 'online_sync':
|
||||
for journal in self:
|
||||
if journal.bank_statements_source == 'online_sync':
|
||||
# unlink current connection
|
||||
vals['account_online_account_id'] = False
|
||||
journal.account_online_link_id.has_unlinked_accounts = True
|
||||
return super().write(vals)
|
||||
|
||||
@api.depends('expiring_synchronization_date')
|
||||
def _compute_expiring_synchronization_due_day(self):
|
||||
for record in self:
|
||||
if record.expiring_synchronization_date:
|
||||
due_day_delta = record.expiring_synchronization_date - fields.Date.context_today(record)
|
||||
record.expiring_synchronization_due_day = due_day_delta.days
|
||||
else:
|
||||
record.expiring_synchronization_due_day = 0
|
||||
|
||||
def _fill_bank_cash_dashboard_data(self, dashboard_data):
|
||||
super()._fill_bank_cash_dashboard_data(dashboard_data)
|
||||
# Caching data to avoid one call per journal
|
||||
self.browse(list(dashboard_data.keys())).fetch(['type', 'account_online_account_id'])
|
||||
for journal_id, journal_data in dashboard_data.items():
|
||||
journal = self.browse(journal_id)
|
||||
journal_data['display_connect_bank_in_dashboard'] = journal.type in ('bank', 'credit') \
|
||||
and not journal.account_online_account_id \
|
||||
and journal.company_id.id == self.env.company.id
|
||||
|
||||
@api.constrains('account_online_account_id')
|
||||
def _check_account_online_account_id(self):
|
||||
for journal in self:
|
||||
if len(journal.account_online_account_id.journal_ids) > 1:
|
||||
raise ValidationError(_('You cannot have two journals associated with the same Online Account.'))
|
||||
|
||||
def _fetch_online_transactions(self):
|
||||
for journal in self:
|
||||
try:
|
||||
journal.account_online_link_id._pop_connection_state_details(journal=journal)
|
||||
journal.manual_sync()
|
||||
# for cron jobs it is usually recommended committing after each iteration,
|
||||
# so that a later error or job timeout doesn't discard previous work
|
||||
self.env.cr.commit()
|
||||
except (UserError, RedirectWarning):
|
||||
# We need to rollback here otherwise the next iteration will still have the error when trying to commit
|
||||
self.env.cr.rollback()
|
||||
|
||||
def fetch_online_sync_favorite_institutions(self):
|
||||
self.ensure_one()
|
||||
timeout = int(self.env['ir.config_parameter'].sudo().get_param('odex30_account_online_sync.request_timeout')) or 60
|
||||
endpoint_url = self.env['account.online.link']._get_odoofin_url('/proxy/v1/get_dashboard_institutions')
|
||||
params = {'country': self.sudo().company_id.account_fiscal_country_id.code, 'limit': 28}
|
||||
try:
|
||||
resp = requests.post(endpoint_url, json=params, timeout=timeout)
|
||||
resp_dict = resp.json()['result']
|
||||
for institution in resp_dict:
|
||||
if institution['picture'].startswith('/'):
|
||||
institution['picture'] = self.env['account.online.link']._get_odoofin_url(institution['picture'])
|
||||
return resp_dict
|
||||
except (Timeout, ConnectionError, RequestException, ValueError) as e:
|
||||
_logger.warning(e)
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def _cron_fetch_waiting_online_transactions(self):
|
||||
""" This method is only called when the user fetch transactions asynchronously.
|
||||
We only fetch transactions on synchronizations that are in "waiting" status.
|
||||
Once the synchronization is done, the status is changed for "done".
|
||||
We have to that to avoid having too much logic in the same cron function to do
|
||||
2 different things. This cron should only be used for asynchronous fetchs.
|
||||
"""
|
||||
|
||||
# 'limit_time_real_cron' and 'limit_time_real' default respectively to -1 and 120.
|
||||
# Manual fallbacks applied for non-POSIX systems where this key is disabled (set to None).
|
||||
limit_time = tools.config['limit_time_real_cron'] or -1
|
||||
if limit_time <= 0:
|
||||
limit_time = tools.config['limit_time_real'] or 120
|
||||
journals = self.search([
|
||||
'|',
|
||||
('online_sync_fetching_status', 'in', ('planned', 'waiting')),
|
||||
'&',
|
||||
('online_sync_fetching_status', '=', 'processing'),
|
||||
('account_online_link_id.last_refresh', '<', fields.Datetime.now() - relativedelta(seconds=limit_time)),
|
||||
])
|
||||
journals.with_context(cron=True)._fetch_online_transactions()
|
||||
|
||||
@api.model
|
||||
def _cron_fetch_online_transactions(self):
|
||||
""" This method is called by the cron (by default twice a day) to fetch (for all journals)
|
||||
the new transactions.
|
||||
"""
|
||||
journals = self.search([('account_online_account_id', '!=', False)])
|
||||
journals.with_context(cron=True)._fetch_online_transactions()
|
||||
|
||||
@api.model
|
||||
def _cron_send_reminder_email(self):
|
||||
for journal in self.search([('account_online_account_id', '!=', False)]):
|
||||
if journal.expiring_synchronization_due_day in {1, 3, 15}:
|
||||
journal.action_send_reminder()
|
||||
|
||||
def manual_sync(self):
|
||||
self.ensure_one()
|
||||
if self.account_online_link_id:
|
||||
account = self.account_online_account_id
|
||||
return self.account_online_link_id._fetch_transactions(accounts=account)
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
Override of the unlink method.
|
||||
That's useful to unlink account.online.account too.
|
||||
"""
|
||||
if self.account_online_account_id:
|
||||
self.account_online_account_id.unlink()
|
||||
return super(AccountJournal, self).unlink()
|
||||
|
||||
def action_configure_bank_journal(self):
|
||||
"""
|
||||
Override the "action_configure_bank_journal" and change the flow for the
|
||||
"Configure" button in dashboard.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.env['account.online.link'].action_new_synchronization()
|
||||
|
||||
def action_open_account_online_link(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': self.account_online_link_id.name,
|
||||
'res_model': 'account.online.link',
|
||||
'target': 'main',
|
||||
'view_mode': 'form',
|
||||
'views': [[False, 'form']],
|
||||
'res_id': self.account_online_link_id.id,
|
||||
}
|
||||
|
||||
def action_extend_consent(self):
|
||||
"""
|
||||
Extend the consent of the user by redirecting him to update his credentials
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.account_online_link_id._open_iframe(
|
||||
mode='updateCredentials',
|
||||
include_param={
|
||||
'account_online_identifier': self.account_online_account_id.online_identifier,
|
||||
},
|
||||
)
|
||||
|
||||
def action_reconnect_online_account(self):
|
||||
self.ensure_one()
|
||||
return self.account_online_link_id.action_reconnect_account()
|
||||
|
||||
def action_send_reminder(self):
|
||||
self.ensure_one()
|
||||
self._portal_ensure_token()
|
||||
template = self.env.ref('odex30_account_online_sync.email_template_sync_reminder')
|
||||
subtype = self.env.ref('odex30_account_online_sync.bank_sync_consent_renewal')
|
||||
self.message_post_with_source(source_ref=template, subtype_id=subtype.id)
|
||||
|
||||
def action_open_missing_transaction_wizard(self):
|
||||
""" This method allows to open the wizard to fetch the missing
|
||||
transactions and the pending ones.
|
||||
Depending on where the function is called, we'll receive
|
||||
one journal or none of them.
|
||||
If we receive more or less than one journal, we do not set
|
||||
it on the wizard, the user should select it by himself.
|
||||
|
||||
:return: An action opening the wizard.
|
||||
"""
|
||||
journal_id = None
|
||||
if len(self) == 1:
|
||||
if not self.account_online_account_id or self.account_online_link_state != 'connected':
|
||||
raise UserError(_("You can't find missing transactions for a journal that isn't connected."))
|
||||
|
||||
journal_id = self.id
|
||||
|
||||
wizard = self.env['account.missing.transaction.wizard'].create({'journal_id': journal_id})
|
||||
return {
|
||||
'name': _("Find Missing Transactions"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.missing.transaction.wizard',
|
||||
'res_id': wizard.id,
|
||||
'views': [(False, 'form')],
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_open_duplicate_transaction_wizard(self, from_date=None):
|
||||
""" This method allows to open the wizard to find duplicate transactions.
|
||||
:param from_date: date from with we must check for duplicates.
|
||||
|
||||
:return: An action opening the wizard.
|
||||
"""
|
||||
wizard = self.env['account.duplicate.transaction.wizard'].create({
|
||||
'journal_id': self.id if len(self) == 1 else None,
|
||||
**({'date': from_date} if from_date else {}),
|
||||
})
|
||||
return wizard._get_records_action(name=_("Find Duplicate Transactions"))
|
||||
|
||||
def _has_duplicate_transactions(self, date_from):
|
||||
""" Has any transaction with
|
||||
- same amount &
|
||||
- same date &
|
||||
- same account number
|
||||
We do not check on online_transaction_identifier because this is called after the fetch
|
||||
where transitions would already have been filtered on existing online_transaction_identifier.
|
||||
|
||||
:param from_date: date from with we must check for duplicates.
|
||||
"""
|
||||
self.env.cr.execute(SQL.join(SQL(''), [
|
||||
self._get_duplicate_amount_date_account_transactions_query(date_from),
|
||||
SQL('LIMIT 1'),
|
||||
]))
|
||||
return bool(self.env.cr.rowcount)
|
||||
|
||||
def _get_duplicate_transactions(self, date_from):
|
||||
"""Find all transaction with
|
||||
- same amount &
|
||||
- same date &
|
||||
- same account number
|
||||
or
|
||||
- same transaction id
|
||||
|
||||
:param from_date: date from with we must check for duplicates.
|
||||
"""
|
||||
query = SQL.join(SQL(''), [
|
||||
self._get_duplicate_amount_date_account_transactions_query(date_from),
|
||||
SQL('UNION'),
|
||||
self._get_duplicate_online_transaction_identifier_transactions_query(date_from),
|
||||
SQL('ORDER BY ids'),
|
||||
])
|
||||
return [res[0] for res in self.env.execute_query(query)]
|
||||
|
||||
def _get_duplicate_amount_date_account_transactions_query(self, date_from):
|
||||
self.ensure_one()
|
||||
return SQL('''
|
||||
SELECT ARRAY_AGG(st_line.id ORDER BY st_line.id) AS ids
|
||||
FROM account_bank_statement_line st_line
|
||||
JOIN account_move move ON move.id = st_line.move_id
|
||||
WHERE st_line.journal_id = %(journal_id)s AND move.date >= %(date_from)s
|
||||
GROUP BY st_line.currency_id, st_line.amount, st_line.account_number, move.date
|
||||
HAVING count(st_line.id) > 1
|
||||
''',
|
||||
journal_id=self.id,
|
||||
date_from=date_from,
|
||||
)
|
||||
|
||||
def _get_duplicate_online_transaction_identifier_transactions_query(self, date_from):
|
||||
return SQL('''
|
||||
SELECT ARRAY_AGG(st_line.id ORDER BY st_line.id) AS ids
|
||||
FROM account_bank_statement_line st_line
|
||||
JOIN account_move move ON move.id = st_line.move_id
|
||||
WHERE st_line.journal_id = %(journal_id)s AND
|
||||
move.date >= %(prior_date)s AND
|
||||
st_line.online_transaction_identifier IS NOT NULL
|
||||
GROUP BY st_line.online_transaction_identifier
|
||||
HAVING count(st_line.id) > 1 AND BOOL_OR(move.date >= %(date_from)s) -- at least one date is > date_from
|
||||
''',
|
||||
journal_id=self.id,
|
||||
date_from=date_from,
|
||||
prior_date=date_from - relativedelta(months=3), # allow 1 of duplicate statements to be older than "from" date
|
||||
)
|
||||
|
||||
def action_open_dashboard_asynchronous_action(self):
|
||||
""" This method allows to open action asynchronously
|
||||
during the fetching process.
|
||||
When a user clicks on the Fetch Transactions button in
|
||||
the dashboard, we fetch the transactions asynchronously
|
||||
and save connection state details on the synchronization.
|
||||
This action allows the user to open the action saved in
|
||||
the connection state details.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.account_online_account_id:
|
||||
raise UserError(_("You can only execute this action for bank-synchronized journals."))
|
||||
|
||||
connection_state_details = self.account_online_link_id._pop_connection_state_details(journal=self)
|
||||
if connection_state_details and connection_state_details.get('action'):
|
||||
if connection_state_details.get('error_type') == 'redirect_warning':
|
||||
self.env.cr.commit()
|
||||
raise RedirectWarning(connection_state_details['error_message'], connection_state_details['action'], _('Report Issue'))
|
||||
else:
|
||||
return connection_state_details['action']
|
||||
|
||||
return {'type': 'ir.actions.client', 'tag': 'soft_reload'}
|
||||
|
||||
def _get_journal_dashboard_data_batched(self):
|
||||
dashboard_data = super()._get_journal_dashboard_data_batched()
|
||||
for journal in self.filtered(lambda j: j.type in ('bank', 'credit')):
|
||||
if journal.account_online_account_id:
|
||||
if journal.company_id.id not in self.env.companies.ids:
|
||||
continue
|
||||
connection_state_details = journal.account_online_link_id._get_connection_state_details(journal=journal)
|
||||
if not connection_state_details and journal.account_online_account_id.fetching_status in ('waiting', 'processing'):
|
||||
connection_state_details = {'status': 'fetching'}
|
||||
dashboard_data[journal.id]['connection_state_details'] = connection_state_details
|
||||
dashboard_data[journal.id]['show_sync_actions'] = journal.account_online_link_id.show_sync_actions
|
||||
return dashboard_data
|
||||
|
||||
def get_related_connection_state_details(self):
|
||||
""" This method allows JS widget to get the last connection state details
|
||||
It's useful if the user wasn't on the dashboard when we send the message
|
||||
by websocket that the asynchronous flow is finished.
|
||||
In case we don't have a connection state details and if the fetching
|
||||
status is set on "waiting" or "processing". We're returning that the sync
|
||||
is currently fetching.
|
||||
"""
|
||||
self.ensure_one()
|
||||
connection_state_details = self.account_online_link_id._get_connection_state_details(journal=self)
|
||||
if not connection_state_details and self.account_online_account_id.fetching_status in ('waiting', 'processing'):
|
||||
connection_state_details = {'status': 'fetching'}
|
||||
return connection_state_details
|
||||
|
||||
def _consume_connection_state_details(self):
|
||||
self.ensure_one()
|
||||
if self.account_online_link_id and self.env.user.has_group('account.group_account_manager'):
|
||||
# In case we have a bank synchronization connected to the journal
|
||||
# we want to remove the last connection state because it means that we
|
||||
# have "mark as read" this state, and we don't want to display it again to
|
||||
# the user.
|
||||
self.account_online_link_id._pop_connection_state_details(journal=self)
|
||||
|
||||
def open_action(self):
|
||||
# Extends 'account_accountant'
|
||||
if not self._context.get('action_name') and self.type == 'bank' and self.bank_statements_source == 'online_sync':
|
||||
self._consume_connection_state_details()
|
||||
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
|
||||
default_context={'search_default_journal_id': self.id},
|
||||
)
|
||||
return super().open_action()
|
||||
|
||||
def action_open_reconcile(self):
|
||||
# Extends 'account_accountant'
|
||||
self._consume_connection_state_details()
|
||||
return super().action_open_reconcile()
|
||||
|
||||
def action_open_bank_transactions(self):
|
||||
# Extends 'account_accountant'
|
||||
self._consume_connection_state_details()
|
||||
return super().action_open_bank_transactions()
|
||||
|
||||
@api.model
|
||||
def _toggle_asynchronous_fetching_cron(self):
|
||||
cron = self.env.ref('odex30_account_online_sync.online_sync_cron_waiting_synchronization', raise_if_not_found=False)
|
||||
if cron:
|
||||
cron.sudo().toggle(model=self._name, domain=[('account_online_account_id', '!=', False)])
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class BankRecWidget(models.Model):
|
||||
_inherit = 'bank.rec.widget'
|
||||
|
||||
def _action_validate(self):
|
||||
# EXTENDS account_accountant
|
||||
super()._action_validate()
|
||||
line = self.st_line_id
|
||||
if line.partner_id and line.online_partner_information:
|
||||
# write value for account and merchant on partner only if partner has no value,
|
||||
# in case value are different write False
|
||||
value_merchant = line.partner_id.online_partner_information or line.online_partner_information
|
||||
value_merchant = value_merchant if value_merchant == line.online_partner_information else False
|
||||
line.partner_id.online_partner_information = value_merchant
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
@api.model
|
||||
def setting_init_bank_account_action(self):
|
||||
"""
|
||||
Override the "setting_init_bank_account_action" in accounting menu
|
||||
and change the flow for the "Add a bank account" menu item in dashboard.
|
||||
"""
|
||||
return self.env['account.online.link'].action_new_synchronization()
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
from odoo import api, models
|
||||
|
||||
|
||||
class MailActivityType(models.Model):
|
||||
_inherit = "mail.activity.type"
|
||||
|
||||
@api.model
|
||||
def _get_model_info_by_xmlid(self):
|
||||
info = super()._get_model_info_by_xmlid()
|
||||
info['odex30_account_online_sync.bank_sync_activity_update_consent'] = {
|
||||
'res_model': 'account.journal',
|
||||
'unlink': False,
|
||||
}
|
||||
return info
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import werkzeug.urls
|
||||
|
||||
|
||||
class OdooFinAuth(requests.auth.AuthBase):
|
||||
|
||||
def __init__(self, record=None):
|
||||
self.access_token = record and record.access_token or False
|
||||
self.refresh_token = record and record.refresh_token or False
|
||||
self.client_id = record and record.client_id or False
|
||||
|
||||
def __call__(self, request):
|
||||
# We don't sign request that still don't have a client_id/refresh_token
|
||||
if not self.client_id or not self.refresh_token:
|
||||
return request
|
||||
# craft the message (timestamp|url path|client_id|access_token|query params|body content)
|
||||
msg_timestamp = int(time.time())
|
||||
parsed_url = werkzeug.urls.url_parse(request.path_url)
|
||||
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
body = json.loads(body)
|
||||
|
||||
message = '%s|%s|%s|%s|%s|%s' % (
|
||||
msg_timestamp, # timestamp
|
||||
parsed_url.path, # url path
|
||||
self.client_id,
|
||||
self.access_token,
|
||||
json.dumps(werkzeug.urls.url_decode(parsed_url.query), sort_keys=True), # url query params sorted by key
|
||||
json.dumps(body, sort_keys=True)) # http request body
|
||||
|
||||
h = hmac.new(base64.b64decode(self.refresh_token), message.encode('utf-8'), digestmod=hashlib.sha256)
|
||||
|
||||
request.headers.update({
|
||||
'odoofin-client-id': self.client_id,
|
||||
'odoofin-access-token': self.access_token,
|
||||
'odoofin-signature': base64.b64encode(h.digest()),
|
||||
'odoofin-timestamp': msg_timestamp,
|
||||
})
|
||||
return request
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
online_partner_information = fields.Char(readonly=True)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record model="ir.rule" id="account_online_sync_link_rule">
|
||||
<field name="name">Account online link company rule</field>
|
||||
<field name="model_id" ref="model_account_online_link"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id', 'parent_of', company_ids)]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="account_online_sync_account_rule">
|
||||
<field name="name">Online account company rule</field>
|
||||
<field name="model_id" ref="model_account_online_account"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('account_online_link_id.company_id','parent_of', company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_online_link_id,access_account_online_link_id,model_account_online_link,account.group_account_basic,1,1,1,0
|
||||
access_account_online_link_id_readonly,access_account_online_link_id_readonly,model_account_online_link,account.group_account_readonly,1,0,0,0
|
||||
access_account_online_link_id_manager,access_account_online_link_id manager,model_account_online_link,account.group_account_manager,1,1,1,1
|
||||
access_account_online_account_id,access_account_online_account_id,model_account_online_account,account.group_account_basic,1,1,1,0
|
||||
access_account_online_account_id_readonly,access_account_online_account_id_readonly,model_account_online_account,account.group_account_readonly,1,0,0,0
|
||||
access_account_online_account_id_manager,access_account_online_account_id manager,model_account_online_account,account.group_account_manager,1,1,1,1
|
||||
access_account_bank_selection_manager,access.account.bank.selection manager,model_account_bank_selection,account.group_account_manager,1,1,1,1
|
||||
access_account_bank_selection,access.account.bank.selection basic,model_account_bank_selection,account.group_account_basic,1,1,1,0
|
||||
access_account_bank_statement_line_transient,access_account_bank_statement_line_transient,model_account_bank_statement_line_transient,account.group_account_manager,1,1,1,1
|
||||
access_account_missing_transaction_wizard,access_account_missing_transaction_wizard,model_account_missing_transaction_wizard,account.group_account_manager,1,1,1,1
|
||||
access_account_duplicate_transaction_wizard,access_account_duplicate_transaction_wizard,model_account_duplicate_transaction_wizard,account.group_account_user,1,1,1,0
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useCheckDuplicateService } from "./account_duplicate_transaction_hook";
|
||||
|
||||
export class AccountDuplicateTransactionsFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.duplicateCheckService = useCheckDuplicateService();
|
||||
}
|
||||
|
||||
async beforeExecuteActionButton(clickParams) {
|
||||
if (clickParams.name === "delete_selected_transactions") {
|
||||
const selected = this.duplicateCheckService.selectedLines;
|
||||
if (selected.size) {
|
||||
await this.orm.call(
|
||||
"account.bank.statement.line",
|
||||
"unlink",
|
||||
[Array.from(selected)],
|
||||
);
|
||||
this.env.services.action.doAction({type: 'ir.actions.client', tag: 'reload'});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return super.beforeExecuteActionButton(...arguments);
|
||||
}
|
||||
|
||||
get cogMenuProps() {
|
||||
const props = super.cogMenuProps;
|
||||
props.items.action = [];
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
export const form = { ...formView, Controller: AccountDuplicateTransactionsFormController };
|
||||
|
||||
registry.category("views").add("account_duplicate_transactions_form", form);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useState } from "@odoo/owl";
|
||||
|
||||
export function useCheckDuplicateService() {
|
||||
return useState(useService("odex30_account_online_sync.duplicate_check_service"));
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
class AccountDuplicateTransactionsServiceModel {
|
||||
constructor() {
|
||||
this.selectedLines = new Set();
|
||||
}
|
||||
|
||||
updateLIne(selected, id) {
|
||||
this.selectedLines[selected ? "add" : "delete"](id);
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateCheckService = {
|
||||
start(env, services) {
|
||||
return new AccountDuplicateTransactionsServiceModel();
|
||||
},
|
||||
};
|
||||
|
||||
registry
|
||||
.category("services")
|
||||
.add("odex30_account_online_sync.duplicate_check_service", duplicateCheckService);
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { onMounted } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { useCheckDuplicateService } from "./account_duplicate_transaction_hook";
|
||||
|
||||
export class AccountDuplicateTransactionsListRenderer extends ListRenderer {
|
||||
static template = "odex30_account_online_sync.AccountDuplicateTransactionsListRenderer";
|
||||
static recordRowTemplate = "odex30_account_online_sync.AccountDuplicateTransactionsRecordRow";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.duplicateCheckService = useCheckDuplicateService();
|
||||
|
||||
onMounted(() => {
|
||||
this.deleteButton = document.querySelector('button[name="delete_selected_transactions"]');
|
||||
this.deleteButton.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
toggleRecordSelection(selected, record) {
|
||||
this.duplicateCheckService.updateLIne(selected, record.data.id);
|
||||
this.deleteButton.disabled = this.duplicateCheckService.selectedLines.size === 0;
|
||||
}
|
||||
|
||||
get hasSelectors() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getRowClass(record) {
|
||||
let classes = super.getRowClass(record);
|
||||
const firstIdsInGroup = this.env.model.root.data.first_ids_in_group;
|
||||
if (firstIdsInGroup instanceof Array && firstIdsInGroup.includes(record.data.id)) {
|
||||
classes += " account_duplicate_transactions_lines_list_x2many_group_line";
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountDuplicateTransactionsLinesListX2ManyField extends X2ManyField {
|
||||
static components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: AccountDuplicateTransactionsListRenderer,
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("fields").add("account_duplicate_transactions_lines_list_x2many", {
|
||||
...x2ManyField,
|
||||
component: AccountDuplicateTransactionsLinesListX2ManyField,
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.account_duplicate_transactions_lines_list_x2many_group_line {
|
||||
border-top-width: thick;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="odex30_account_online_sync.AccountDuplicateTransactionsListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
|
||||
<xpath expr="//th[1]" position="replace"><th><i class="fa fa-trash"/></th></xpath>
|
||||
<xpath expr="//tfoot" position="replace"></xpath>
|
||||
</t>
|
||||
<t t-name="odex30_account_online_sync.AccountDuplicateTransactionsRecordRow" t-inherit="web.ListRenderer.RecordRow" t-inherit-mode="primary">
|
||||
<xpath expr="//td[1]" position="replace">
|
||||
<td class="o_list_record_selector user-select-none" tabindex="-1">
|
||||
<CheckBox className="'d-flex m-0'" onChange.bind="(selected) => this.toggleRecordSelection(selected, record)"/>
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { useService, useBus } from "@web/core/utils/hooks";
|
||||
import { SIZES } from "@web/core/ui/ui_service";
|
||||
import { Component, useState, useRef, onWillStart } from "@odoo/owl";
|
||||
|
||||
class BankConfigureWidget extends Component {
|
||||
static template = "account.BankConfigureWidget";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
}
|
||||
setup() {
|
||||
this.container = useRef("container");
|
||||
this.allInstitutions = [];
|
||||
this.state = useState({
|
||||
isLoading: true,
|
||||
institutions: [],
|
||||
gridStyle: "grid-template-columns: repeat(5, minmax(90px, 1fr));"
|
||||
});
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.ui = useService("ui");
|
||||
onWillStart(this.fetchInstitutions);
|
||||
useBus(this.ui.bus, "resize", this.computeGrid);
|
||||
}
|
||||
|
||||
computeGrid() {
|
||||
if (this.allInstitutions.length > 4) {
|
||||
let containerWidth = this.container.el ? this.container.el.offsetWidth - 32 : 0;
|
||||
// when the container width can't be computed, use the screen size and number of journals.
|
||||
if (!containerWidth) {
|
||||
if (this.ui.size >= SIZES.XXL) {
|
||||
containerWidth = window.innerWidth / (this.props.record.model.root.count < 6 ? 2 : 3);
|
||||
} else {
|
||||
containerWidth = Math.max(this.ui.size * 100, 400);
|
||||
}
|
||||
}
|
||||
const canFit = Math.floor(containerWidth / 100);
|
||||
const numberOfRows = (Math.floor((this.allInstitutions.length + 1) / 2) >= canFit) + 1;
|
||||
this.state.gridStyle = `grid-template-columns: repeat(${canFit}, minmax(90px, 1fr));
|
||||
grid-template-rows: repeat(${numberOfRows}, 1fr);
|
||||
grid-auto-rows: 0px;
|
||||
`;
|
||||
}
|
||||
this.state.institutions = this.allInstitutions;
|
||||
}
|
||||
|
||||
async fetchInstitutions() {
|
||||
this.orm.silent.call(this.props.record.resModel, "fetch_online_sync_favorite_institutions", [this.props.record.resId])
|
||||
.then((response) => {
|
||||
this.allInstitutions = response;
|
||||
})
|
||||
.finally(() => {
|
||||
this.state.isLoading = false;
|
||||
this.computeGrid();
|
||||
});
|
||||
}
|
||||
|
||||
async connectBank(institutionId=null) {
|
||||
const action = await this.orm.call("account.online.link", "action_new_synchronization", [[]], {
|
||||
preferred_inst: institutionId,
|
||||
journal_id: this.props.record.resId,
|
||||
})
|
||||
this.action.doAction(action);
|
||||
}
|
||||
|
||||
async fallbackConnectBank() {
|
||||
const action = await this.orm.call('account.online.link', 'create_new_bank_account_action', [], {
|
||||
context: {
|
||||
active_model: 'account.journal',
|
||||
active_id: this.props.record.resId,
|
||||
}
|
||||
});
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const bankConfigureWidget = {
|
||||
component: BankConfigureWidget,
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("bank_configure", bankConfigureWidget);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
.bank_configure_container {
|
||||
.d-grid {
|
||||
overflow: hidden;
|
||||
column-gap: 0.25rem;
|
||||
}
|
||||
.dashboard_bank {
|
||||
aspect-ratio: 1 / 1;
|
||||
.align-self-center {
|
||||
background-color: $gray-100;
|
||||
border: 1px solid $gray-100;
|
||||
}
|
||||
margin-bottom: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<template>
|
||||
<t t-name="account.BankConfigureWidget">
|
||||
<div class="bank_configure_container d-flex flex-wrap" t-ref="container">
|
||||
<t t-if="state.isLoading">
|
||||
loading...
|
||||
</t>
|
||||
<span t-elif="!state.institutions.length">
|
||||
<button class="btn btn-primary" t-on-click="fallbackConnectBank" name="action_configure_bank_journal">Setup Bank</button>
|
||||
</span>
|
||||
<div class="d-grid" t-attf-style="{{state.gridStyle}}" t-if="!state.isLoading and state.institutions.length">
|
||||
<div class="dashboard_bank" t-on-click="() => this.connectBank()">
|
||||
<button class="h-100 w-100 rounded btn btn-primary fw-normal" name="action_configure_bank_journal">
|
||||
<span class="img-fluid">Search over <span class="text-nowrap">26 000</span> banks</span>
|
||||
</button>
|
||||
</div>
|
||||
<div t-foreach="state.institutions" t-as="institution" t-key="institution.id"
|
||||
t-attf-class="dashboard_bank"
|
||||
t-on-click="() => this.connectBank(institution.id)"
|
||||
>
|
||||
<button class="align-self-center h-100 w-100 rounded">
|
||||
<span t-if="institution.default_picture" t-out="institution.name"/>
|
||||
<img t-else="" class="img-fluid p-2" t-att-src="institution.picture" t-att-title="institution.name"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const cogMenuRegistry = registry.category("cogMenu");
|
||||
|
||||
/**
|
||||
* 'Fetch Missing Transactions' menu
|
||||
*
|
||||
* This component is used to open a wizard allowing the user to fetch their missing/pending
|
||||
* transaction since a specific date.
|
||||
* It's only available in the bank reconciliation widget.
|
||||
* By default, if there is only one selected journal, this journal is directly selected.
|
||||
* In case there is no selected journal or more than one, we let the user choose which
|
||||
* journal he/she wants. This part is handled by the server.
|
||||
* @extends Component
|
||||
*/
|
||||
export class FetchMissingTransactions extends Component {
|
||||
static template = "odex30_account_online_sync.FetchMissingTransactions";
|
||||
static components = { DropdownItem };
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Protected
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
async openFetchMissingTransactionsWizard() {
|
||||
const { context } = this.env.searchModel;
|
||||
const activeModel = context.active_model;
|
||||
let activeIds = [];
|
||||
if (activeModel === "account.journal") {
|
||||
activeIds = context.active_ids;
|
||||
} else if (!!context.default_journal_id) {
|
||||
activeIds = context.default_journal_id;
|
||||
}
|
||||
// We have to use this.env.services.orm.call instead of using useService
|
||||
// for a specific reason. useService implies that function calls with
|
||||
// are "protected", it means that if the component is closed the
|
||||
// response will be pending and the code stop their execution.
|
||||
// By passing directly from the env, this protection is not activated.
|
||||
const action = await this.env.services.orm.call(
|
||||
"account.journal",
|
||||
"action_open_missing_transaction_wizard",
|
||||
[activeIds]
|
||||
);
|
||||
return this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchMissingTransactionItem = {
|
||||
Component: FetchMissingTransactions,
|
||||
groupNumber: 5,
|
||||
isDisplayed: ({ config, isSmall }) => {
|
||||
return !isSmall &&
|
||||
config.actionType === "ir.actions.act_window" &&
|
||||
["kanban", "list"].includes(config.viewType) &&
|
||||
["bank_rec_widget_kanban", "bank_rec_list"].includes(config.viewSubType);
|
||||
},
|
||||
};
|
||||
|
||||
cogMenuRegistry.add("fetch-missing-transaction-menu", fetchMissingTransactionItem, { sequence: 1 });
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="odex30_account_online_sync.FetchMissingTransactions">
|
||||
<DropdownItem class="'o_import_menu'" onSelected.bind="openFetchMissingTransactionsWizard">
|
||||
Find Missing Transactions
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
/**
|
||||
* 'Find Duplicate Transactions' menu
|
||||
*
|
||||
* This component is used to open a wizard allowing the user to find duplicate
|
||||
* transactions since a specific date.
|
||||
* It's only available in the bank reconciliation widget.
|
||||
* By default, if there is only one selected journal, this journal is directly selected.
|
||||
* In case there is no selected journal or more than one, we let the user choose.
|
||||
* @extends Component
|
||||
*/
|
||||
export class FindDuplicateTransactions extends Component {
|
||||
static template = "odex30_account_online_sync.FindDuplicateTransactions";
|
||||
static components = { DropdownItem };
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Protected
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
async openFindDuplicateTransactionsWizard() {
|
||||
const { context } = this.env.searchModel;
|
||||
const activeModel = context.active_model;
|
||||
let activeIds = [];
|
||||
if (activeModel === "account.journal") {
|
||||
activeIds = context.active_ids;
|
||||
} else if (context.default_journal_id) {
|
||||
activeIds = context.default_journal_id;
|
||||
}
|
||||
return this.action.doActionButton({
|
||||
type: "object",
|
||||
resModel: "account.journal",
|
||||
name:"action_open_duplicate_transaction_wizard",
|
||||
resIds: activeIds,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const findDuplicateTransactionItem = {
|
||||
Component: FindDuplicateTransactions,
|
||||
groupNumber: 5, // same group as fetch missing transactions
|
||||
isDisplayed: ({ config, isSmall }) => {
|
||||
return (
|
||||
!isSmall &&
|
||||
config.actionType === "ir.actions.act_window" &&
|
||||
["kanban", "list"].includes(config.viewType) &&
|
||||
["bank_rec_widget_kanban", "bank_rec_list"].includes(config.viewSubType)
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("cogMenu").add(
|
||||
"find-duplicate-transaction-menu",
|
||||
findDuplicateTransactionItem,
|
||||
{ sequence: 3 }, // after fetch missing transactions
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="odex30_account_online_sync.FindDuplicateTransactions">
|
||||
<DropdownItem class="'o_import_menu'" onSelected.bind="openFindDuplicateTransactionsWizard">
|
||||
Find Duplicate Transactions
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
class ConnectedUntil extends Component {
|
||||
static template = "odex30_account_online_sync.ConnectedUntil";
|
||||
static props = { ...standardWidgetProps };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isHovered: false,
|
||||
displayReconnectButton: false,
|
||||
});
|
||||
|
||||
if (this.isConnectionExpiredIn(0)) {
|
||||
this.state.displayReconnectButton = true;
|
||||
}
|
||||
|
||||
this.action = useService("action");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
get cssClasses() {
|
||||
let cssClasses = "text-nowrap w-100";
|
||||
if (this.isConnectionExpiredIn(7)) {
|
||||
cssClasses += this.isConnectionExpiredIn(3) ? " text-danger" : " text-warning";
|
||||
}
|
||||
return cssClasses;
|
||||
}
|
||||
|
||||
onMouseEnter() {
|
||||
this.state.isHovered = true;
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.state.isHovered = false;
|
||||
}
|
||||
|
||||
isConnectionExpiredIn(nbDays) {
|
||||
return this.props.record.data.expiring_synchronization_due_day <= nbDays;
|
||||
}
|
||||
|
||||
async extendConnection() {
|
||||
const action = await this.orm.call(
|
||||
"account.journal",
|
||||
"action_extend_consent",
|
||||
[this.props.record.resId],
|
||||
{}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const connectedUntil = {
|
||||
component: ConnectedUntil,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("connected_until_widget", connectedUntil);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="odex30_account_online_sync.ConnectedUntil">
|
||||
<div t-on-mouseenter="onMouseEnter" t-on-mouseleave="onMouseLeave" t-att-class="cssClasses">
|
||||
<t t-if="state.displayReconnectButton">
|
||||
<button class="btn btn-danger" t-on-click="extendConnection">
|
||||
Reconnect Bank
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="!this.state.isHovered">
|
||||
<i class="fa fa-warning" t-if="isConnectionExpiredIn(3)"/>
|
||||
Connected until <t t-out="this.props.record.data.expiring_synchronization_date.toFormat('MMM dd')"/>
|
||||
</t>
|
||||
<t t-elif="this.state.isHovered">
|
||||
<a class="oe_inline oe_kanban_action" href="#" t-on-click="extendConnection">
|
||||
Extend Connection
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { onMounted, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
|
||||
|
||||
class OnlineAccountRadio extends RadioField {
|
||||
static template = "odex30_account_online_sync.OnlineAccountRadio";
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.state = useState({balances: {}});
|
||||
|
||||
onMounted(async () => {
|
||||
this.state.balances = await this.loadData();
|
||||
// Make sure the first option is selected by default.
|
||||
this.onChange(this.items[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
const ids = this.items.map(i => i[0]);
|
||||
return await this.orm.call("account.online.account", "get_formatted_balances", [ids]);
|
||||
}
|
||||
|
||||
getBalanceName(itemID) {
|
||||
return this.state.balances?.[itemID]?.[0] ?? "Loading ...";
|
||||
}
|
||||
|
||||
isNegativeAmount(itemID) {
|
||||
// In case of the value is undefined, it will return false as intended.
|
||||
return this.state.balances?.[itemID]?.[1] < 0;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("online_account_radio", {
|
||||
...radioField,
|
||||
component: OnlineAccountRadio,
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odex30_account_online_sync.OnlineAccountRadio">
|
||||
<div
|
||||
role="radiogroup"
|
||||
t-attf-class="o_{{ props.orientation }}"
|
||||
t-att-aria-label="string"
|
||||
>
|
||||
<t t-foreach="items" t-as="item" t-key="item[0]">
|
||||
<div class="form-check o_radio_item" aria-atomic="true">
|
||||
<input
|
||||
type="radio"
|
||||
class="form-check-input o_radio_input"
|
||||
t-att-checked="item[0] === value"
|
||||
t-att-disabled="props.readonly"
|
||||
t-att-name="id"
|
||||
t-att-data-value="item[0]"
|
||||
t-att-data-index="item_index"
|
||||
t-att-id="`${id}_${item[0]}`"
|
||||
t-on-change="() => this.onChange(item)"
|
||||
/>
|
||||
<label class="form-check-label o_form_label" t-att-for="`${id}_${item[0]}`" t-out="item[1]" />
|
||||
<br/>
|
||||
<p t-out="this.getBalanceName(item[0])" t-att-class="this.isNegativeAmount(item[0]) ? 'text-danger' : ''"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, useState, onWillStart, markup } from "@odoo/owl";
|
||||
|
||||
class RefreshSpin extends Component {
|
||||
static template = "odex30_account_online_sync.RefreshSpin";
|
||||
static props = { ...standardWidgetProps };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isHovered: false,
|
||||
fetchingStatus: false,
|
||||
connectionStateDetails: null,
|
||||
});
|
||||
|
||||
this.actionService = useService("action");
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.orm = useService("orm");
|
||||
this.state.fetchingStatus = this.props.record.data.online_sync_fetching_status;
|
||||
|
||||
this.busService.subscribe("online_sync", (notification) => {
|
||||
if (notification?.id === this.recordId && notification?.connection_state_details) {
|
||||
this.state.connectionStateDetails = notification.connection_state_details;
|
||||
}
|
||||
});
|
||||
|
||||
onWillStart(() => {
|
||||
this._initConnectionStateDetails();
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.actionService.restore(this.actionService.currentController.jsId);
|
||||
}
|
||||
|
||||
onMouseEnter() {
|
||||
this.state.isHovered = true;
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.state.isHovered = false;
|
||||
}
|
||||
|
||||
async openAction() {
|
||||
/**
|
||||
* This function is used to open the action that the asynchronous process saved
|
||||
* on the databsase. It allows users to call the action when they want and not when
|
||||
* the process is over.
|
||||
*/
|
||||
const action = await this.orm.call(
|
||||
"account.journal",
|
||||
"action_open_dashboard_asynchronous_action",
|
||||
[this.recordId],
|
||||
);
|
||||
this.actionService.doAction(action);
|
||||
this.state.connectionStateDetails = null;
|
||||
}
|
||||
|
||||
async fetchTransactions() {
|
||||
/**
|
||||
* This function call the function to fetch transactions.
|
||||
* In the main case, we don't do anything after calling the function.
|
||||
* The idea is that websockets will update the status by themselves.
|
||||
* In one specific case, we have to return an action to the user to open
|
||||
* the Odoo Fin iframe to refresh the connection.
|
||||
*/
|
||||
this.state.connectionStateDetails = { status: "fetching" };
|
||||
const action = await this.orm.call("account.journal", "manual_sync", [this.recordId]);
|
||||
if (action) {
|
||||
action.help = markup(action.help);
|
||||
this.actionService.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
_initConnectionStateDetails() {
|
||||
/**
|
||||
* This function is used to get the last state of the connection (if there is one)
|
||||
*/
|
||||
const kanbanDashboardData = JSON.parse(this.props.record.data.kanban_dashboard);
|
||||
this.state.connectionStateDetails = kanbanDashboardData?.connection_state_details;
|
||||
}
|
||||
|
||||
get recordId() {
|
||||
return this.props.record.data.id;
|
||||
}
|
||||
|
||||
get connectionStatus() {
|
||||
return this.state.connectionStateDetails?.status;
|
||||
}
|
||||
}
|
||||
|
||||
export const refreshSpin = {
|
||||
component: RefreshSpin,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("refresh_spin_widget", refreshSpin);
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="odex30_account_online_sync.RefreshSpin">
|
||||
<t t-if="this.connectionStatus === 'success'">
|
||||
<div>
|
||||
<t t-if="!!this.state.connectionStateDetails.nb_fetched_transactions" >
|
||||
<a href="#" t-on-click.prevent.stop="openAction">
|
||||
<t t-out="this.state.connectionStateDetails.nb_fetched_transactions"/> transactions fetched
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
0 transaction fetched
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="this.connectionStatus === 'error'">
|
||||
<div>
|
||||
<a t-if="this.state.connectionStateDetails.error_type === 'redirect_warning'" class="text-danger" href="#" t-on-click.prevent.stop="openAction">
|
||||
<i class="fa fa-warning"/> See error
|
||||
</a>
|
||||
<button t-else="" class="btn btn-danger" t-on-click.prevent.stop="openAction">
|
||||
Reconnect Bank
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="this.connectionStatus === 'fetching'">
|
||||
<div t-on-mouseenter="onMouseEnter" t-on-mouseleave="onMouseLeave">
|
||||
<t t-if="this.state.isHovered">
|
||||
<a class="oe_inline oe_kanban_action" href="#" t-on-click="refresh">Refresh</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Fetching... <i class='fa fa-spinner fa-spin'/></span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a href="#" t-on-click.prevent.stop="fetchTransactions" class="oe_inline">
|
||||
Fetch Transactions
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class TransientBankStatementLineListController extends ListController {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
async onClickImportTransactions() {
|
||||
const resIds = await this.getSelectedResIds();
|
||||
const resultAction = await this.orm.call("account.bank.statement.line.transient", "action_import_transactions", [resIds]);
|
||||
this.action.doAction(resultAction);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransientBankStatementLineListRenderer extends ListRenderer {
|
||||
|
||||
static template = "odex30_account_online_sync.TransientBankStatementLineRenderer";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
async openManualEntries() {
|
||||
if (this.env.searchModel.context.active_model === "account.missing.transaction.wizard" && this.env.searchModel.context.active_ids) {
|
||||
const activeIds = this.env.searchModel.context.active_ids;
|
||||
const action = await this.orm.call("account.missing.transaction.wizard", "action_open_manual_bank_statement_lines", activeIds);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const TransientBankStatementLineListView = {
|
||||
...listView,
|
||||
Renderer: TransientBankStatementLineListRenderer,
|
||||
Controller: TransientBankStatementLineListController,
|
||||
buttonTemplate: "TransientBankStatementLineButtonTemplate",
|
||||
}
|
||||
|
||||
registry.category("views").add("transient_bank_statement_line_list_view", TransientBankStatementLineListView);
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odex30_account_online_sync.TransientBankStatementLineRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
|
||||
<xpath expr="//table" position="before">
|
||||
<t t-if="env.searchModel.context.has_manual_entries">
|
||||
<div class="alert alert-warning text-center mb-0" role="alert">
|
||||
You have <a href="" t-on-click.prevent="() => this.openManualEntries()">entries</a>
|
||||
within this period that were not created using the online synchronization. This might cause duplicate entries.
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="env.searchModel.context.is_fetch_before_creation">
|
||||
<div class="alert alert-warning text-center mb-0" role="alert">
|
||||
You are importing transactions before the creation of your online synchronization
|
||||
(<t t-out="env.searchModel.context.account_online_link_create_date"/>).
|
||||
This might cause duplicate entries.
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="TransientBankStatementLineButtonTemplate" t-inherit="web.ListView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
|
||||
<button type="button" class="btn btn-primary" data-hotkey="i" t-on-click.stop="onClickImportTransactions">
|
||||
Import Transactions
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { BankRecKanbanController } from "@odex30_account_accountant/components/bank_reconciliation/kanban";
|
||||
|
||||
patch(BankRecKanbanController.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.displayDuplicateWarning = !!this.props.context.duplicates_from_date;
|
||||
},
|
||||
async onWarningClick () {
|
||||
const { context } = this.env.searchModel;
|
||||
return this.action.doActionButton({
|
||||
type: "object",
|
||||
resModel: "account.journal",
|
||||
name:"action_open_duplicate_transaction_wizard",
|
||||
resId: this.state.journalId,
|
||||
args: JSON.stringify([context.duplicates_from_date]),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odex30_account_online_sync.AOSKanbanView" t-inherit="account.BankRecoKanbanController" t-inherit-mode="extension">
|
||||
<xpath expr="//Layout" position="before">
|
||||
<t t-if="displayDuplicateWarning">
|
||||
<div class="alert alert-warning text-center mb-0" role="alert">
|
||||
Some transactions <a class="alert-link" href="" t-on-click.prevent="onWarningClick">may be duplicates.</a>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { loadJS } from "@web/core/assets";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { markup } from "@odoo/owl";
|
||||
const actionRegistry = registry.category('actions');
|
||||
/* global OdooFin */
|
||||
|
||||
function OdooFinConnector(parent, action) {
|
||||
const orm = parent.services.orm;
|
||||
const actionService = parent.services.action;
|
||||
const notificationService = parent.services.notification;
|
||||
const debugMode = parent.debug;
|
||||
|
||||
const id = action.id;
|
||||
action.params.colorScheme = cookie.get("color_scheme");
|
||||
let mode = action.params.mode || 'link';
|
||||
// Ensure that the proxyMode is valid
|
||||
const modeRegexp = /^[a-z0-9-_]+$/;
|
||||
const runbotRegexp = /^https:\/\/[a-z0-9-_]+\.[a-z0-9-_]+\.odoo\.com$/;
|
||||
if (!modeRegexp.test(action.params.proxyMode) && !runbotRegexp.test(action.params.proxyMode)) {
|
||||
return;
|
||||
}
|
||||
let url = 'https://' + action.params.proxyMode + '.odoofin.com/proxy/v1/odoofin_link';
|
||||
if (runbotRegexp.test(action.params.proxyMode)) {
|
||||
url = action.params.proxyMode + '/proxy/v1/odoofin_link';
|
||||
}
|
||||
let actionResult = false;
|
||||
|
||||
loadJS(url)
|
||||
.then(function () {
|
||||
// Create and open the iframe
|
||||
const params = {
|
||||
data: action.params,
|
||||
proxyMode: action.params.proxyMode,
|
||||
onEvent: async function (event, data) {
|
||||
switch (event) {
|
||||
case 'close':
|
||||
return;
|
||||
case 'reload':
|
||||
return actionService.doAction({type: 'ir.actions.client', tag: 'reload'});
|
||||
case 'notification':
|
||||
notificationService.add(data.message, data);
|
||||
break;
|
||||
case 'exchange_token':
|
||||
await orm.call('account.online.link', 'exchange_token',
|
||||
[[id], data], {context: action.context});
|
||||
break;
|
||||
case 'success':
|
||||
mode = data.mode || mode;
|
||||
actionResult = await orm.call('account.online.link', 'success', [[id], mode, data], {context: action.context});
|
||||
actionResult.help = markup(actionResult.help)
|
||||
return actionService.doAction(actionResult);
|
||||
case 'connect_existing_account':
|
||||
actionResult = await orm.call('account.online.link', 'connect_existing_account', [data], {context: action.context});
|
||||
actionResult.help = markup(actionResult.help)
|
||||
return actionService.doAction(actionResult);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
onAddBank: async function (data) {
|
||||
// If the user doesn't find his bank
|
||||
actionResult = await orm.call(
|
||||
"account.online.link",
|
||||
"create_new_bank_account_action",
|
||||
[[id], data],
|
||||
{ context: action.context }
|
||||
);
|
||||
return actionService.doAction(actionResult);
|
||||
}
|
||||
};
|
||||
// propagate parent debug mode to iframe
|
||||
if (typeof debugMode !== "undefined" && debugMode) {
|
||||
params.data["debug"] = debugMode;
|
||||
}
|
||||
OdooFin.create(params);
|
||||
OdooFin.open();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
actionRegistry.add('odoo_fin_connector', OdooFinConnector);
|
||||
|
||||
export default OdooFinConnector;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { loadJS } from "@web/core/assets";
|
||||
/* global OdooFin */
|
||||
|
||||
publicWidget.registry.OnlineSyncPortal = publicWidget.Widget.extend({
|
||||
selector: '.oe_online_sync',
|
||||
events: Object.assign({}, {
|
||||
'click #renew_consent_button': '_onRenewConsent',
|
||||
}),
|
||||
|
||||
OdooFinConnector: function (parent, action) {
|
||||
// Ensure that the proxyMode is valid
|
||||
const modeRegexp = /^[a-z0-9-_]+$/i;
|
||||
if (!modeRegexp.test(action.params.proxyMode)) {
|
||||
return;
|
||||
}
|
||||
const url = 'https://' + action.params.proxyMode + '.odoofin.com/proxy/v1/odoofin_link';
|
||||
|
||||
loadJS(url)
|
||||
.then(() => {
|
||||
// Create and open the iframe
|
||||
const params = {
|
||||
data: action.params,
|
||||
proxyMode: action.params.proxyMode,
|
||||
onEvent: function (event, data) {
|
||||
switch (event) {
|
||||
case 'success':
|
||||
const processUrl = window.location.pathname + '/complete' + window.location.search;
|
||||
$('.js_reconnect').toggleClass('d-none');
|
||||
$.post(processUrl, {csrf_token: odoo.csrf_token});
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
OdooFin.create(params);
|
||||
OdooFin.open();
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onRenewConsent: async function (ev) {
|
||||
ev.preventDefault();
|
||||
const action = JSON.parse($(ev.currentTarget).attr('iframe-params'));
|
||||
return this.OdooFinConnector(this, action);
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
OnlineSyncPortal: publicWidget.registry.OnlineSyncPortal,
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch(["account.online.link", "account.online.account", "account.bank.selection"]);
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/* @odoo-module */
|
||||
|
||||
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { openFormView, start } from "@mail/../tests/helpers/test_utils";
|
||||
|
||||
import { click, contains } from "@web/../tests/utils";
|
||||
|
||||
QUnit.module("Views", {}, function () {
|
||||
QUnit.module("AccountOnlineSynchronizationAccountRadio");
|
||||
|
||||
QUnit.test("can be rendered", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const onlineLink = pyEnv["account.online.link"].create([
|
||||
{
|
||||
state: "connected",
|
||||
name: "Fake Bank",
|
||||
},
|
||||
]);
|
||||
pyEnv["account.online.account"].create([
|
||||
{
|
||||
name: "account_1",
|
||||
online_identifier: "abcd",
|
||||
balance: 10.0,
|
||||
account_number: "account_number_1",
|
||||
account_online_link_id: onlineLink,
|
||||
},
|
||||
{
|
||||
name: "account_2",
|
||||
online_identifier: "efgh",
|
||||
balance: 20.0,
|
||||
account_number: "account_number_2",
|
||||
account_online_link_id: onlineLink,
|
||||
},
|
||||
]);
|
||||
const bankSelection = pyEnv["account.bank.selection"].create([
|
||||
{
|
||||
account_online_link_id: onlineLink,
|
||||
},
|
||||
]);
|
||||
|
||||
const views = {
|
||||
"account.bank.selection,false,form": `<form>
|
||||
<div>
|
||||
<field name="account_online_account_ids" invisible="1"/>
|
||||
<field name="selected_account" widget="online_account_radio" nolabel="1"/>
|
||||
</div>
|
||||
</form>`,
|
||||
};
|
||||
await start({
|
||||
serverData: { views },
|
||||
mockRPC: function (route, args) {
|
||||
if (
|
||||
route === "/web/dataset/call_kw/account.online.account/get_formatted_balances"
|
||||
) {
|
||||
return {
|
||||
1: ["$ 10.0", 10.0],
|
||||
2: ["$ 20.0", 20.0],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
await openFormView("account.bank.selection", bankSelection);
|
||||
await contains(".o_radio_item", { count: 2 });
|
||||
await contains(":nth-child(1 of .o_radio_item)", {
|
||||
contains: [
|
||||
["p", { text: "$ 10.0" }],
|
||||
["label", { text: "account_1" }],
|
||||
[".o_radio_input:checked"],
|
||||
],
|
||||
});
|
||||
await contains(":nth-child(2 of .o_radio_item)", {
|
||||
contains: [
|
||||
["p", { text: "$ 20.0" }],
|
||||
["label", { text: "account_2" }],
|
||||
[".o_radio_input:not(:checked)"],
|
||||
],
|
||||
});
|
||||
await click(":nth-child(2 of .o_radio_item) .o_radio_input");
|
||||
await contains(":nth-child(1 of .o_radio_item) .o_radio_input:not(:checked)");
|
||||
await contains(":nth-child(2 of .o_radio_item) .o_radio_input:checked");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import common
|
||||
from . import test_account_online_account
|
||||
from . import test_online_sync_creation_statement
|
||||
from . import test_account_missing_transactions_wizard
|
||||
from . import test_online_sync_branch_companies
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
# Part of Odoo. 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
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class AccountOnlineSynchronizationCommon(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
|
||||
cls.euro_bank_journal = cls.env['account.journal'].create({
|
||||
'name': 'Euro Bank Journal',
|
||||
'type': 'bank',
|
||||
'code': 'EURB',
|
||||
'currency_id': cls.other_currency.id,
|
||||
})
|
||||
cls.account_online_link = cls.env['account.online.link'].create({
|
||||
'name': 'Test Bank',
|
||||
'client_id': 'client_id_1',
|
||||
'refresh_token': 'refresh_token',
|
||||
'access_token': 'access_token',
|
||||
})
|
||||
cls.account_online_account = cls.env['account.online.account'].create({
|
||||
'name': 'MyBankAccount',
|
||||
'account_online_link_id': cls.account_online_link.id,
|
||||
'journal_ids': [Command.set(cls.euro_bank_journal.id)]
|
||||
})
|
||||
cls.BankStatementLine = cls.env['account.bank.statement.line']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.transaction_id = 1
|
||||
self.account_online_account.balance = 0.0
|
||||
|
||||
def _create_one_online_transaction(self, transaction_identifier=None, date=None, payment_ref=None, amount=10.0, partner_name=None, foreign_currency_code=None, amount_currency=8.0):
|
||||
""" This method allows to create an online transaction granularly
|
||||
|
||||
:param transaction_identifier: Online identifier of the transaction, by default transaction_id from the
|
||||
setUp. If used, transaction_id is not incremented.
|
||||
:param date: Date of the transaction, by default the date of today
|
||||
:param payment_ref: Label of the transaction
|
||||
:param amount: Amount of the transaction, by default equals 10.0
|
||||
:param foreign_currency_code: Code of transaction's foreign currency
|
||||
:param amount_currency: Amount of transaction in foreign currency, update transaction only if foreign_currency_code is given, by default equals 8.0
|
||||
:return: A dictionnary representing an online transaction (not formatted)
|
||||
"""
|
||||
transaction_identifier = transaction_identifier if transaction_identifier is not None else self.transaction_id
|
||||
if date:
|
||||
date = date if isinstance(date, str) else fields.Date.to_string(date)
|
||||
else:
|
||||
date = fields.Date.to_string(fields.Date.today())
|
||||
|
||||
payment_ref = payment_ref or f'transaction_{transaction_identifier}'
|
||||
transaction = {
|
||||
'online_transaction_identifier': transaction_identifier,
|
||||
'date': date,
|
||||
'payment_ref': payment_ref,
|
||||
'amount': amount,
|
||||
'partner_name': partner_name,
|
||||
}
|
||||
if foreign_currency_code:
|
||||
transaction.update({
|
||||
'foreign_currency_code': foreign_currency_code,
|
||||
'amount_currency': amount_currency
|
||||
})
|
||||
return transaction
|
||||
|
||||
def _create_online_transactions(self, dates):
|
||||
""" This method returns a list of transactions with the
|
||||
given dates.
|
||||
All amounts equals 10.0
|
||||
|
||||
:param dates: A list of dates, one transaction is created for each given date.
|
||||
:return: A formatted list of transactions
|
||||
"""
|
||||
transactions = []
|
||||
for date in dates:
|
||||
transactions.append(self._create_one_online_transaction(date=date))
|
||||
self.transaction_id += 1
|
||||
return self.account_online_account._format_transactions(transactions)
|
||||
|
||||
def _mock_odoofin_response(self, data=None):
|
||||
if not data:
|
||||
data = {}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'result': data,
|
||||
}
|
||||
return mock_response
|
||||
|
||||
def _mock_odoofin_error_response(self, code=200, message='Default', data=None):
|
||||
if not data:
|
||||
data = {}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'error': {
|
||||
'code': code,
|
||||
'message': message,
|
||||
'data': data,
|
||||
},
|
||||
}
|
||||
return mock_response
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
from odoo import fields
|
||||
from odoo.addons.odex30_account_online_sync.tests.common import AccountOnlineSynchronizationCommon
|
||||
from odoo.tests import tagged
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMissingTransactionsWizard(AccountOnlineSynchronizationCommon):
|
||||
""" Tests the account journal missing transactions wizard. """
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_odoo_fin')
|
||||
def test_fetch_missing_transaction(self, patched_fetch_odoofin):
|
||||
self.account_online_link.state = 'connected'
|
||||
patched_fetch_odoofin.side_effect = [{
|
||||
'transactions': [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01', date='2023-07-06', foreign_currency_code='EGP', amount_currency=8.0),
|
||||
],
|
||||
'pendings': [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD02_pending', date='2023-07-25', foreign_currency_code='GBP', amount_currency=8.0),
|
||||
]
|
||||
}]
|
||||
start_date = fields.Date.from_string('2023-07-01')
|
||||
wizard = self.env['account.missing.transaction.wizard'].new({
|
||||
'date': start_date,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
})
|
||||
|
||||
action = wizard.action_fetch_missing_transaction()
|
||||
transient_transactions = self.env['account.bank.statement.line.transient'].search(domain=action['domain'])
|
||||
egp_currency = self.env['res.currency'].search([('name', '=', 'EGP')])
|
||||
gbp_currency = self.env['res.currency'].search([('name', '=', 'GBP')])
|
||||
|
||||
self.assertEqual(2, len(transient_transactions))
|
||||
# Posted Transaction
|
||||
self.assertEqual(transient_transactions[0]['online_transaction_identifier'], 'ABCD01')
|
||||
self.assertEqual(transient_transactions[0]['date'], fields.Date.from_string('2023-07-06'))
|
||||
self.assertEqual(transient_transactions[0]['state'], 'posted')
|
||||
self.assertEqual(transient_transactions[0]['foreign_currency_id'], egp_currency)
|
||||
self.assertEqual(transient_transactions[0]['amount_currency'], 8.0)
|
||||
# Pending Transaction
|
||||
self.assertEqual(transient_transactions[1]['online_transaction_identifier'], 'ABCD02_pending')
|
||||
self.assertEqual(transient_transactions[1]['date'], fields.Date.from_string('2023-07-25'))
|
||||
self.assertEqual(transient_transactions[1]['state'], 'pending')
|
||||
self.assertEqual(transient_transactions[1]['foreign_currency_id'], gbp_currency)
|
||||
self.assertEqual(transient_transactions[1]['amount_currency'], 8.0)
|
||||
|
|
@ -1,491 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command, fields, tools
|
||||
from odoo.addons.odex30_account_online_sync.tests.common import AccountOnlineSynchronizationCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountOnlineAccount(AccountOnlineSynchronizationCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.bank_account_id = cls.env['account.account'].create({
|
||||
'name': 'Bank Account',
|
||||
'account_type': 'asset_cash',
|
||||
'code': cls.env['account.account']._search_new_account_code('BNK100'),
|
||||
})
|
||||
cls.bank_journal = cls.env['account.journal'].create({
|
||||
'name': 'A bank journal',
|
||||
'default_account_id': cls.bank_account_id.id,
|
||||
'type': 'bank',
|
||||
'code': cls.env['account.journal'].get_next_bank_cash_default_code('bank', cls.company_data['company']),
|
||||
})
|
||||
|
||||
@freeze_time('2023-08-01')
|
||||
def test_get_filtered_transactions(self):
|
||||
""" This test verifies that duplicate transactions are filtered """
|
||||
self.BankStatementLine.with_context(skip_statement_line_cron_trigger=True).create({
|
||||
'date': '2023-08-01',
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'online_transaction_identifier': 'ABCD01',
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'amount': 10.0,
|
||||
})
|
||||
|
||||
transactions_to_filtered = [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01'),
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD02'),
|
||||
]
|
||||
|
||||
filtered_transactions = self.account_online_account._get_filtered_transactions(transactions_to_filtered)
|
||||
|
||||
self.assertEqual(
|
||||
filtered_transactions,
|
||||
[
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD02',
|
||||
'date': '2023-08-01',
|
||||
'online_transaction_identifier': 'ABCD02',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@freeze_time('2023-08-01')
|
||||
def test_get_filtered_transactions_with_empty_transaction_identifier(self):
|
||||
""" This test verifies that transactions without a transaction identifier
|
||||
are not filtered due to their empty transaction identifier.
|
||||
"""
|
||||
self.BankStatementLine.with_context(skip_statement_line_cron_trigger=True).create({
|
||||
'date': '2023-08-01',
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'online_transaction_identifier': '',
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'amount': 10.0,
|
||||
})
|
||||
|
||||
transactions_to_filtered = [
|
||||
self._create_one_online_transaction(transaction_identifier=''),
|
||||
self._create_one_online_transaction(transaction_identifier=''),
|
||||
]
|
||||
|
||||
filtered_transactions = self.account_online_account._get_filtered_transactions(transactions_to_filtered)
|
||||
|
||||
self.assertEqual(
|
||||
filtered_transactions,
|
||||
[
|
||||
{
|
||||
'payment_ref': 'transaction_',
|
||||
'date': '2023-08-01',
|
||||
'online_transaction_identifier': '',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
},
|
||||
{
|
||||
'payment_ref': 'transaction_',
|
||||
'date': '2023-08-01',
|
||||
'online_transaction_identifier': '',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@freeze_time('2023-08-01')
|
||||
def test_format_transactions(self):
|
||||
transactions_to_format = [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01'),
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD02'),
|
||||
]
|
||||
formatted_transactions = self.account_online_account._format_transactions(transactions_to_format)
|
||||
self.assertEqual(
|
||||
formatted_transactions,
|
||||
[
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'date': fields.Date.from_string('2023-08-01'),
|
||||
'online_transaction_identifier': 'ABCD01',
|
||||
'amount': 10.0,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
'partner_name': None,
|
||||
},
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD02',
|
||||
'date': fields.Date.from_string('2023-08-01'),
|
||||
'online_transaction_identifier': 'ABCD02',
|
||||
'amount': 10.0,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
'partner_name': None,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@freeze_time('2023-08-01')
|
||||
def test_format_transactions_invert_sign(self):
|
||||
transactions_to_format = [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01', amount=25.0),
|
||||
]
|
||||
self.account_online_account.inverse_transaction_sign = True
|
||||
formatted_transactions = self.account_online_account._format_transactions(transactions_to_format)
|
||||
self.assertEqual(
|
||||
formatted_transactions,
|
||||
[
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'date': fields.Date.from_string('2023-08-01'),
|
||||
'online_transaction_identifier': 'ABCD01',
|
||||
'amount': -25.0,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
'partner_name': None,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@freeze_time('2023-08-01')
|
||||
def test_format_transactions_foreign_currency_code_to_id_with_activation(self):
|
||||
""" This test ensures conversion of foreign currency code to foreign currency id and activates foreign currency if not already activate """
|
||||
gbp_currency = self.env['res.currency'].with_context(active_test=False).search([('name', '=', 'GBP')])
|
||||
egp_currency = self.env['res.currency'].with_context(active_test=False).search([('name', '=', 'EGP')])
|
||||
|
||||
transactions_to_format = [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01', foreign_currency_code='GBP'),
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD02', foreign_currency_code='EGP', amount_currency=500.0),
|
||||
]
|
||||
formatted_transactions = self.account_online_account._format_transactions(transactions_to_format)
|
||||
|
||||
self.assertTrue(gbp_currency.active)
|
||||
self.assertTrue(egp_currency.active)
|
||||
|
||||
self.assertEqual(
|
||||
formatted_transactions,
|
||||
[
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'date': fields.Date.from_string('2023-08-01'),
|
||||
'online_transaction_identifier': 'ABCD01',
|
||||
'amount': 10.0,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
'partner_name': None,
|
||||
'foreign_currency_id': gbp_currency.id,
|
||||
'amount_currency': 8.0,
|
||||
},
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD02',
|
||||
'date': fields.Date.from_string('2023-08-01'),
|
||||
'online_transaction_identifier': 'ABCD02',
|
||||
'amount': 10.0,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
'partner_name': None,
|
||||
'foreign_currency_id': egp_currency.id,
|
||||
'amount_currency': 500.0,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@freeze_time('2023-07-25')
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_odoo_fin')
|
||||
def test_retrieve_pending_transactions(self, patched_fetch_odoofin):
|
||||
self.account_online_link.state = 'connected'
|
||||
patched_fetch_odoofin.side_effect = [{
|
||||
'transactions': [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD01', date='2023-07-06'),
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD02', date='2023-07-22'),
|
||||
],
|
||||
'pendings': [
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD03_pending', date='2023-07-25'),
|
||||
self._create_one_online_transaction(transaction_identifier='ABCD04_pending', date='2023-07-25'),
|
||||
]
|
||||
}]
|
||||
|
||||
start_date = fields.Date.from_string('2023-07-01')
|
||||
result = self.account_online_account._retrieve_transactions(date=start_date, include_pendings=True)
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
'transactions': [
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD01',
|
||||
'date': fields.Date.from_string('2023-07-06'),
|
||||
'online_transaction_identifier': 'ABCD01',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
},
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD02',
|
||||
'date': fields.Date.from_string('2023-07-22'),
|
||||
'online_transaction_identifier': 'ABCD02',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
}
|
||||
],
|
||||
'pendings': [
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD03_pending',
|
||||
'date': fields.Date.from_string('2023-07-25'),
|
||||
'online_transaction_identifier': 'ABCD03_pending',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
},
|
||||
{
|
||||
'payment_ref': 'transaction_ABCD04_pending',
|
||||
'date': fields.Date.from_string('2023-07-25'),
|
||||
'online_transaction_identifier': 'ABCD04_pending',
|
||||
'amount': 10.0,
|
||||
'partner_name': None,
|
||||
'online_account_id': self.account_online_account.id,
|
||||
'journal_id': self.euro_bank_journal.id,
|
||||
'company_id': self.euro_bank_journal.company_id.id,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@freeze_time('2023-01-01 01:10:15')
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineAccount._retrieve_transactions', return_value={})
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineAccount._refresh', return_value={'success': True, 'data': {}})
|
||||
def test_basic_flow_manual_fetching_transactions(self, patched_refresh, patched_transactions):
|
||||
self.addCleanup(self.env.registry.leave_test_mode)
|
||||
# flush and clear everything for the new "transaction"
|
||||
self.env.invalidate_all()
|
||||
|
||||
self.env.registry.enter_test_mode(self.cr)
|
||||
with self.env.registry.cursor() as test_cr:
|
||||
test_env = self.env(cr=test_cr)
|
||||
test_link_account = self.account_online_link.with_env(test_env)
|
||||
test_link_account.state = 'connected'
|
||||
# Call fetch_transaction in manual mode and check that a call was made to refresh and to transaction
|
||||
test_link_account._fetch_transactions()
|
||||
patched_refresh.assert_called_once()
|
||||
patched_transactions.assert_called_once()
|
||||
self.assertEqual(test_link_account.account_online_account_ids[0].fetching_status, 'done')
|
||||
|
||||
@freeze_time('2023-01-01 01:10:15')
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineAccount._retrieve_transactions', return_value={})
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_odoo_fin')
|
||||
def test_refresh_incomplete_fetching_transactions(self, patched_refresh, patched_transactions):
|
||||
patched_refresh.return_value = {'success': False}
|
||||
# Call fetch_transaction and if call result is false, don't call transaction
|
||||
self.account_online_link._fetch_transactions()
|
||||
patched_transactions.assert_not_called()
|
||||
|
||||
patched_refresh.return_value = {'success': False, 'currently_fetching': True}
|
||||
# Call fetch_transaction and if call result is false but in the process of fetching, don't call transaction
|
||||
# and wait for the async cron to try again
|
||||
self.account_online_link._fetch_transactions()
|
||||
patched_transactions.assert_not_called()
|
||||
self.assertEqual(self.account_online_account.fetching_status, 'waiting')
|
||||
|
||||
@freeze_time('2023-01-01 01:10:15')
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineAccount._retrieve_transactions', return_value={})
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineAccount._refresh', return_value={'success': True, 'data': {}})
|
||||
def test_currently_processing_fetching_transactions(self, patched_refresh, patched_transactions):
|
||||
self.account_online_account.fetching_status = 'processing' # simulate the fact that we are currently creating entries in odoo
|
||||
limit_time = tools.config['limit_time_real_cron'] if tools.config['limit_time_real_cron'] > 0 else tools.config['limit_time_real']
|
||||
self.account_online_link.last_refresh = datetime.now()
|
||||
with freeze_time(datetime.now() + timedelta(seconds=(limit_time - 10))):
|
||||
# Call to fetch_transaction should be skipped, and the cron should not try to fetch either
|
||||
self.account_online_link._fetch_transactions()
|
||||
self.euro_bank_journal._cron_fetch_waiting_online_transactions()
|
||||
patched_refresh.assert_not_called()
|
||||
patched_transactions.assert_not_called()
|
||||
|
||||
self.addCleanup(self.env.registry.leave_test_mode)
|
||||
# flush and clear everything for the new "transaction"
|
||||
self.env.invalidate_all()
|
||||
|
||||
self.env.registry.enter_test_mode(self.cr)
|
||||
with self.env.registry.cursor() as test_cr:
|
||||
test_env = self.env(cr=test_cr)
|
||||
with freeze_time(datetime.now() + timedelta(seconds=(limit_time + 100))):
|
||||
# Call to fetch_transaction should be started by the cron when the time limit is exceeded and still in processing
|
||||
self.euro_bank_journal.with_env(test_env)._cron_fetch_waiting_online_transactions()
|
||||
patched_refresh.assert_not_called()
|
||||
patched_transactions.assert_called_once()
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.requests')
|
||||
def test_delete_with_redirect_error(self, patched_request):
|
||||
# Use case being tested: call delete on a record, first call returns token expired exception
|
||||
# Which trigger a call to get a new token, which result in a 104 user_deleted_error, since version 17,
|
||||
# such error are returned as a OdooFinRedirectException with mode link to reopen the iframe and link with a new
|
||||
# bank. In our case we don't want that and want to be able to delete the record instead.
|
||||
# Such use case happen when db_uuid has changed as the check for db_uuid is done after the check for token_validity
|
||||
account_online_link = self.env['account.online.link'].create({
|
||||
'name': 'Test Delete',
|
||||
'client_id': 'client_id_test',
|
||||
'refresh_token': 'refresh_token',
|
||||
'access_token': 'access_token',
|
||||
})
|
||||
first_call = self._mock_odoofin_error_response(code=102)
|
||||
second_call = self._mock_odoofin_error_response(code=300, data={'mode': 'link'})
|
||||
patched_request.post.side_effect = [first_call, second_call]
|
||||
nb_connections = len(self.env['account.online.link'].search([]))
|
||||
# Try to delete record
|
||||
account_online_link.unlink()
|
||||
# Record should be deleted
|
||||
self.assertEqual(len(self.env['account.online.link'].search([])), nb_connections - 1)
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.requests')
|
||||
def test_redirect_mode_link(self, patched_request):
|
||||
# Use case being tested: Call to open the iframe which result in a OdoofinRedirectException in link mode
|
||||
# This should not trigger a traceback but delete the current online.link and reopen the iframe
|
||||
account_online_link = self.env['account.online.link'].create({
|
||||
'name': 'Test Delete',
|
||||
'client_id': 'client_id_test',
|
||||
'refresh_token': 'refresh_token',
|
||||
'access_token': 'access_token',
|
||||
})
|
||||
link_id = account_online_link.id
|
||||
first_call = self._mock_odoofin_error_response(code=300, data={'mode': 'link'})
|
||||
second_call = self._mock_odoofin_response(data={'delete': True})
|
||||
patched_request.post.side_effect = [first_call, second_call]
|
||||
# Try to open iframe with broken connection
|
||||
action = account_online_link.action_new_synchronization()
|
||||
# Iframe should open in mode link and with a different record (old one should have been deleted)
|
||||
self.assertEqual(action['params']['mode'], 'link')
|
||||
self.assertNotEqual(action['id'], link_id)
|
||||
self.assertEqual(len(self.env['account.online.link'].search([('id', '=', link_id)])), 0)
|
||||
|
||||
@patch("odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._update_connection_status", return_value={})
|
||||
def test_assign_journal_with_currency_on_account_online_account(self, patched_update_connection_status):
|
||||
self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': fields.Date.from_string('2025-06-25'),
|
||||
'journal_id': self.bank_journal.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'a line',
|
||||
'account_id': self.bank_account_id.id,
|
||||
'debit': 100,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'another line',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'credit': 100,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': fields.Date.from_string('2025-06-26'),
|
||||
'journal_id': self.bank_journal.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'a line',
|
||||
'account_id': self.bank_account_id.id,
|
||||
'debit': 220,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'another line',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'credit': 220,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
self.account_online_account.currency_id = self.company_data['currency'].id
|
||||
self.account_online_account.with_context(active_id=self.bank_journal.id, active_model='account.journal')._assign_journal()
|
||||
self.assertEqual(
|
||||
self.bank_journal.currency_id.id,
|
||||
self.company_data['currency'].id,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.bank_journal.default_account_id.currency_id.id,
|
||||
self.company_data['currency'].id,
|
||||
)
|
||||
|
||||
@patch("odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._update_connection_status", return_value={})
|
||||
def test_set_currency_on_journal_when_existing_currencies_on_move_lines(self, patched_update_connection_status):
|
||||
bank_account_id = self.env['account.account'].create({
|
||||
'name': 'Bank Account',
|
||||
'account_type': 'asset_cash',
|
||||
'code': self.env['account.account']._search_new_account_code('BNK100'),
|
||||
})
|
||||
bank_journal = self.env['account.journal'].create({
|
||||
'name': 'A bank journal',
|
||||
'default_account_id': bank_account_id.id,
|
||||
'type': 'bank',
|
||||
'code': self.env['account.journal'].get_next_bank_cash_default_code('bank', self.company_data['company']),
|
||||
})
|
||||
|
||||
self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': fields.Date.from_string('2025-06-25'),
|
||||
'journal_id': bank_journal.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'a line',
|
||||
'account_id': bank_account_id.id,
|
||||
'debit': 100,
|
||||
'currency_id': self.other_currency.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'another line',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'credit': 100,
|
||||
'currency_id': self.other_currency.id,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': fields.Date.from_string('2025-06-26'),
|
||||
'journal_id': bank_journal.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'a line',
|
||||
'account_id': bank_account_id.id,
|
||||
'debit': 220,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'another line',
|
||||
'account_id': self.company_data['default_account_expense'].id,
|
||||
'credit': 220,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}),
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
self.account_online_account.currency_id = self.company_data['currency'].id
|
||||
self.account_online_account.with_context(active_id=bank_journal.id, active_model='account.journal')._assign_journal()
|
||||
|
||||
# Silently ignore the error and don't set currency on the journal and on the account
|
||||
self.assertEqual(bank_journal.currency_id.id, False)
|
||||
self.assertEqual(bank_journal.default_account_id.currency_id.id, False)
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.odex30_account_online_sync.tests.common import AccountOnlineSynchronizationCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSynchInBranches(AccountOnlineSynchronizationCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.mother_company = cls.env['res.company'].create({'name': 'Mother company 2000'})
|
||||
cls.branch_company = cls.env['res.company'].create({'name': 'Branch company', 'parent_id': cls.mother_company.id})
|
||||
|
||||
cls.mother_bank_journal = cls.env['account.journal'].create({
|
||||
'name': 'Mother Bank Journal',
|
||||
'type': 'bank',
|
||||
'code': 'MBJ',
|
||||
'company_id': cls.mother_company.id,
|
||||
})
|
||||
cls.mother_account_online_link = cls.env['account.online.link'].create({
|
||||
'name': 'Test Bank',
|
||||
'client_id': 'client_id_1',
|
||||
'refresh_token': 'refresh_token',
|
||||
'access_token': 'access_token',
|
||||
'company_id': cls.mother_company.id,
|
||||
})
|
||||
|
||||
def test_show_sync_actions(self):
|
||||
"""We test if the sync actions are correctly displayed based on the selected and enabled companies.
|
||||
|
||||
Let's have company A with an online link, and a branch of that company: company B.
|
||||
|
||||
- If we only have company A enabled and selected, the sync actions should be shown.
|
||||
- If company A and B are enabled, no matter which company is selected, the sync actions should be shown.
|
||||
- If we only have company B enabled and selected, the sync actions should be hidden.
|
||||
"""
|
||||
self.assertTrue(
|
||||
self.mother_account_online_link
|
||||
.with_context(allowed_company_ids=(self.mother_company)._ids)
|
||||
.with_company(self.mother_company)
|
||||
.show_sync_actions
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
self.mother_account_online_link
|
||||
.with_context(allowed_company_ids=(self.branch_company + self.mother_company)._ids)
|
||||
.with_company(self.mother_company)
|
||||
.show_sync_actions
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
self.mother_account_online_link
|
||||
.with_context(allowed_company_ids=(self.branch_company + self.mother_company)._ids)
|
||||
.with_company(self.branch_company)
|
||||
.show_sync_actions
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.mother_account_online_link
|
||||
.with_context(allowed_company_ids=(self.branch_company)._ids)
|
||||
.with_company(self.branch_company)
|
||||
.show_sync_actions
|
||||
)
|
||||
|
||||
def test_show_bank_connect(self):
|
||||
"""We test if the 'connect' bank button appears on the journal on the dashboard given the selected company.
|
||||
|
||||
Let's have company A with an bank journal, and a branch of that company: company B.
|
||||
|
||||
- On the dashboard of company A, the connect bank button should appear on the journal.
|
||||
- On the dashboard of company B, the connect bank button should not appear on the journal, even with company A enabled.
|
||||
"""
|
||||
dashboard_data = self.mother_bank_journal\
|
||||
.with_context(allowed_company_ids=(self.mother_company)._ids)\
|
||||
.with_company(self.mother_company)\
|
||||
._get_journal_dashboard_data_batched()
|
||||
self.assertTrue(dashboard_data[self.mother_bank_journal.id].get('display_connect_bank_in_dashboard'))
|
||||
|
||||
dashboard_data = self.mother_bank_journal\
|
||||
.with_context(allowed_company_ids=(self.branch_company + self.mother_company)._ids)\
|
||||
.with_company(self.branch_company)\
|
||||
._get_journal_dashboard_data_batched()
|
||||
self.assertFalse(dashboard_data[self.mother_bank_journal.id].get('display_connect_bank_in_dashboard'))
|
||||
|
|
@ -1,374 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from odoo.addons.base.models.res_bank import sanitize_account_number
|
||||
from odoo.addons.odex30_account_online_sync.tests.common import AccountOnlineSynchronizationCommon
|
||||
from odoo.exceptions import RedirectWarning
|
||||
from odoo.tests import tagged
|
||||
from odoo import fields, Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSynchStatementCreation(AccountOnlineSynchronizationCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.account = cls.env['account.account'].create({
|
||||
'name': 'Fixed Asset Account',
|
||||
'code': 'AA',
|
||||
'account_type': 'asset_fixed',
|
||||
})
|
||||
|
||||
def reconcile_st_lines(self, st_lines):
|
||||
for line in st_lines:
|
||||
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=line.id).new({})
|
||||
line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance')
|
||||
wizard._js_action_mount_line_in_edit(line.index)
|
||||
line.name = "toto"
|
||||
wizard._line_value_changed_name(line)
|
||||
line.account_id = self.account
|
||||
wizard._line_value_changed_account_id(line)
|
||||
wizard._action_validate()
|
||||
|
||||
# Tests
|
||||
def test_creation_initial_sync_statement(self):
|
||||
transactions = self._create_online_transactions(['2016-01-01', '2016-01-03'])
|
||||
self.account_online_account.balance = 1000
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
# Since ending balance is 1000$ and we only have 20$ of transactions and that it is the first statement
|
||||
# it should create a statement before this one with the initial statement line
|
||||
created_st_lines = self.BankStatementLine.search([('journal_id', '=', self.euro_bank_journal.id)], order='internal_index asc')
|
||||
self.assertEqual(len(created_st_lines), 3, 'Should have created an initial bank statement line and two for the synchronization')
|
||||
transactions = self._create_online_transactions(['2016-01-05'])
|
||||
self.account_online_account.balance = 2000
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
created_st_lines = self.BankStatementLine.search([('journal_id', '=', self.euro_bank_journal.id)], order='internal_index asc')
|
||||
self.assertRecordValues(
|
||||
created_st_lines,
|
||||
[
|
||||
{'date': fields.Date.from_string('2016-01-01'), 'amount': 980.0},
|
||||
{'date': fields.Date.from_string('2016-01-01'), 'amount': 10.0},
|
||||
{'date': fields.Date.from_string('2016-01-03'), 'amount': 10.0},
|
||||
{'date': fields.Date.from_string('2016-01-05'), 'amount': 10.0},
|
||||
]
|
||||
)
|
||||
|
||||
def test_creation_initial_sync_statement_bis(self):
|
||||
transactions = self._create_online_transactions(['2016-01-01', '2016-01-03'])
|
||||
self.account_online_account.balance = 20
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
# Since ending balance is 20$ and we only have 20$ of transactions and that it is the first statement
|
||||
# it should NOT create a initial statement before this one
|
||||
created_st_lines = self.BankStatementLine.search([('journal_id', '=', self.euro_bank_journal.id)], order='internal_index asc')
|
||||
self.assertRecordValues(
|
||||
created_st_lines,
|
||||
[
|
||||
{'date': fields.Date.from_string('2016-01-01'), 'amount': 10.0},
|
||||
{'date': fields.Date.from_string('2016-01-03'), 'amount': 10.0},
|
||||
]
|
||||
)
|
||||
|
||||
def test_creation_initial_sync_statement_invert_sign(self):
|
||||
self.account_online_account.balance = -20
|
||||
self.account_online_account.inverse_transaction_sign = True
|
||||
self.account_online_account.inverse_balance_sign = True
|
||||
transactions = self._create_online_transactions(['2016-01-01', '2016-01-03'])
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
# Since ending balance is 1000$ and we only have 20$ of transactions and that it is the first statement
|
||||
# it should create a statement before this one with the initial statement line
|
||||
created_st_lines = self.BankStatementLine.search([('journal_id', '=', self.euro_bank_journal.id)], order='internal_index asc')
|
||||
self.assertEqual(len(created_st_lines), 2, 'Should have created two bank statement lines for the synchronization')
|
||||
transactions = self._create_online_transactions(['2016-01-05'])
|
||||
self.account_online_account.balance = -30
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
created_st_lines = self.BankStatementLine.search([('journal_id', '=', self.euro_bank_journal.id)], order='internal_index asc')
|
||||
self.assertRecordValues(
|
||||
created_st_lines,
|
||||
[
|
||||
{'date': fields.Date.from_string('2016-01-01'), 'amount': -10.0},
|
||||
{'date': fields.Date.from_string('2016-01-03'), 'amount': -10.0},
|
||||
{'date': fields.Date.from_string('2016-01-05'), 'amount': -10.0},
|
||||
]
|
||||
)
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_transactions')
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._update_connection_status')
|
||||
def test_automatic_journal_assignment(self, patched_update_connection_status, patched_fetch_transactions):
|
||||
def create_online_account(name, link_id, iban, currency_id):
|
||||
return self.env['account.online.account'].create({
|
||||
'name': name,
|
||||
'account_online_link_id': link_id,
|
||||
'account_number': iban,
|
||||
'currency_id' : currency_id,
|
||||
})
|
||||
|
||||
def create_bank_account(account_number, partner_id):
|
||||
return self.env['res.partner.bank'].create({
|
||||
'acc_number': account_number,
|
||||
'partner_id': partner_id,
|
||||
})
|
||||
|
||||
def create_journal(name, journal_type, code, currency_id=False, bank_account_id=False):
|
||||
return self.env['account.journal'].create({
|
||||
'name': name,
|
||||
'type': journal_type,
|
||||
'code': code,
|
||||
'currency_id': currency_id,
|
||||
'bank_account_id': bank_account_id,
|
||||
})
|
||||
|
||||
bank_account_1 = create_bank_account('BE48485444456727', self.company_data['company'].partner_id.id)
|
||||
bank_account_2 = create_bank_account('BE23798242487491', self.company_data['company'].partner_id.id)
|
||||
|
||||
bank_journal_with_account_gol = create_journal('Bank with account', 'bank', 'BJWA1', self.other_currency.id)
|
||||
bank_journal_with_account_usd = create_journal('Bank with account USD', 'bank', 'BJWA3', self.env.ref('base.USD').id, bank_account_2.id)
|
||||
|
||||
online_account_1 = create_online_account('OnlineAccount1', self.account_online_link.id, 'BE48485444456727', self.other_currency.id)
|
||||
online_account_2 = create_online_account('OnlineAccount2', self.account_online_link.id, 'BE61954856342317', self.other_currency.id)
|
||||
online_account_3 = create_online_account('OnlineAccount3', self.account_online_link.id, 'BE23798242487495', self.other_currency.id)
|
||||
|
||||
patched_fetch_transactions.return_value = True
|
||||
patched_update_connection_status.return_value = {
|
||||
'consent_expiring_date': None,
|
||||
'is_payment_enabled': False,
|
||||
'is_payment_activated': False,
|
||||
}
|
||||
|
||||
account_link_journal_wizard = self.env['account.bank.selection'].create({'account_online_link_id': self.account_online_link.id})
|
||||
account_link_journal_wizard.with_context(active_model='account.journal', active_id=bank_journal_with_account_gol.id).sync_now()
|
||||
self.assertEqual(
|
||||
online_account_1.id, bank_journal_with_account_gol.account_online_account_id.id,
|
||||
"The wizard should have linked the online account to the journal with the same account."
|
||||
)
|
||||
self.assertEqual(bank_journal_with_account_gol.bank_account_id, bank_account_1, "Account should be set on the journal")
|
||||
|
||||
# Test with no context present, should create a new journal
|
||||
previous_number = self.env['account.journal'].search_count([])
|
||||
account_link_journal_wizard.selected_account = online_account_2
|
||||
account_link_journal_wizard.sync_now()
|
||||
actual_number = self.env['account.journal'].search_count([])
|
||||
self.assertEqual(actual_number, previous_number+1, "should have created a new journal")
|
||||
self.assertEqual(online_account_2.journal_ids.currency_id, self.other_currency)
|
||||
self.assertEqual(online_account_2.journal_ids.bank_account_id.sanitized_acc_number, sanitize_account_number('BE61954856342317'))
|
||||
|
||||
# Test assigning to a journal in another currency
|
||||
account_link_journal_wizard.selected_account = online_account_3
|
||||
account_link_journal_wizard.with_context(active_model='account.journal', active_id=bank_journal_with_account_usd.id).sync_now()
|
||||
self.assertEqual(online_account_3.id, bank_journal_with_account_usd.account_online_account_id.id)
|
||||
self.assertEqual(bank_journal_with_account_usd.bank_account_id, bank_account_2, "Bank Account should not have changed")
|
||||
self.assertEqual(bank_journal_with_account_usd.currency_id, self.other_currency, "Currency should have changed")
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_odoo_fin')
|
||||
def test_fetch_transaction_date_start(self, patched_fetch):
|
||||
""" This test verifies that the start_date params used when fetching transaction is correct """
|
||||
patched_fetch.return_value = {'transactions': []}
|
||||
# Since no transactions exists in db, we should fetch transactions without a starting_date
|
||||
self.account_online_account._retrieve_transactions()
|
||||
data = {
|
||||
'start_date': False,
|
||||
'account_id': False,
|
||||
'last_transaction_identifier': False,
|
||||
'currency_code': 'EUR',
|
||||
'provider_data': False,
|
||||
'account_data': False,
|
||||
'include_pendings': False,
|
||||
'include_foreign_currency': True,
|
||||
}
|
||||
patched_fetch.assert_called_with('/proxy/v1/transactions', data=data)
|
||||
|
||||
# No transaction exists in db but we have a value for last_sync on the online_account, we should use that date
|
||||
self.account_online_account.last_sync = '2020-03-04'
|
||||
data['start_date'] = '2020-03-04'
|
||||
self.account_online_account._retrieve_transactions()
|
||||
patched_fetch.assert_called_with('/proxy/v1/transactions', data=data)
|
||||
|
||||
# We have transactions, we should use the date of the latest one instead of the last_sync date
|
||||
transactions = self._create_online_transactions(['2016-01-01', '2016-01-03'])
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
self.account_online_account.last_sync = '2020-03-04'
|
||||
data['start_date'] = '2016-01-03'
|
||||
data['last_transaction_identifier'] = '2'
|
||||
self.account_online_account._retrieve_transactions()
|
||||
patched_fetch.assert_called_with('/proxy/v1/transactions', data=data)
|
||||
|
||||
def test_multiple_transaction_identifier_fetched(self):
|
||||
# Ensure that if we receive twice the same transaction within the same call, it won't be created twice
|
||||
transactions = self._create_online_transactions(['2016-01-01', '2016-01-03'])
|
||||
# Add first transactions to the list again
|
||||
transactions.append(transactions[0])
|
||||
self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
bnk_stmt_lines = self.BankStatementLine.search([('online_transaction_identifier', '!=', False), ('journal_id', '=', self.euro_bank_journal.id)])
|
||||
self.assertEqual(len(bnk_stmt_lines), 2, 'Should only have created two lines')
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.AccountOnlineLink._fetch_odoo_fin')
|
||||
def test_fetch_transactions_reauth(self, patched_refresh):
|
||||
patched_refresh.side_effect = [
|
||||
{
|
||||
'success': False,
|
||||
'code': 300,
|
||||
'data': {'mode': 'updateCredentials'},
|
||||
},
|
||||
{
|
||||
'access_token': 'open_sesame',
|
||||
},
|
||||
]
|
||||
self.account_online_account.account_online_link_id.state = 'connected'
|
||||
res = self.account_online_account.account_online_link_id._fetch_transactions()
|
||||
self.assertTrue('account_online_identifier' in res.get('params', {}).get('includeParam', {}))
|
||||
|
||||
def test_duplicate_transaction_date_amount_account(self):
|
||||
""" This test verifies that the duplicate transaction wizard is detects transactions with
|
||||
same date, amount, account_number and currency
|
||||
"""
|
||||
# Create 2 groups of respectively 2 and 3 duplicate transactions. We create one transaction the day before so the opening statement does not interfere with the test.
|
||||
transactions = self._create_online_transactions([
|
||||
'2024-01-01',
|
||||
'2024-01-02', '2024-01-02',
|
||||
'2024-01-03', '2024-01-03', '2024-01-03',
|
||||
])
|
||||
bsls = self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
self.env.flush_all() # _get_duplicate_transactions make sql request, must write to db
|
||||
duplicate_transactions = self.euro_bank_journal._get_duplicate_transactions(
|
||||
fields.Date.to_date('2000-01-01')
|
||||
)
|
||||
group_1 = bsls.filtered(lambda bsl: bsl.date == fields.Date.from_string('2024-01-02')).ids
|
||||
group_2 = bsls.filtered(lambda bsl: bsl.date == fields.Date.from_string('2024-01-03')).ids
|
||||
|
||||
self.assertEqual(duplicate_transactions, [group_1, group_2])
|
||||
|
||||
# check has_duplicate_transactions
|
||||
has_duplicate_transactions = self.euro_bank_journal._has_duplicate_transactions(
|
||||
fields.Date.to_date('2000-01-01')
|
||||
)
|
||||
self.assertTrue(has_duplicate_transactions is True) # explicit check on bool type
|
||||
|
||||
def test_duplicate_transaction_online_transaction_identifier(self):
|
||||
""" This test verifies that the duplicate transaction wizard is detects transactions with
|
||||
same online_transaction_identifier
|
||||
"""
|
||||
# Create transactions
|
||||
transactions = self._create_online_transactions([
|
||||
'2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'
|
||||
])
|
||||
bsls = self.BankStatementLine._online_sync_bank_statement(transactions, self.account_online_account)
|
||||
|
||||
group_1, group_2 = [], []
|
||||
for bsl in bsls:
|
||||
# have to update the online_transaction_identifier after to force duplicates
|
||||
if bsl.payment_ref in ('transaction_1', 'transaction_2'):
|
||||
group_1.append(bsl.id)
|
||||
bsl.online_transaction_identifier = 'same_oti_1'
|
||||
if bsl.payment_ref in ('transaction_3, transaction_4, transaction_5'):
|
||||
group_2.append(bsl.id)
|
||||
bsl.online_transaction_identifier = 'same_oti_2'
|
||||
|
||||
self.env.flush_all() # _get_duplicate_transactions make sql request, must write to db
|
||||
duplicate_transactions = self.euro_bank_journal._get_duplicate_transactions(
|
||||
fields.Date.to_date('2000-01-01')
|
||||
)
|
||||
self.assertEqual(duplicate_transactions, [group_1, group_2])
|
||||
|
||||
@patch('odoo.addons.odex30_account_online_sync.models.account_online.requests')
|
||||
def test_fetch_receive_error_message(self, patched_request):
|
||||
# We want to test that when we receive an error, a redirectWarning with the correct parameter is thrown
|
||||
# However the method _log_information that we need to test for that is performing a rollback as it needs
|
||||
# to save the message error on the record as well (so it rollback, save message, commit, raise error).
|
||||
# So in order to test the method, we need to use a "test cursor".
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'error': {
|
||||
'code': 400,
|
||||
'message': 'Shit Happened',
|
||||
'data': {
|
||||
'exception_type': 'random',
|
||||
'message': 'This kind of things can happen.',
|
||||
'error_reference': 'abc123',
|
||||
'provider_type': 'theonlyone',
|
||||
'redirect_warning_url': 'odoo_support',
|
||||
},
|
||||
},
|
||||
}
|
||||
patched_request.post.return_value = mock_response
|
||||
|
||||
generated_url = 'https://www.odoo.com/help?stage=bank_sync&summary=Bank+sync+error+ref%3A+abc123+-+Provider%3A+theonlyone+-+Client+ID%3A+client_id_1&description=ClientID%3A+client_id_1%0AInstitution%3A+Test+Bank%0AError+Reference%3A+abc123%0AError+Message%3A+This+kind+of+things+can+happen.%0A'
|
||||
return_act_url = {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': generated_url
|
||||
}
|
||||
body_generated_url = generated_url.replace('&', '&') #in post_message, & has been escaped to &
|
||||
message_body = f"""<p>This kind of things can happen.
|
||||
|
||||
If you've already opened a ticket for this issue, don't report it again: a support agent will contact you shortly.<br>You can contact Odoo support <a href=\"{body_generated_url}\">Here</a></p>"""
|
||||
|
||||
# flush and clear everything for the new "transaction"
|
||||
self.env.invalidate_all()
|
||||
try:
|
||||
self.env.registry.enter_test_mode(self.cr)
|
||||
with self.env.registry.cursor() as test_cr:
|
||||
test_env = self.env(cr=test_cr)
|
||||
test_link_account = self.account_online_link.with_env(test_env)
|
||||
test_link_account.state = 'connected'
|
||||
|
||||
# this hand-written self.assertRaises() does not roll back self.cr,
|
||||
# which is necessary below to inspect the message being posted
|
||||
try:
|
||||
test_link_account._fetch_odoo_fin('/testthisurl')
|
||||
except RedirectWarning as exception:
|
||||
self.assertEqual(exception.args[0], "This kind of things can happen.\n\nIf you've already opened a ticket for this issue, don't report it again: a support agent will contact you shortly.")
|
||||
self.assertEqual(exception.args[1], return_act_url)
|
||||
self.assertEqual(exception.args[2], 'Report issue')
|
||||
else:
|
||||
self.fail("Expected RedirectWarning not raised")
|
||||
self.assertEqual(test_link_account.message_ids[0].body, message_body)
|
||||
finally:
|
||||
self.env.registry.leave_test_mode()
|
||||
|
||||
def test_account_online_link_having_journal_ids(self):
|
||||
""" This test verifies that the account online link object
|
||||
has all the journal in the field journal_ids.
|
||||
It's important to handle these journals because we need
|
||||
them to add the consent expiring date.
|
||||
"""
|
||||
# Create a bank sync connection having 2 online accounts (with one journal connected for each account)
|
||||
online_link = self.env['account.online.link'].create({
|
||||
'name': 'My New Bank connection',
|
||||
})
|
||||
online_accounts = self.env['account.online.account'].create([
|
||||
{
|
||||
'name': 'Account 1',
|
||||
'account_online_link_id': online_link.id,
|
||||
'journal_ids': [Command.create({
|
||||
'name': 'Account 1',
|
||||
'code': 'BK1',
|
||||
'type': 'bank',
|
||||
})],
|
||||
},
|
||||
{
|
||||
'name': 'Account 2',
|
||||
'account_online_link_id': online_link.id,
|
||||
'journal_ids': [Command.create({
|
||||
'name': 'Account 2',
|
||||
'code': 'BK2',
|
||||
'type': 'bank',
|
||||
})],
|
||||
},
|
||||
])
|
||||
self.assertEqual(online_link.account_online_account_ids, online_accounts)
|
||||
self.assertEqual(len(online_link.journal_ids), 2) # Our online link connections should have 2 journals.
|
||||
|
||||
def test_transaction_details_json_compatibility_from_html(self):
|
||||
""" This test checks that, after being imported from the transient model
|
||||
the records of account.bank.statement.line will have the
|
||||
'transaction_details' field able to be decoded to a JSON,
|
||||
i.e. it is not encapsulated in <p> </p> tags.
|
||||
"""
|
||||
transaction = self._create_one_online_transaction()
|
||||
transaction['transaction_details'] = '{\n "account_id": "1",\n "status": "posted"\n}'
|
||||
transient_transaction = self.env['account.bank.statement.line.transient'].create(transaction)
|
||||
transaction_details = transient_transaction.read(fields=['transaction_details'], load=None)[0]['transaction_details']
|
||||
self.assertFalse(transaction_details.startswith('<p>'), 'Transient transaction details should not start with <p> when read.')
|
||||
self.assertFalse(transaction_details.endswith('</p>'), 'Transient transaction details should not end with </p> when read.')
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_bank_statement_line_tree_inherit" model="ir.ui.view">
|
||||
<field name="name">bank.statement.line.list.inherit</field>
|
||||
<field name="model">account.bank.statement.line</field>
|
||||
<field name="inherit_id" ref="odex30_account_accountant.view_bank_statement_line_tree_bank_rec_widget"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='account_number']" position="after">
|
||||
<field name="online_transaction_identifier" optional="hide"/>
|
||||
<field name="online_account_id" optional="hide"/>
|
||||
<field name="online_link_id" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_bank_statement_line_form_bank_rec_widget_inherit" model="ir.ui.view">
|
||||
<field name="name">account.bank.statement.line.form.bank_rec_widget.inherit</field>
|
||||
<field name="model">account.bank.statement.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="odex30_account_accountant.view_bank_statement_line_form_bank_rec_widget"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//footer" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_journal_dashboard_inherit_online_sync" model="ir.ui.view">
|
||||
<field name="name">account.journal.dashboard.inherit.online.sync</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="kanban_dashboard" position="after">
|
||||
<field name="expiring_synchronization_date"/>
|
||||
<field name="expiring_synchronization_due_day"/>
|
||||
<field name="next_link_synchronization"/>
|
||||
<field name="account_online_account_id"/>
|
||||
<field name="account_online_link_state"/>
|
||||
<field name="online_sync_fetching_status"/>
|
||||
</field>
|
||||
|
||||
<xpath expr="//t[@t-set='bank_unconfigured']" position="attributes">
|
||||
<attribute name="t-value" add="!record.account_online_account_id.raw_value" separator=" and "/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//t[@name='empty_journal_helper']/span[@t-if="journal_type == 'bank'"]" position="attributes">
|
||||
<attribute name="t-if" add="!record.account_online_account_id.raw_value" separator=" and "/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[@name='action_configure_bank_journal']" position="attributes">
|
||||
<attribute name="t-if" add="dashboard.display_connect_bank_in_dashboard" separator=" and "/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr='//div[@id="dashboard_bank_cash_left"]' position='inside'>
|
||||
<div t-if="dashboard.bank_statements_source === 'online_sync' and dashboard.show_sync_actions">
|
||||
<t t-if="record.account_online_link_state.raw_value === 'connected' and record.account_online_account_id">
|
||||
<widget name="refresh_spin_widget" groups="account.group_account_manager"/>
|
||||
<span invisible="not expiring_synchronization_date" groups="account.group_account_manager">
|
||||
<widget name="connected_until_widget"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="record.account_online_link_state.raw_value == 'error' || (record.expiring_synchronization_date.raw_value && record.expiring_synchronization_due_day.value <= 0)">
|
||||
<button groups="account.group_account_user" type="object" name="manual_sync" class="btn btn-danger">Reconnect Bank</button>
|
||||
</t>
|
||||
<t t-elif="record.account_online_link_state.raw_value == 'disconnected'">
|
||||
<button groups="account.group_account_user" type="object" name="action_reconnect_online_account" class="btn btn-danger">Reconnect Bank</button>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_card_manage_settings')][field[@name='show_on_dashboard']]" position="attributes">
|
||||
<attribute name="groups"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_card_manage_settings')]/field[@name='show_on_dashboard']" position="attributes">
|
||||
<attribute name="groups">account.group_account_manager</attribute>
|
||||
<attribute name="class" remove="col-4"/>
|
||||
<attribute name="t-att-class">dashboard.display_connect_bank_in_dashboard ? 'col-4' : 'col-6'</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_card_manage_settings')]/div[hasclass('col-6')]" position="attributes">
|
||||
<attribute name="groups">account.group_account_manager</attribute>
|
||||
<attribute name="class" remove="col-4"/>
|
||||
<attribute name="t-att-class">dashboard.display_connect_bank_in_dashboard ? 'col-4' : 'col-6'</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_card_manage_settings')]/field[@name='show_on_dashboard']" position="after">
|
||||
<t t-if="dashboard.display_connect_bank_in_dashboard and dashboard.bank_statements_source === 'online_sync'
|
||||
and record.account_online_link_state.raw_value != 'connected'">
|
||||
<div class="col-4 text-center" groups="account.group_account_basic">
|
||||
<a class="dropdown-item px-0" type="object" name="action_configure_bank_journal">Connect bank</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="dashboard.display_connect_bank_in_dashboard">
|
||||
<div class="col-4 text-center" groups="account.group_account_manager">
|
||||
<a class="dropdown-item px-0" type="object" name="action_configure_bank_journal">Connect bank</a>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//t[@t-name='bank_configuration_placeholder']" position="replace">
|
||||
<t t-if="bank_unconfigured and dashboard.display_connect_bank_in_dashboard">
|
||||
<widget name="bank_configure" groups="account.group_account_manager"/>
|
||||
</t>
|
||||
<t t-else="" t-call="JournalBodyBankCash"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_journal_form" model="ir.ui.view">
|
||||
<field name="name">account.journal.form.online.sync</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='bank_account_number']" position="inside">
|
||||
<field name="account_online_account_id" invisible="1"/>
|
||||
<field name="renewal_contact_email" groups="account.group_account_readonly" invisible="not account_online_account_id"/>
|
||||
<!-- Take one cell in order to have the button aligned with the field and not its label -->
|
||||
<div class="o_cell flex-grow-1 flex-sm-grow-0" invisible="not account_online_account_id"/>
|
||||
<button name="action_send_reminder" class="oe_link oe_inline ps-0 py-0" type="object"
|
||||
invisible="not account_online_account_id">
|
||||
<i class="oi oi-arrow-right me-2"/> Send Now
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="portal_renew_consent" name="Renew Consent">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="False"/>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<h4>Connect your bank account to Odoo</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center mb-4">
|
||||
<div class="col-auto">
|
||||
<img alt="Odoo" src="/web/static/img/odoo_logo.svg" style="height: 5em;" class="align-baseline w-auto"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="js_reconnect">
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-auto">
|
||||
<table class="table table-responsive">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-end h5">Bank</td>
|
||||
<td class="text-start ps-4 h5"><t t-out="bank"/><t t-if="bank">,</t> <t t-out="bank_account"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end h5">Journal</td>
|
||||
<td class="text-start ps-4 h5"><t t-out="journal"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end h5">Latest Balance</td>
|
||||
<td class="text-start ps-4 h5"><span t-att-class="latest_balance > 0 and '' or 'text-danger'" t-out="latest_balance_formatted"/> on <t t-out="latest_sync"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center mb-4 oe_online_sync">
|
||||
<div class="col-auto">
|
||||
<a id="renew_consent_button" role="button" class="btn btn-primary py-2" t-att-iframe-params="iframe_params">
|
||||
<h3 class="mb-0">Connect my Bank</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center" style="color: #d4924a;">
|
||||
<div class="col-auto">
|
||||
Security Tip: always check the domain name of this page, before clicking on the button.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="js_reconnect d-none">
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-auto">
|
||||
<h2>Thank You!</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_online_link_view_form" model="ir.ui.view">
|
||||
<field name="name">account.online.link.form</field>
|
||||
<field name="model">account.online.link</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false">
|
||||
<header>
|
||||
<button name="action_fetch_transactions" string="Fetch Transactions" class="oe_highlight"
|
||||
type="object" groups="account.group_account_user"
|
||||
invisible="state == 'disconnected' or not show_sync_actions"/>
|
||||
<button groups="account.group_account_manager" name="action_update_credentials" string="Update Credentials" class="btn-secondary" type="object" invisible="state == 'disconnected' or not show_sync_actions"/>
|
||||
<button groups="account.group_account_manager" name="action_reconnect_account" string="Reconnect" class="btn-primary" type="object" invisible="state != 'disconnected' or not show_sync_actions"/>
|
||||
<button groups="account.group_account_manager" name="action_new_synchronization" string="Connect" class="btn-primary" type="object" invisible="state != 'disconnected' or not show_sync_actions"/>
|
||||
<field name="state" widget="statusbar" readonly="1"/>
|
||||
<field name="show_sync_actions" invisible="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="False"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="client_id" readonly="1" string="Client id"/>
|
||||
<field name="auto_sync"/>
|
||||
<field name="provider_type" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="last_refresh" string="Last refresh" readonly="1"/>
|
||||
<field name="next_refresh" readonly="1" invisible="not auto_sync"/>
|
||||
<field name="expiring_synchronization_date" readonly="1"/>
|
||||
<field name="company_id" readonly="1" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="account_online_account_ids" nolabel="1" widget="one2many" mode="list" string="Online Accounts" colspan="2">
|
||||
<list create="false" editable="bottom"> <!-- Clicks on records (for edit) are blocked -->
|
||||
<field name="name" required="1"/>
|
||||
<field name="account_number"/>
|
||||
<field name="journal_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_quick_create': True}"
|
||||
domain="[('type', 'in', ['bank', 'credit']), ('account_online_account_id', '=', False), ('company_id', '=', company_id)]"
|
||||
context="{'default_type': 'bank', 'default_bank_statements_source': 'online_sync', 'default_account_online_account_id': id}"/>
|
||||
<field name="last_sync"/>
|
||||
<field name="balance" readonly="1"/>
|
||||
<field name="inverse_balance_sign" groups="base.group_no_one" optional="hide"/>
|
||||
<field name="inverse_transaction_sign" groups="base.group_no_one" optional="hide"/>
|
||||
<button name="action_reset_fetching_status" type="object" string="Reset" help="This button will reset the fetching status"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
<!-- Chatter -->
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_online_account_view_form" model="ir.ui.view">
|
||||
<field name="name">account.online.account.form</field>
|
||||
<field name="model">account.online.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="account_number" readonly="1"/>
|
||||
<field name="journal_ids" widget="many2many_tags" domain="[('type', 'in', ['bank', 'credit']), ('account_online_account_id', '=', False), ('company_id', '=', company_id)]"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="last_sync"/>
|
||||
<field name="balance" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_online_link_view_tree" model="ir.ui.view">
|
||||
<field name="name">account.online.link.list</field>
|
||||
<field name="model">account.online.link</field>
|
||||
<field name="arch" type="xml">
|
||||
<list decoration-danger="state != 'connected'" create="false">
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="state"/>
|
||||
<field name="provider_type" readonly="1"/>
|
||||
<field name="last_refresh" readonly="1"/>
|
||||
<field name="next_refresh" readonly="1" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" readonly="1" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_account_online_link_form">
|
||||
<field name="name">Online Synchronization</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="account_online_link_view_tree"/>
|
||||
<field name="res_model">account.online.link</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Online Synchronization
|
||||
</p>
|
||||
<p>
|
||||
To create a synchronization with your banking institution,<br/>
|
||||
please click on <b>Add a Bank Account</b>.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Online Synchronization"
|
||||
parent="account.account_banks_menu"
|
||||
action="action_account_online_link_form"
|
||||
id="menu_action_online_link_account"
|
||||
groups="base.group_no_one"
|
||||
sequence="4"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_bank_selection_wizard
|
||||
from . import account_journal_missing_transactions
|
||||
from . import account_journal_duplicate_transactions
|
||||
from . import account_bank_statement_line
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue