Merge remote-tracking branch 'source_origin/dev_odex30_accounting' into dev_odex30_accounting
This commit is contained in:
commit
ab76ca2aff
|
|
@ -41,11 +41,14 @@
|
||||||
'account_chart_of_accounts/static/src/js/account_type_selection_extend.js',
|
'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/filters_patch.js',
|
||||||
'account_chart_of_accounts/static/src/js/account_report.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',
|
'account_chart_of_accounts/static/src/xml/filter_full_hierarchy.xml',
|
||||||
],
|
# CRITICAL: SCSS must be in backend, not frontend!
|
||||||
'web.assets_frontend': [
|
|
||||||
'account_chart_of_accounts/static/src/scss/account_hierarchy.scss',
|
'account_chart_of_accounts/static/src/scss/account_hierarchy.scss',
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'installable': True,
|
'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
|
@api.model
|
||||||
def search_panel_select_range(self, field_name, **kwargs):
|
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':
|
if field_name != 'parent_id':
|
||||||
return super().search_panel_select_range(field_name, **kwargs)
|
return super().search_panel_select_range(field_name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
enable_counters = kwargs.get('enable_counters', False)
|
enable_counters = kwargs.get('enable_counters', False)
|
||||||
search_domain = kwargs.get('search_domain', [])
|
search_domain = kwargs.get('search_domain', [])
|
||||||
|
|
||||||
|
# Get ALL accounts for hierarchy
|
||||||
|
all_accounts = self.search([], order='code')
|
||||||
|
|
||||||
all_view_accounts = self.search([('account_type', '=', 'view')], order='code')
|
# Filter accounts to show:
|
||||||
|
# - Root level (parent_id = False): ONLY view accounts WITH children
|
||||||
|
# - Sub levels: ALL accounts (shown when parent is expanded)
|
||||||
|
accounts_to_show = all_accounts.filtered(
|
||||||
|
lambda a: (not a.parent_id and a.account_type == 'view' and a.child_ids) or a.parent_id
|
||||||
|
)
|
||||||
|
|
||||||
count_by_parent = {}
|
count_by_account = {}
|
||||||
if enable_counters:
|
if enable_counters:
|
||||||
all_accounts = self.search(search_domain)
|
# Count records that match the search domain for each account
|
||||||
|
filtered_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
|
|
||||||
|
|
||||||
|
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 = []
|
values = []
|
||||||
for parent in all_view_accounts:
|
for account in accounts_to_show:
|
||||||
value = {
|
value = {
|
||||||
'id': parent.id,
|
'id': account.id,
|
||||||
'display_name': f"{parent.code} {parent.name}" if parent.code else parent.name,
|
'display_name': f"{account.code} {account.name}" if account.code else account.name,
|
||||||
'parent_id': parent.parent_id.id if parent.parent_id else False,
|
'parent_id': account.parent_id.id if account.parent_id else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if enable_counters:
|
if enable_counters:
|
||||||
value['__count'] = count_by_parent.get(parent.id, 0)
|
value['__count'] = count_by_account.get(account.id, 0)
|
||||||
|
|
||||||
values.append(value)
|
values.append(value)
|
||||||
|
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'parent_field': 'parent_id',
|
'parent_field': 'parent_id',
|
||||||
'values': values,
|
'values': values,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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 {
|
// CRITICAL: HTML uses class "account_root" not "account_account"
|
||||||
.o_search_panel_field.parent_id {
|
.o_search_panel.account_root {
|
||||||
|
.o_search_panel_field {
|
||||||
.o_search_panel_category_value {
|
.o_search_panel_category_value {
|
||||||
.o_toggle_fold {
|
.o_toggle_fold {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
|
@ -11,9 +12,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
|
|
||||||
&:hover {
|
// &:hover {
|
||||||
color: #f0f0f0;
|
// color: #f0f0f0;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.o_search_panel_label_title {
|
.o_search_panel_label_title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -25,15 +26,64 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Levels
|
// ONLY use JavaScript-added class (not :has() selector)
|
||||||
&[data-level="0"] {
|
// Only accounts with actual toggle buttons get this class
|
||||||
font-weight: bold;
|
&.has-children-bold {
|
||||||
|
font-weight: 900 !important;
|
||||||
|
|
||||||
|
header,
|
||||||
|
.o_search_panel_label_title {
|
||||||
|
font-weight: 900 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-level="1"] header { padding-left: 20px; }
|
// Levels indentation
|
||||||
&[data-level="2"] header { padding-left: 35px; }
|
&[data-level="1"] header {
|
||||||
&[data-level="3"] header { padding-left: 50px; }
|
padding-left: 20px;
|
||||||
&[data-level="4"] header { padding-left: 65px; }
|
}
|
||||||
|
|
||||||
|
&[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="code" position="after">
|
||||||
<field name="parent_id" optional="hide"/>
|
<field name="parent_id" optional="hide"/>
|
||||||
</field>
|
</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>
|
</field>
|
||||||
</record>
|
</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>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'ODEX Account: 3-Way Matching',
|
||||||
|
'category': 'Account/Accounting',
|
||||||
|
'author': 'ODEX',
|
||||||
|
'summary': 'Manage 3-way matching on bills',
|
||||||
|
'description': """
|
||||||
|
Manage 3-way matching on supplier bills
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
In this system, you can manage the verification process for supplier bills against
|
||||||
|
received goods. This ensures that payments are only made when the items
|
||||||
|
have actually been delivered.
|
||||||
|
|
||||||
|
This feature allows creating the supplier bill based on ordered quantities
|
||||||
|
while keeping the payment pending until the received quantities on the purchase lines
|
||||||
|
match the recorded supplier bill.
|
||||||
|
|
||||||
|
The system introduces a "release to pay" status that marks for each bill
|
||||||
|
whether it is ready for payment.
|
||||||
|
|
||||||
|
Each bill receives one of the following three states:
|
||||||
|
|
||||||
|
- Yes (The bill can be paid)
|
||||||
|
- No (The bill cannot be paid, delivery is pending)
|
||||||
|
- Exception (Differences found between received and billed quantities)
|
||||||
|
""",
|
||||||
|
'depends': ['purchase'],
|
||||||
|
'data': [
|
||||||
|
'views/account_invoice_view.xml',
|
||||||
|
'views/account_journal_dashboard_view.xml'
|
||||||
|
],
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_3way_match
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-01-27 13:54+0000\n"
|
||||||
|
"PO-Revision-Date: 2025-01-27 13:54+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay_manual
|
||||||
|
msgid ""
|
||||||
|
" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills in Exception"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills to Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills to Validate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__exception
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__exception
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__exception
|
||||||
|
msgid "Exception"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__force_release_to_pay
|
||||||
|
msgid "Force Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__force_release_to_pay
|
||||||
|
msgid ""
|
||||||
|
"Indicates whether the 'Should Be Paid' status is defined automatically or "
|
||||||
|
"manually."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_journal
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_move_line
|
||||||
|
msgid "Journal Item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__no
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__no
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__no
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay
|
||||||
|
msgid "Release To Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move_line__can_be_paid
|
||||||
|
msgid "Release to Pay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay_manual
|
||||||
|
msgid "Should Be Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay
|
||||||
|
msgid ""
|
||||||
|
"This field can take the following values :\n"
|
||||||
|
" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__yes
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__yes
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__yes
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_3way_match
|
||||||
|
#
|
||||||
|
# Translators:
|
||||||
|
# Wil ODEX, 2025
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-01-27 13:54+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
|
||||||
|
"Last-Translator: Wil ODEX, 2025\n"
|
||||||
|
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Language: ar\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay_manual
|
||||||
|
msgid ""
|
||||||
|
" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
|
||||||
|
msgstr ""
|
||||||
|
"* نعم: عليك سداد قيمة الفاتورة، لقد استلمت المنتجات \n"
|
||||||
|
"* لا: ليس عليك سداد قيمة الفاتورة، لم تستلم المنتجات\n"
|
||||||
|
" * استثناء: هناك فرق بين الكمية المستلمة والكمية المدفوع قيمتها\n"
|
||||||
|
"هذه الحالة تُحدد تلقائياً، لكن يمكنك فرض الحالة من خلال تحديد اختيار 'فرض الحالة'."
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills in Exception"
|
||||||
|
msgstr "الفواتير المستثناة "
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills to Pay"
|
||||||
|
msgstr "الفواتير بانتظار السداد "
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
msgid "Bills to Validate"
|
||||||
|
msgstr "الفواتير بانتظار التصديق "
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__exception
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__exception
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__exception
|
||||||
|
msgid "Exception"
|
||||||
|
msgstr "استثناء "
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__force_release_to_pay
|
||||||
|
msgid "Force Status"
|
||||||
|
msgstr "فرض الحالة"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__force_release_to_pay
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__force_release_to_pay
|
||||||
|
msgid ""
|
||||||
|
"Indicates whether the 'Should Be Paid' status is defined automatically or "
|
||||||
|
"manually."
|
||||||
|
msgstr "يحدد إذا ما كانت الحالة 'واجبة السداد' تُعين تلقائياً أم يدوياً. "
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_journal
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr "دفتر اليومية"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "قيد اليومية"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model,name:odex30_account_3way_match.model_account_move_line
|
||||||
|
msgid "Journal Item"
|
||||||
|
msgstr "عنصر اليومية"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__no
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__no
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__no
|
||||||
|
msgid "No"
|
||||||
|
msgstr "لا"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay
|
||||||
|
msgid "Release To Pay"
|
||||||
|
msgstr "جاهزة للسداد"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move_line__can_be_paid
|
||||||
|
msgid "Release to Pay"
|
||||||
|
msgstr "جاهزة للسداد"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay_manual
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_3way_match.field_account_move__release_to_pay_manual
|
||||||
|
msgid "Should Be Paid"
|
||||||
|
msgstr "واجبة السداد"
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_bank_statement_line__release_to_pay
|
||||||
|
#: model:ir.model.fields,help:odex30_account_3way_match.field_account_move__release_to_pay
|
||||||
|
msgid ""
|
||||||
|
"This field can take the following values :\n"
|
||||||
|
" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox."
|
||||||
|
msgstr ""
|
||||||
|
"يتحمل هذا الحقل القيم التالية:\n"
|
||||||
|
" * نعم: عليك سداد قيمة الفاتورة، لقد استلمت المنتجات\n"
|
||||||
|
" * لا: ليس عليك سداد قيمة الفاتورة، لم تستلم المنتجات\n"
|
||||||
|
" * استثناء: هناك فرق بين الكمية المستلمة والكمية المدفوع قيمتها\n"
|
||||||
|
"هذه الحالة تُحدد تلقائياً، لكن يمكنك فرض الحالة من خلال تحديد اختيار 'فرض الحالة'."
|
||||||
|
|
||||||
|
#. module: odex30_account_3way_match
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay__yes
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move__release_to_pay_manual__yes
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_3way_match.selection__account_move_line__can_be_paid__yes
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "نعم"
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import account_invoice
|
||||||
|
from . import account_journal_dashboard
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.tools.float_utils import float_compare
|
||||||
|
from odoo.tools.sql import column_exists, create_column
|
||||||
|
|
||||||
|
# Available values for the release_to_pay field.
|
||||||
|
_release_to_pay_status_list = [('yes', 'Yes'), ('no', 'No'), ('exception', 'Exception')]
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = 'account.move'
|
||||||
|
|
||||||
|
def _auto_init(self):
|
||||||
|
if not column_exists(self.env.cr, "account_move", "release_to_pay"):
|
||||||
|
# Create column manually to set default value to 'exception' on postgres level.
|
||||||
|
# This way we avoid heavy computation on module installation.
|
||||||
|
self.env.cr.execute("ALTER TABLE account_move ADD COLUMN release_to_pay VARCHAR DEFAULT 'exception'")
|
||||||
|
|
||||||
|
return super()._auto_init()
|
||||||
|
|
||||||
|
release_to_pay = fields.Selection(
|
||||||
|
_release_to_pay_status_list,
|
||||||
|
compute='_compute_release_to_pay',
|
||||||
|
copy=False,
|
||||||
|
store=True,
|
||||||
|
help="This field can take the following values :\n"
|
||||||
|
" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox.")
|
||||||
|
release_to_pay_manual = fields.Selection(
|
||||||
|
_release_to_pay_status_list,
|
||||||
|
string='Should Be Paid',
|
||||||
|
compute='_compute_release_to_pay_manual', store='True', readonly=False,
|
||||||
|
help=" * Yes: you should pay the bill, you have received the products\n"
|
||||||
|
" * No, you should not pay the bill, you have not received the products\n"
|
||||||
|
" * Exception, there is a difference between received and billed quantities\n"
|
||||||
|
"This status is defined automatically, but you can force it by ticking the 'Force Status' checkbox.")
|
||||||
|
force_release_to_pay = fields.Boolean(
|
||||||
|
string="Force Status",
|
||||||
|
help="Indicates whether the 'Should Be Paid' status is defined automatically or manually.")
|
||||||
|
|
||||||
|
@api.depends('invoice_line_ids.can_be_paid', 'force_release_to_pay', 'payment_state')
|
||||||
|
def _compute_release_to_pay(self):
|
||||||
|
records = self
|
||||||
|
if self.env.context.get('module') == 'odex30_account_3way_match':
|
||||||
|
# on module installation we set 'no' for all paid bills and other non relevant records at once
|
||||||
|
records = records.filtered(lambda r: r.payment_state != 'paid' and r.move_type in ('in_invoice', 'in_refund'))
|
||||||
|
(self - records).release_to_pay = 'no'
|
||||||
|
for invoice in records:
|
||||||
|
if invoice.payment_state == 'paid' or not invoice.is_invoice(include_receipts=True):
|
||||||
|
# no need to pay, if it's already paid
|
||||||
|
invoice.release_to_pay = 'no'
|
||||||
|
elif invoice.force_release_to_pay:
|
||||||
|
#we must use the manual value contained in release_to_pay_manual
|
||||||
|
invoice.release_to_pay = invoice.release_to_pay_manual
|
||||||
|
else:
|
||||||
|
#otherwise we must compute the field
|
||||||
|
result = None
|
||||||
|
for invoice_line in invoice.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_section', 'line_note')):
|
||||||
|
line_status = invoice_line.can_be_paid
|
||||||
|
if line_status == 'exception':
|
||||||
|
#If one line is in exception, the entire bill is
|
||||||
|
result = 'exception'
|
||||||
|
break
|
||||||
|
elif not result:
|
||||||
|
result = line_status
|
||||||
|
elif line_status != result:
|
||||||
|
result = 'exception'
|
||||||
|
break
|
||||||
|
#The last two elif conditions model the fact that a
|
||||||
|
#bill will be in exception if its lines have different status.
|
||||||
|
#Otherwise, its status will be the one all its lines share.
|
||||||
|
|
||||||
|
#'result' can be None if the bill was entirely empty.
|
||||||
|
invoice.release_to_pay = result or 'no'
|
||||||
|
|
||||||
|
@api.depends('release_to_pay', 'force_release_to_pay')
|
||||||
|
def _compute_release_to_pay_manual(self):
|
||||||
|
for invoice in self:
|
||||||
|
if not (invoice.payment_state == 'paid' or not invoice.is_invoice(include_receipts=True) or invoice.force_release_to_pay):
|
||||||
|
invoice.release_to_pay_manual = invoice.release_to_pay
|
||||||
|
|
||||||
|
@api.onchange('release_to_pay_manual')
|
||||||
|
def _onchange_release_to_pay_manual(self):
|
||||||
|
if self.release_to_pay and self.release_to_pay_manual != self.release_to_pay:
|
||||||
|
self.force_release_to_pay = True
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMoveLine(models.Model):
|
||||||
|
_inherit = 'account.move.line'
|
||||||
|
|
||||||
|
def _auto_init(self):
|
||||||
|
if not column_exists(self.env.cr, "account_move_line", "can_be_paid"):
|
||||||
|
# Create column manually to set default value to 'exception' on postgres level.
|
||||||
|
# This way we avoid heavy computation on module installation.
|
||||||
|
self.env.cr.execute("ALTER TABLE account_move_line ADD COLUMN can_be_paid VARCHAR DEFAULT 'exception'")
|
||||||
|
|
||||||
|
return super()._auto_init()
|
||||||
|
|
||||||
|
|
||||||
|
@api.depends('purchase_line_id.qty_received', 'purchase_line_id.qty_invoiced', 'purchase_line_id.product_qty', 'price_unit')
|
||||||
|
def _can_be_paid(self):
|
||||||
|
""" Computes the 'release to pay' status of an invoice line, depending on
|
||||||
|
the invoicing policy of the product linked to it, by calling the dedicated
|
||||||
|
subfunctions. This function also ensures the line is linked to a purchase
|
||||||
|
order (otherwise, can_be_paid will be set as 'exception'), and the price
|
||||||
|
between this order and the invoice did not change (otherwise, again,
|
||||||
|
the line is put in exception).
|
||||||
|
"""
|
||||||
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||||
|
for invoice_line in self:
|
||||||
|
po_line = invoice_line.purchase_line_id
|
||||||
|
if po_line:
|
||||||
|
invoiced_qty = po_line.qty_invoiced
|
||||||
|
received_qty = po_line.qty_received
|
||||||
|
ordered_qty = po_line.product_qty
|
||||||
|
|
||||||
|
# A price difference between the original order and the invoice results in an exception
|
||||||
|
invoice_currency = invoice_line.currency_id
|
||||||
|
order_currency = po_line.currency_id
|
||||||
|
invoice_converted_price = invoice_currency._convert(
|
||||||
|
invoice_line.price_unit, order_currency, invoice_line.company_id, fields.Date.today())
|
||||||
|
if order_currency.compare_amounts(po_line.price_unit, invoice_converted_price) != 0:
|
||||||
|
invoice_line.can_be_paid = 'exception'
|
||||||
|
continue
|
||||||
|
|
||||||
|
if po_line.product_id.purchase_method == 'purchase': # 'on ordered quantities'
|
||||||
|
invoice_line._can_be_paid_ordered_qty(invoiced_qty, received_qty, ordered_qty, precision)
|
||||||
|
else: # 'on received quantities'
|
||||||
|
invoice_line._can_be_paid_received_qty(invoiced_qty, received_qty, ordered_qty, precision)
|
||||||
|
|
||||||
|
else: # Serves as default if the line is not linked to any Purchase.
|
||||||
|
invoice_line.can_be_paid = 'exception'
|
||||||
|
|
||||||
|
def _can_be_paid_ordered_qty(self, invoiced_qty, received_qty, ordered_qty, precision):
|
||||||
|
"""
|
||||||
|
Gives the release_to_pay status of an invoice line for 'on ordered
|
||||||
|
quantity' billing policy, if this line's invoice is related to a purchase order.
|
||||||
|
|
||||||
|
This function sets can_be_paid field to one of the following:
|
||||||
|
'yes': the content of the line has been ordered and can be invoiced
|
||||||
|
'no' : the content of the line hasn't been ordered at all, and cannot be invoiced
|
||||||
|
'exception' : only part of the invoice has been ordered
|
||||||
|
"""
|
||||||
|
if float_compare(invoiced_qty - self.quantity, ordered_qty, precision_digits=precision) >= 0:
|
||||||
|
self.can_be_paid = 'no'
|
||||||
|
elif float_compare(invoiced_qty, ordered_qty, precision_digits=precision) <= 0:
|
||||||
|
self.can_be_paid = 'yes'
|
||||||
|
else:
|
||||||
|
self.can_be_paid = 'exception'
|
||||||
|
|
||||||
|
def _can_be_paid_received_qty(self, invoiced_qty, received_qty, ordered_qty, precision):
|
||||||
|
"""
|
||||||
|
Gives the release_to_pay status of an invoice line for 'on received
|
||||||
|
quantity' billing policy, if this line's invoice is related to a purchase order.
|
||||||
|
|
||||||
|
This function sets can_be_paid field to one of the following:
|
||||||
|
'yes': the content of the line has been received and can be invoiced
|
||||||
|
'no' : the content of the line hasn't been received at all, and cannot be invoiced
|
||||||
|
'exception' : ordered and received quantities differ
|
||||||
|
"""
|
||||||
|
if float_compare(invoiced_qty, received_qty, precision_digits=precision) <= 0:
|
||||||
|
self.can_be_paid = 'yes'
|
||||||
|
elif received_qty == 0 and float_compare(invoiced_qty, ordered_qty, precision_digits=precision) <= 0: # "and" part to ensure a too high billed quantity results in an exception:
|
||||||
|
self.can_be_paid = 'no'
|
||||||
|
else:
|
||||||
|
self.can_be_paid = 'exception'
|
||||||
|
|
||||||
|
can_be_paid = fields.Selection(
|
||||||
|
_release_to_pay_status_list,
|
||||||
|
compute='_can_be_paid',
|
||||||
|
copy=False,
|
||||||
|
store=True,
|
||||||
|
string='Release to Pay')
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
from odoo import fields, models
|
||||||
|
from odoo.osv import expression
|
||||||
|
from odoo.tools import SQL
|
||||||
|
|
||||||
|
|
||||||
|
class AccountJournal(models.Model):
|
||||||
|
_inherit = 'account.journal'
|
||||||
|
|
||||||
|
def open_action(self):
|
||||||
|
action = super(AccountJournal, self).open_action()
|
||||||
|
view = self.env.ref('account.action_move_in_invoice_type')
|
||||||
|
if view and action.get("id") == view.id:
|
||||||
|
action['context']['search_default_in_invoice'] = 0
|
||||||
|
account_purchase_filter = self.env.ref('odex30_account_3way_match.account_invoice_filter_inherit_odex30_account_3way_match', False)
|
||||||
|
action['search_view_id'] = account_purchase_filter and [account_purchase_filter.id, account_purchase_filter.name] or False
|
||||||
|
return action
|
||||||
|
|
||||||
|
def _get_open_sale_purchase_query(self, journal_type):
|
||||||
|
# OVERRIDE
|
||||||
|
assert journal_type in ('sale', 'purchase')
|
||||||
|
query = self.env['account.move']._where_calc([
|
||||||
|
*self.env['account.move']._check_company_domain(self.env.companies),
|
||||||
|
('journal_id', 'in', self.ids),
|
||||||
|
('payment_state', 'in', ('not_paid', 'partial')),
|
||||||
|
('move_type', 'in', ('out_invoice', 'out_refund') if journal_type == 'sale' else ('in_invoice', 'in_refund')),
|
||||||
|
('state', '=', 'posted'),
|
||||||
|
])
|
||||||
|
|
||||||
|
selects = [
|
||||||
|
SQL("journal_id"),
|
||||||
|
SQL("company_id"),
|
||||||
|
SQL("currency_id AS currency"),
|
||||||
|
SQL("invoice_date_due < %s AS late", fields.Date.context_today(self)),
|
||||||
|
SQL("SUM(amount_residual_signed) AS amount_total_company"),
|
||||||
|
SQL("SUM((CASE WHEN move_type = 'in_invoice' THEN -1 ELSE 1 END) * amount_residual) AS amount_total"),
|
||||||
|
SQL("COUNT(*)"),
|
||||||
|
SQL("release_to_pay IN ('yes', 'exception') AS to_pay")
|
||||||
|
]
|
||||||
|
|
||||||
|
return query, selects
|
||||||
|
|
||||||
|
def _get_draft_sales_purchases_query(self):
|
||||||
|
# OVERRIDE
|
||||||
|
domain_sale = [
|
||||||
|
('journal_id', 'in', self.filtered(lambda j: j.type == 'sale').ids),
|
||||||
|
('move_type', 'in', self.env['account.move'].get_sale_types(include_receipts=True))
|
||||||
|
]
|
||||||
|
|
||||||
|
domain_purchase = [
|
||||||
|
('journal_id', 'in', self.filtered(lambda j: j.type == 'purchase').ids),
|
||||||
|
('move_type', 'in', self.env['account.move'].get_purchase_types(include_receipts=False)),
|
||||||
|
'|',
|
||||||
|
('invoice_date_due', '<', fields.Date.today()),
|
||||||
|
('release_to_pay', '=', 'yes')
|
||||||
|
]
|
||||||
|
domain = expression.AND([
|
||||||
|
[('state', '=', 'draft'), ('payment_state', 'in', ('not_paid', 'partial'))],
|
||||||
|
expression.OR([domain_sale, domain_purchase])
|
||||||
|
])
|
||||||
|
return self.env['account.move']._where_calc([
|
||||||
|
*self.env['account.move']._check_company_domain(self.env.companies),
|
||||||
|
*domain
|
||||||
|
])
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import test_release_to_pay_invoice
|
||||||
|
from . import test_account_journal_dashboard
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
from odoo.addons.account.tests.test_account_journal_dashboard_common import TestAccountJournalDashboardCommon
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tools.misc import format_amount
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class AccountJournalDashboard3WayWatchTest(TestAccountJournalDashboardCommon):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=None, amounts=None, taxes=None, company=False, currency=None, journal=None, invoice_date_due=None, release_to_pay=None):
|
||||||
|
move = super().init_invoice(move_type, partner, invoice_date, False, products, amounts, taxes, company, currency, journal)
|
||||||
|
if invoice_date_due:
|
||||||
|
move.invoice_date_due = invoice_date_due
|
||||||
|
if release_to_pay:
|
||||||
|
move.release_to_pay = release_to_pay
|
||||||
|
if post:
|
||||||
|
move.action_post()
|
||||||
|
return move
|
||||||
|
|
||||||
|
def test_sale_purchase_journal_for_purchase(self):
|
||||||
|
"""
|
||||||
|
Test different purchase journal setups with or without multicurrency:
|
||||||
|
1) Journal with no currency, bills in foreign currency -> dashboard data should be displayed in company currency
|
||||||
|
2) Journal in foreign currency, bills in foreign currency -> dashboard data should be displayed in foreign currency
|
||||||
|
3) Journal in foreign currency, bills in company currency -> dashboard data should be displayed in foreign currency
|
||||||
|
4) Journal in company currency, bills in company currency -> dashboard data should be displayed in company currency
|
||||||
|
5) Journal in company currency, bills in foreign currency -> dashboard data should be displayed in company currency
|
||||||
|
"""
|
||||||
|
foreign_currency = self.other_currency
|
||||||
|
company_currency = self.company_data['currency']
|
||||||
|
|
||||||
|
setup_values = [
|
||||||
|
[self.company_data['default_journal_purchase'], foreign_currency],
|
||||||
|
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
|
||||||
|
[self.company_data['default_journal_purchase'].copy({'currency_id': foreign_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
|
||||||
|
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), company_currency],
|
||||||
|
[self.company_data['default_journal_purchase'].copy({'currency_id': company_currency.id, 'default_account_id': self.company_data['default_account_expense'].id}), foreign_currency],
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_vals_list = [
|
||||||
|
# number_draft, sum_draft, number_waiting, sum_waiting, number_late, sum_late, currency
|
||||||
|
[ 1, 100, 1, 55, 1, 55, company_currency],
|
||||||
|
[ 1, 200, 1, 110, 1, 110, foreign_currency],
|
||||||
|
[ 1, 400, 1, 220, 1, 220, foreign_currency],
|
||||||
|
[ 1, 200, 1, 110, 1, 110, company_currency],
|
||||||
|
[ 1, 100, 1, 55, 1, 55, company_currency],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (purchase_journal, bill_currency), expected_vals in zip(setup_values, expected_vals_list):
|
||||||
|
with self.subTest(purchase_journal_currency=purchase_journal.currency_id, bill_currency=bill_currency, expected_vals=expected_vals):
|
||||||
|
bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=True, amounts=[200], currency=bill_currency, journal=purchase_journal)
|
||||||
|
_draft_bill = self.init_invoice('in_invoice', invoice_date='2017-01-01', post=False, amounts=[200], currency=bill_currency, journal=purchase_journal)
|
||||||
|
|
||||||
|
payment = self.init_payment(-90, post=True, date='2017-01-01', currency=bill_currency)
|
||||||
|
(bill + payment.move_id).line_ids.filtered_domain([
|
||||||
|
('account_id', '=', self.company_data['default_account_payable'].id)
|
||||||
|
]).reconcile()
|
||||||
|
|
||||||
|
self.assertDashboardPurchaseSaleData(purchase_journal, *expected_vals)
|
||||||
|
|
||||||
|
@freeze_time("2023-03-15")
|
||||||
|
def test_purchase_journal_numbers_and_sums(self):
|
||||||
|
company_currency = self.company_data['currency']
|
||||||
|
journal = self.company_data['default_journal_purchase']
|
||||||
|
|
||||||
|
self._create_test_vendor_bills(journal)
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
# Expected behavior is to have three moves waiting for payment for a total amount of 4440$ one of which would be late
|
||||||
|
# for a total amount of 40$ (second move has one of three lines late but that's not enough to make the move late)
|
||||||
|
self.assertEqual(3, dashboard_data['number_waiting'])
|
||||||
|
self.assertEqual(format_amount(self.env, 4440, company_currency), dashboard_data['sum_waiting'])
|
||||||
|
self.assertEqual(1, dashboard_data['number_late'])
|
||||||
|
self.assertEqual(format_amount(self.env, 40, company_currency), dashboard_data['sum_late'])
|
||||||
|
|
||||||
|
@freeze_time("2019-01-22")
|
||||||
|
def test_customer_invoice_dashboard(self):
|
||||||
|
journal = self.company_data['default_journal_sale']
|
||||||
|
|
||||||
|
invoice = self.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'journal_id': journal.id,
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'invoice_date': '2019-01-21',
|
||||||
|
'date': '2019-01-21',
|
||||||
|
'invoice_line_ids': [(0, 0, {
|
||||||
|
'product_id': self.product_a.id,
|
||||||
|
'quantity': 40.0,
|
||||||
|
'name': 'product test 1',
|
||||||
|
'discount': 10.00,
|
||||||
|
'price_unit': 2.27,
|
||||||
|
'tax_ids': [],
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
refund = self.env['account.move'].create({
|
||||||
|
'move_type': 'out_refund',
|
||||||
|
'journal_id': journal.id,
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'invoice_date': '2019-01-21',
|
||||||
|
'date': '2019-01-21',
|
||||||
|
'invoice_line_ids': [(0, 0, {
|
||||||
|
'product_id': self.product_a.id,
|
||||||
|
'quantity': 1.0,
|
||||||
|
'name': 'product test 1',
|
||||||
|
'price_unit': 13.3,
|
||||||
|
'tax_ids': [],
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check Draft
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_draft'], 2)
|
||||||
|
self.assertIn('68.42', dashboard_data['sum_draft'])
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_waiting'], 0)
|
||||||
|
self.assertIn('0.00', dashboard_data['sum_waiting'])
|
||||||
|
|
||||||
|
# Check Both
|
||||||
|
invoice.action_post()
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
self.assertEqual(dashboard_data['number_draft'], 1)
|
||||||
|
self.assertIn('-\N{ZERO WIDTH NO-BREAK SPACE}13.30', dashboard_data['sum_draft'])
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_waiting'], 1)
|
||||||
|
self.assertIn('81.72', dashboard_data['sum_waiting'])
|
||||||
|
|
||||||
|
# Check partial on invoice
|
||||||
|
partial_payment = self.env['account.payment'].create({
|
||||||
|
'amount': 13.3,
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'partner_type': 'customer',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
})
|
||||||
|
partial_payment.action_post()
|
||||||
|
|
||||||
|
(invoice + partial_payment.move_id).line_ids.filtered(lambda line: line.account_type == 'asset_receivable').reconcile()
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
self.assertEqual(dashboard_data['number_draft'], 1)
|
||||||
|
self.assertIn('13.3', dashboard_data['sum_draft'])
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_waiting'], 1)
|
||||||
|
self.assertIn('68.42', dashboard_data['sum_waiting'])
|
||||||
|
|
||||||
|
# Check waiting payment
|
||||||
|
refund.action_post()
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
self.assertEqual(dashboard_data['number_draft'], 0)
|
||||||
|
self.assertIn('0.00', dashboard_data['sum_draft'])
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_waiting'], 2)
|
||||||
|
self.assertIn('55.12', dashboard_data['sum_waiting'])
|
||||||
|
|
||||||
|
# Check partial on refund
|
||||||
|
payment = self.env['account.payment'].create({
|
||||||
|
'amount': 10.0,
|
||||||
|
'payment_type': 'outbound',
|
||||||
|
'partner_type': 'customer',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
})
|
||||||
|
payment.action_post()
|
||||||
|
|
||||||
|
(refund + payment.move_id).line_ids\
|
||||||
|
.filtered(lambda line: line.account_type == 'asset_receivable')\
|
||||||
|
.reconcile()
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
self.assertEqual(dashboard_data['number_draft'], 0)
|
||||||
|
self.assertIn('0.00', dashboard_data['sum_draft'])
|
||||||
|
|
||||||
|
self.assertEqual(dashboard_data['number_waiting'], 2)
|
||||||
|
self.assertIn('65.12', dashboard_data['sum_waiting'])
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
self.assertEqual(dashboard_data['number_late'], 2)
|
||||||
|
self.assertIn('65.12', dashboard_data['sum_late'])
|
||||||
|
|
||||||
|
def test_sale_purchase_journal_for_multi_currency_sale(self):
|
||||||
|
currency = self.other_currency
|
||||||
|
company_currency = self.company_data['currency']
|
||||||
|
|
||||||
|
invoice = self.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'invoice_date': '2017-01-01',
|
||||||
|
'date': '2017-01-01',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'currency_id': currency.id,
|
||||||
|
'invoice_line_ids': [
|
||||||
|
(0, 0, {'name': 'test', 'price_unit': 200})
|
||||||
|
],
|
||||||
|
})
|
||||||
|
invoice.action_post()
|
||||||
|
|
||||||
|
payment = self.env['account.payment'].create({
|
||||||
|
'amount': 90.0,
|
||||||
|
'date': '2016-01-01',
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'partner_type': 'customer',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'currency_id': currency.id,
|
||||||
|
})
|
||||||
|
payment.action_post()
|
||||||
|
|
||||||
|
(invoice + payment.move_id).line_ids.filtered_domain([
|
||||||
|
('account_id', '=', self.company_data['default_account_receivable'].id)
|
||||||
|
]).reconcile()
|
||||||
|
|
||||||
|
default_journal_sale = self.company_data['default_journal_sale']
|
||||||
|
dashboard_data = default_journal_sale._get_journal_dashboard_data_batched()[default_journal_sale.id]
|
||||||
|
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_waiting'])
|
||||||
|
self.assertEqual(format_amount(self.env, 55, company_currency), dashboard_data['sum_late'])
|
||||||
|
|
||||||
|
@freeze_time("2023-03-15")
|
||||||
|
def test_purchase_journal_numbers_and_sums_to_validate(self):
|
||||||
|
company_currency = self.company_data['currency']
|
||||||
|
journal = self.company_data['default_journal_purchase']
|
||||||
|
|
||||||
|
datas = [
|
||||||
|
{'invoice_date_due': '2023-04-30'},
|
||||||
|
{'invoice_date_due': '2023-04-30', 'release_to_pay': 'yes'},
|
||||||
|
{'invoice_date_due': '2023-04-30', 'release_to_pay': 'no'},
|
||||||
|
{'invoice_date_due': '2023-03-01'},
|
||||||
|
{'invoice_date_due': '2023-03-01', 'release_to_pay': 'yes'},
|
||||||
|
{'invoice_date_due': '2023-03-01', 'release_to_pay': 'no'},
|
||||||
|
]
|
||||||
|
|
||||||
|
for data in datas:
|
||||||
|
self.init_invoice('in_invoice', invoice_date='2023-03-01', post=False, amounts=[4000], journal=journal, invoice_date_due=data['invoice_date_due'], release_to_pay=data.get('release_to_pay'))
|
||||||
|
|
||||||
|
dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id]
|
||||||
|
# Expected behavior is to have six amls waiting for payment for a total amount of 4440$
|
||||||
|
# three of which would be late for a total amount of 140$
|
||||||
|
self.assertEqual(4, dashboard_data['number_draft'])
|
||||||
|
self.assertEqual(format_amount(self.env, 16000, company_currency), dashboard_data['sum_draft'])
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import Command, fields
|
||||||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
|
from odoo.tests import tagged, Form
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestReleaseToPayInvoice(AccountTestInvoicingCommon):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
cls.partner = cls.env['res.partner'].create({'name': 'Zizizapartner'})
|
||||||
|
cls.product = cls.env['product.product'].create({
|
||||||
|
'name': 'VR Computer',
|
||||||
|
'standard_price': 2500.0,
|
||||||
|
'list_price': 2899.0,
|
||||||
|
'type': 'service',
|
||||||
|
'default_code': 'VR-01',
|
||||||
|
'weight': 1.0,
|
||||||
|
'purchase_method': 'receive',
|
||||||
|
})
|
||||||
|
cls.other_currency = cls.setup_other_currency('HRK', rounding=0.001)
|
||||||
|
|
||||||
|
def check_release_to_pay_scenario(self, ordered_qty, scenario, invoicing_policy='receive', order_price=500.0):
|
||||||
|
""" Generic test function to check that each use scenario behaves properly.
|
||||||
|
"""
|
||||||
|
self.product.purchase_method = invoicing_policy
|
||||||
|
|
||||||
|
purchase_order = self.env['purchase.order'].create({
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'order_line': [
|
||||||
|
(0, 0, {
|
||||||
|
'name': self.product.name,
|
||||||
|
'product_id': self.product.id,
|
||||||
|
'product_qty': ordered_qty,
|
||||||
|
'product_uom': self.product.uom_po_id.id,
|
||||||
|
'price_unit': order_price,
|
||||||
|
'date_planned': fields.Datetime.now(),
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
invoices_list = []
|
||||||
|
purchase_line = purchase_order.order_line[-1]
|
||||||
|
AccountMove = self.env['account.move'].with_context(default_move_type='in_invoice')
|
||||||
|
for (action, params) in scenario:
|
||||||
|
if action == 'invoice':
|
||||||
|
# <field name="purchase_id" invisible="1"/>
|
||||||
|
move_form = Form(AccountMove.with_context(default_purchase_id=purchase_order.id))
|
||||||
|
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||||
|
if 'price' in params:
|
||||||
|
line_form.price_unit = params['price']
|
||||||
|
if 'qty' in params:
|
||||||
|
line_form.quantity = params['qty']
|
||||||
|
new_invoice = move_form.save()
|
||||||
|
new_invoice.write({'invoice_line_ids': [
|
||||||
|
Command.create({'display_type': 'line_section', 'name': 'Section'}),
|
||||||
|
Command.create({'display_type': 'line_note', 'name': 'Note'}),
|
||||||
|
]})
|
||||||
|
invoices_list.append(new_invoice)
|
||||||
|
|
||||||
|
self.assertEqual(new_invoice.release_to_pay, params['rslt'], "Wrong invoice release to pay status for scenario " + str(scenario))
|
||||||
|
|
||||||
|
elif action == 'receive':
|
||||||
|
purchase_line.write({'qty_received': params['qty']}) # as the product is a service, its recieved quantity is set manually
|
||||||
|
|
||||||
|
if 'rslt' in params:
|
||||||
|
for (invoice_index, status) in params['rslt']:
|
||||||
|
self.assertEqual(invoices_list[invoice_index].release_to_pay, status, "Wrong invoice release to pay status for scenario " + str(scenario))
|
||||||
|
|
||||||
|
def test_3_way_match(self):
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'yes'})], invoicing_policy='purchase')
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 10, 'rslt': 'yes'})], invoicing_policy='purchase')
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 10, 'rslt': 'yes'})], invoicing_policy='purchase')
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'yes'}), ('receive',{'qty': 5}), ('invoice', {'qty': 6, 'rslt': 'exception'})], invoicing_policy='purchase')
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 10, 'rslt': 'yes'}), ('invoice', {'qty': 10, 'rslt': 'no'})], invoicing_policy='purchase')
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'yes'})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 10, 'rslt': 'exception'})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 5, 'rslt': [(-1, 'yes')]})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 3, 'rslt': [(-1, 'exception')]})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('invoice', {'qty': 5, 'rslt': 'no'}), ('receive', {'qty': 10, 'rslt': [(-1, 'yes')]})])
|
||||||
|
|
||||||
|
# Special use case : a price change between order and invoice should always put the bill in exception
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'exception', 'price':42})])
|
||||||
|
self.check_release_to_pay_scenario(10, [('receive',{'qty': 5}), ('invoice', {'qty': 5, 'rslt': 'exception', 'price':42})], invoicing_policy='purchase')
|
||||||
|
|
||||||
|
def test_amount_currency_edit(self):
|
||||||
|
"""
|
||||||
|
Ensure that editing the `amount_currency` of a journal item on an invoice is possible.
|
||||||
|
In 17.0 changes to Binary fields and web_save were made (related to context key 'bin_size').
|
||||||
|
They led to tracebacks in the flow tested here.
|
||||||
|
"""
|
||||||
|
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||||
|
move_form.invoice_date = fields.Date.from_string('2023-01-01')
|
||||||
|
move_form.partner_id = self.partner_a
|
||||||
|
move_form.currency_id = self.other_currency
|
||||||
|
with move_form.invoice_line_ids.new() as line_form:
|
||||||
|
line_form.quantity = 1
|
||||||
|
line_form.price_unit = 10
|
||||||
|
move_form.save()
|
||||||
|
with move_form.line_ids.edit(0) as line_form:
|
||||||
|
line_form.amount_currency = -30
|
||||||
|
move_form.save()
|
||||||
|
self.assertEqual(move_form.line_ids.edit(0).amount_currency, -30)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="account_invoice_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.form.inherit</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@id='other_tab']//field[@name='fiscal_position_id']" position='after'>
|
||||||
|
<label for="release_to_pay_manual" invisible="move_type not in ('in_invoice', 'in_refund')"/>
|
||||||
|
<div class="o_row" col="4" invisible="move_type not in ('in_invoice', 'in_refund')">
|
||||||
|
<field name="release_to_pay" invisible="True" force_save="1"/>
|
||||||
|
<field name="release_to_pay_manual"/>
|
||||||
|
<label class="fw-bold" for="force_release_to_pay" invisible="not force_release_to_pay"/>
|
||||||
|
<field name="force_release_to_pay" invisible="not force_release_to_pay"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_invoice_filter_inherit_odex30_account_3way_match" model="ir.ui.view">
|
||||||
|
<field name="name">account.invoice.select.inherit.odex30_account_3way_match</field>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_bill_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[@name='late']" position='after'>
|
||||||
|
<separator/>
|
||||||
|
<filter name="bills_to_validate" string="Bills to Validate" domain="['&', '|', ('release_to_pay','=', 'yes'), ('invoice_date_due', '<', time.strftime('%Y-%m-%d')), ('state', '=', 'draft')]"/>
|
||||||
|
<filter name="bills_to_pay" string="Bills to Pay" domain="['&', '&', ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), ('release_to_pay','in', ('yes', 'exception'))]"/>
|
||||||
|
<filter name="exception" string="Bills in Exception" domain="[('release_to_pay','=', 'exception')]"/>
|
||||||
|
<separator/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--This action has been redefined and the account_invoice_filter_inherit_odex30_account_3way_match
|
||||||
|
created in order to only display 'bills_to_pay' and 'exception' filters
|
||||||
|
in the view related to vendor bills, as it makes no sense to propose them
|
||||||
|
in the view related to sales invoices, which share the same model.-->
|
||||||
|
<record id="account.action_move_in_invoice_type" model="ir.actions.act_window">
|
||||||
|
<field name="search_view_id" ref="account_invoice_filter_inherit_odex30_account_3way_match"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account.action_move_in_refund_type" model="ir.actions.act_window">
|
||||||
|
<field name="search_view_id" ref="account_invoice_filter_inherit_odex30_account_3way_match"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="account_journal_dashboard_kanban_view_3_way_match" model="ir.ui.view">
|
||||||
|
<field name="name">account.journal.dashboard.kanban</field>
|
||||||
|
<field name="model">account.journal</field>
|
||||||
|
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//a[span[@id='account_dashboard_purchase_draft']]" position="attributes">
|
||||||
|
<attribute name="context">
|
||||||
|
{'search_default_bills_to_validate': 1}
|
||||||
|
</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//a[span[@id='account_dashboard_bills_to_pay']]" position="attributes">
|
||||||
|
<attribute name="context">
|
||||||
|
{'search_default_bills_to_pay':1}
|
||||||
|
</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//a[span[@id='account_dashboard_bills_late']]" position="attributes">
|
||||||
|
<attribute name="context">
|
||||||
|
{'search_default_late':1}
|
||||||
|
</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Account Batch Payment Reconciliation',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Accounting',
|
||||||
|
'summary': 'Allows using Reconciliation with the Batch Payment feature.',
|
||||||
|
'depends': ['odex30_account_accountant', 'odex30_account_batch_payment'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
|
||||||
|
'views/bank_rec_widget_views.xml',
|
||||||
|
'views/account_batch_payment_rejection_views.xml',
|
||||||
|
],
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'OEEL-1',
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'odex30_account_accountant_batch_payment/static/src/components/**/*',
|
||||||
|
],
|
||||||
|
'web.assets_tests': [
|
||||||
|
'odex30_account_accountant_batch_payment/static/tests/tours/*.js',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_accountant_batch_payment
|
||||||
|
#
|
||||||
|
# Translators:
|
||||||
|
# Malaz Abuidris <msea@odoo.com>, 2024
|
||||||
|
# Wil Odoo, 2025
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-01-27 13:54+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
|
||||||
|
"Last-Translator: Wil Odoo, 2025\n"
|
||||||
|
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Language: ar\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid ""
|
||||||
|
"<br/>\n"
|
||||||
|
" <span>Do you want to cancel payments to retry them later or keep the batch open with unprocess payments, if you expect them later.</span>"
|
||||||
|
msgstr ""
|
||||||
|
"<br/>\n"
|
||||||
|
" <span>هل ترغب في إلغاء عمليات الدفع لإعادة المحاولة لاحقاً أو ترك الدفعة مفتوحة مع عمليات دفع غير معالَجة، إذا كنت تتوقع أن تتم معالجتهم لاحقاً.</span>"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
|
||||||
|
msgid "Amount Due"
|
||||||
|
msgstr "المبلغ المستحق"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
|
||||||
|
msgid "Amount Due (in currency)"
|
||||||
|
msgstr "المبلغ المستحق (بالعملة) "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_bank_rec_widget
|
||||||
|
msgid "Bank reconciliation widget for a single statement line"
|
||||||
|
msgstr "أداة التسوية البنكية لبند كشف حساب واحد "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_accountant_batch_payment/models/account_batch_payment.py:0
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
|
||||||
|
msgid "Batch Payment"
|
||||||
|
msgstr "دفعة مجمعة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#. odoo-javascript
|
||||||
|
#: code:addons/odex30_account_accountant_batch_payment/static/src/components/bank_reconciliation/bank_rec_form.xml:0
|
||||||
|
msgid "Batch Payments"
|
||||||
|
msgstr "الدفعات المجمعة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "إلغاء"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "Cancel Payments"
|
||||||
|
msgstr "إلغاء المدفوعات "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr "أنشئ بواسطة"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr "أنشئ في"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "التاريخ"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "اسم العرض "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_accountant_batch_payment/models/bank_rec_widget.py:0
|
||||||
|
msgid "Exchange Difference: %(batch_name)s - %(currency)s"
|
||||||
|
msgstr "فرق سعر الصرف: %(batch_name)s - %(currency)s "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "Expect Payments Later"
|
||||||
|
msgstr "توقع المدفوعات في وقت لاحق"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__flag
|
||||||
|
msgid "Flag"
|
||||||
|
msgstr "إبلاغ"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "المُعرف"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__in_reconcile_payment_ids
|
||||||
|
msgid "In Reconcile Payment"
|
||||||
|
msgstr "في تسوية الدفع "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_accountant_batch_payment/models/bank_rec_widget.py:0
|
||||||
|
msgid "Includes %(count)s payment(s)"
|
||||||
|
msgstr "يتضمن %(count)s مدفوعات "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr "آخر تحديث بواسطة"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr "آخر تحديث في"
|
||||||
|
|
||||||
|
#. module: account_accountant_batch_payment
|
||||||
|
#: model:ir.model,name:account_accountant_batch_payment.model_bank_rec_widget_line
|
||||||
|
msgid "Line of the bank reconciliation widget"
|
||||||
|
msgstr "بند أداة التسوية البنكية "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_batch_payment_rejection
|
||||||
|
msgid "Manage the payment rejection from batch payments"
|
||||||
|
msgstr "قم بإدارة حالات رفض الدفع من المدفوعات المجمعة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__nb_batch_payment_ids
|
||||||
|
msgid "Nb Batch Payment"
|
||||||
|
msgstr "رقم الدفعة المجمعة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__nb_rejected_payment_ids
|
||||||
|
msgid "Nb Rejected Payment"
|
||||||
|
msgstr "ملاحظة الدفعة مرفوضة"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
|
||||||
|
msgid "Paid"
|
||||||
|
msgstr "مدفوع"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_batch_payment.model_account_reconcile_model
|
||||||
|
msgid ""
|
||||||
|
"Preset to create journal entries during a invoices and payments matching"
|
||||||
|
msgstr "الإعداد المسبق لإنشاء قيود يومية خلال مطابقة الفواتير والدفعات"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
|
||||||
|
msgid "Received"
|
||||||
|
msgstr "تم الاستلام "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_account_batch_payment_rejection__rejected_payment_ids
|
||||||
|
msgid "Rejected Payment"
|
||||||
|
msgstr "عمليات الدفع المرفوضة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget__selected_batch_payment_ids
|
||||||
|
msgid "Selected Batch Payment"
|
||||||
|
msgstr "الدفعة المجمعة المحددة "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__source_batch_payment_id
|
||||||
|
msgid "Source Batch Payment"
|
||||||
|
msgstr "الدفعة المجمعة المصدرية "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_batch_payment.field_bank_rec_widget_line__source_batch_payment_name
|
||||||
|
msgid "Source Batch Payment Name"
|
||||||
|
msgstr "اسم الدفعة المجمعة المصدرية "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
|
||||||
|
msgid "Suggestions"
|
||||||
|
msgstr "الاقتراحات "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget
|
||||||
|
msgid "Unreconciled"
|
||||||
|
msgstr "غير المسواة"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget
|
||||||
|
msgid "View"
|
||||||
|
msgstr "أداة العرض"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "batches have been removed."
|
||||||
|
msgstr "تمت إزالة الدفعات."
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_accountant_batch_payment.selection__bank_rec_widget_line__flag__new_batch
|
||||||
|
msgid "new_batch"
|
||||||
|
msgstr "new_batch"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "payments from"
|
||||||
|
msgstr "المدفوعات من"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_batch_payment
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_accountant_batch_payment.view_account_batch_payment_rejection_form
|
||||||
|
msgid "payments from the batch have been removed."
|
||||||
|
msgstr "تمت إزالة المدفوعات من الدفعة. "
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
from . import account_batch_payment
|
||||||
|
from . import account_reconcile_model
|
||||||
|
from . import bank_rec_widget
|
||||||
|
from . import bank_rec_widget_line
|
||||||
|
from . import account_batch_payment_rejection
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
from odoo import models, _
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBatchPayment(models.Model):
|
||||||
|
_inherit = 'account.batch.payment'
|
||||||
|
|
||||||
|
def action_open_batch_payment(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': _("Batch Payment"),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_id': self.env.ref('account_batch_payment.view_batch_payment_form').id,
|
||||||
|
'res_model': self._name,
|
||||||
|
'res_id': self.id,
|
||||||
|
'context': {
|
||||||
|
'create': False,
|
||||||
|
'delete': False,
|
||||||
|
},
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models, Command
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBatchPaymentRejection(models.TransientModel):
|
||||||
|
_name = 'account.batch.payment.rejection'
|
||||||
|
_description = "Manage the payment rejection from batch payments"
|
||||||
|
|
||||||
|
in_reconcile_payment_ids = fields.Many2many(comodel_name='account.payment')
|
||||||
|
|
||||||
|
rejected_payment_ids = fields.Many2many(
|
||||||
|
comodel_name='account.payment',
|
||||||
|
compute='_compute_rejected_payment_ids',
|
||||||
|
)
|
||||||
|
nb_rejected_payment_ids = fields.Integer(compute='_compute_rejected_payment_ids')
|
||||||
|
nb_batch_payment_ids = fields.Integer(compute='_compute_rejected_payment_ids')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _fetch_rejected_payment_ids(self, in_reconcile_payments):
|
||||||
|
|
||||||
|
batch_ids = in_reconcile_payments.batch_payment_id.ids
|
||||||
|
if batch_ids:
|
||||||
|
return self.env['account.payment'].search([
|
||||||
|
('is_matched', '=', False),
|
||||||
|
('batch_payment_id', 'in', batch_ids),
|
||||||
|
('id', 'not in', in_reconcile_payments.ids),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
return self.env['account.payment']
|
||||||
|
|
||||||
|
@api.depends('in_reconcile_payment_ids')
|
||||||
|
def _compute_rejected_payment_ids(self):
|
||||||
|
for wizard in self:
|
||||||
|
rejected_payments = wizard._fetch_rejected_payment_ids(wizard.in_reconcile_payment_ids)
|
||||||
|
wizard.rejected_payment_ids = [Command.set(rejected_payments.ids)]
|
||||||
|
wizard.nb_rejected_payment_ids = len(wizard.rejected_payment_ids)
|
||||||
|
wizard.nb_batch_payment_ids = len(rejected_payments.batch_payment_id)
|
||||||
|
|
||||||
|
def button_cancel_payments(self):
|
||||||
|
self.rejected_payment_ids.batch_payment_id = False
|
||||||
|
to_unlink = self.rejected_payment_ids.move_id.filtered(lambda x: not x._get_violated_lock_dates(x.date, False))
|
||||||
|
to_reject = self.rejected_payment_ids.move_id - to_unlink
|
||||||
|
if to_unlink:
|
||||||
|
to_unlink.button_draft()
|
||||||
|
to_unlink.button_cancel()
|
||||||
|
if to_reject:
|
||||||
|
to_reject._reverse_moves(cancel=True)
|
||||||
|
return {'type': 'ir.actions.act_window_close', 'infos': 'validate'}
|
||||||
|
|
||||||
|
def button_continue(self):
|
||||||
|
return {'type': 'ir.actions.act_window_close', 'infos': 'validate'}
|
||||||
|
|
||||||
|
def button_cancel(self):
|
||||||
|
return True
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
from odoo.tools import SQL
|
||||||
|
|
||||||
|
|
||||||
|
class AccountReconcileModel(models.Model):
|
||||||
|
_inherit = 'account.reconcile.model'
|
||||||
|
|
||||||
|
def _get_invoice_matching_batch_payments_candidates(self, st_line, partner):
|
||||||
|
assert self.rule_type == 'invoice_matching'
|
||||||
|
self.env['account.batch.payment'].flush_model()
|
||||||
|
|
||||||
|
_numerical_tokens, exact_tokens, _text_tokens = self._get_invoice_matching_st_line_tokens(st_line)
|
||||||
|
if not exact_tokens:
|
||||||
|
return
|
||||||
|
|
||||||
|
batches = self.env['account.batch.payment'].search([('state', '!=', 'reconciled'), ('name', 'in', exact_tokens)])
|
||||||
|
if not batches:
|
||||||
|
return
|
||||||
|
|
||||||
|
aml_domain = self._get_invoice_matching_amls_domain(st_line, partner)
|
||||||
|
query = self.env['account.move.line']._where_calc(aml_domain)
|
||||||
|
|
||||||
|
candidate_ids = [r[0] for r in self.env.execute_query(SQL(
|
||||||
|
'''
|
||||||
|
SELECT DISTINCT account_move_line.id
|
||||||
|
FROM %s
|
||||||
|
JOIN account_payment pay ON pay.id = account_move_line.payment_id
|
||||||
|
JOIN account_batch_payment batch
|
||||||
|
ON batch.id = pay.batch_payment_id
|
||||||
|
AND batch.id = ANY(%s)
|
||||||
|
AND batch.state != 'reconciled'
|
||||||
|
WHERE %s
|
||||||
|
''',
|
||||||
|
query.from_clause,
|
||||||
|
[batches.ids],
|
||||||
|
query.where_clause or SQL("TRUE"),
|
||||||
|
))]
|
||||||
|
if candidate_ids:
|
||||||
|
return {
|
||||||
|
'allow_auto_reconcile': True,
|
||||||
|
'amls': self.env['account.move.line'].browse(candidate_ids),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_invoice_matching_rules_map(self):
|
||||||
|
# EXTENDS account
|
||||||
|
res = super()._get_invoice_matching_rules_map()
|
||||||
|
res[0].append(self._get_invoice_matching_batch_payments_candidates)
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,392 @@
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models, Command
|
||||||
|
from odoo.tools import SQL
|
||||||
|
from odoo.addons.web.controllers.utils import clean_action
|
||||||
|
|
||||||
|
|
||||||
|
class BankRecWidget(models.Model):
|
||||||
|
_inherit = 'bank.rec.widget'
|
||||||
|
|
||||||
|
selected_batch_payment_ids = fields.Many2many(
|
||||||
|
comodel_name='account.batch.payment',
|
||||||
|
compute='_compute_selected_batch_payment_ids',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_available_amls_in_batch_payments(self, batch_payments=None):
|
||||||
|
self.ensure_one()
|
||||||
|
st_line = self.st_line_id
|
||||||
|
|
||||||
|
amls_domain = st_line._get_default_amls_matching_domain()
|
||||||
|
query = self.env['account.move.line']._where_calc(amls_domain)
|
||||||
|
rows = self.env.execute_query(SQL(
|
||||||
|
'''
|
||||||
|
SELECT
|
||||||
|
pay.batch_payment_id,
|
||||||
|
ARRAY_AGG(account_move_line.id) AS aml_ids
|
||||||
|
FROM %s
|
||||||
|
JOIN account_payment pay ON pay.id = account_move_line.payment_id
|
||||||
|
JOIN account_batch_payment batch ON batch.id = pay.batch_payment_id
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
AND pay.batch_payment_id IS NOT NULL
|
||||||
|
AND batch.state != 'reconciled'
|
||||||
|
GROUP BY pay.batch_payment_id
|
||||||
|
''',
|
||||||
|
query.from_clause,
|
||||||
|
query.where_clause or SQL("TRUE"),
|
||||||
|
SQL("pay.batch_payment_id IN %s", tuple(batch_payments.ids)) if batch_payments else SQL("TRUE")
|
||||||
|
))
|
||||||
|
return {r[0]: r[1] for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
@api.depends('company_id', 'line_ids.source_batch_payment_id')
|
||||||
|
def _compute_selected_batch_payment_ids(self):
|
||||||
|
for wizard in self:
|
||||||
|
batch_payment_x_amls = defaultdict(set)
|
||||||
|
new_batches = wizard.line_ids.filtered(lambda x: x.flag == 'new_batch')
|
||||||
|
new_batch_payments = new_batches.source_batch_payment_id
|
||||||
|
new_amls = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml')
|
||||||
|
for new_aml in new_amls:
|
||||||
|
if new_aml.source_batch_payment_id:
|
||||||
|
batch_payment_x_amls[new_aml.source_batch_payment_id].add(new_aml.source_aml_id.id)
|
||||||
|
|
||||||
|
selected_batch_payment_ids = []
|
||||||
|
if batch_payment_x_amls:
|
||||||
|
batch_payments = wizard.line_ids.source_batch_payment_id
|
||||||
|
available_amls_in_batch_payments = wizard._fetch_available_amls_in_batch_payments(batch_payments=batch_payments)
|
||||||
|
selected_batch_payment_ids = [
|
||||||
|
x.id
|
||||||
|
for x in batch_payments
|
||||||
|
if batch_payment_x_amls[x] == set(available_amls_in_batch_payments.get(x.id, []))
|
||||||
|
]
|
||||||
|
if new_batch_payments:
|
||||||
|
selected_batch_payment_ids += new_batch_payments.ids
|
||||||
|
|
||||||
|
wizard.selected_batch_payment_ids = [Command.set(selected_batch_payment_ids)]
|
||||||
|
|
||||||
|
@api.depends('company_id', 'line_ids.source_aml_id', 'line_ids.source_batch_payment_id')
|
||||||
|
def _compute_selected_aml_ids(self):
|
||||||
|
super()._compute_selected_aml_ids()
|
||||||
|
for wizard in self:
|
||||||
|
new_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch')
|
||||||
|
for batch in new_batches.source_batch_payment_id:
|
||||||
|
wizard.selected_aml_ids += self._get_amls_from_batch_payments(batch, include_invoice_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_embedded_views_data(self):
|
||||||
|
results = super()._prepare_embedded_views_data()
|
||||||
|
st_line = self.st_line_id
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'search_view_ref': 'odex30_account_accountant_batch_payment.view_account_batch_payment_search_bank_rec_widget',
|
||||||
|
'list_view_ref': 'odex30_account_accountant_batch_payment.view_account_batch_payment_list_bank_rec_widget',
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic_filters = []
|
||||||
|
|
||||||
|
journal = st_line.journal_id
|
||||||
|
dynamic_filters.append({
|
||||||
|
'name': 'same_journal',
|
||||||
|
'description': journal.display_name,
|
||||||
|
'domain': [('journal_id', '=', journal.id)],
|
||||||
|
})
|
||||||
|
context['search_default_same_journal'] = True
|
||||||
|
context['search_default_unreconciled'] = True
|
||||||
|
|
||||||
|
if self.transaction_currency_id != self.company_currency_id:
|
||||||
|
context['search_default_currency_id'] = self.transaction_currency_id.id
|
||||||
|
|
||||||
|
for dynamic_filter in dynamic_filters:
|
||||||
|
dynamic_filter['domain'] = str(dynamic_filter['domain'])
|
||||||
|
|
||||||
|
results['batch_payments'] = {
|
||||||
|
'domain': [],
|
||||||
|
'dynamic_filters': dynamic_filters,
|
||||||
|
'context': context,
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _lines_prepare_new_aml_line(self, aml, **kwargs):
|
||||||
|
return super()._lines_prepare_new_aml_line(
|
||||||
|
aml,
|
||||||
|
source_batch_payment_id=aml.payment_id.batch_payment_id.id or aml.move_id.matched_payment_ids.batch_payment_id[:1].id,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_amls_from_batch_payments(self, batch_payments, include_invoice_only=False):
|
||||||
|
amls_domain = self.st_line_id._get_default_amls_matching_domain()
|
||||||
|
amls = self.env['account.move.line']
|
||||||
|
for batch in batch_payments:
|
||||||
|
for payment in batch.payment_ids:
|
||||||
|
if payment.move_id:
|
||||||
|
liquidity_lines, _counterpart_lines, _writeoff_lines = payment._seek_for_lines()
|
||||||
|
amls |= liquidity_lines.filtered_domain(amls_domain)
|
||||||
|
elif payment.invoice_ids and include_invoice_only:
|
||||||
|
amls |= payment.invoice_ids.line_ids.filtered(lambda line: line.account_id.account_type in payment._get_valid_payment_account_types())
|
||||||
|
return amls
|
||||||
|
|
||||||
|
def _lines_prepare_new_batch_line(self, batch_payment, **kwargs):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'source_batch_payment_id': batch_payment.id,
|
||||||
|
'flag': 'new_batch',
|
||||||
|
'currency_id': batch_payment.payment_ids.currency_id.id if len(batch_payment.payment_ids.currency_id) == 1 else False,
|
||||||
|
'amount_currency': -batch_payment.amount_residual_currency,
|
||||||
|
'balance': -batch_payment.amount_residual,
|
||||||
|
'source_amount_currency': -batch_payment.amount_residual_currency,
|
||||||
|
'source_balance': -batch_payment.amount_residual,
|
||||||
|
'source_batch_payment_name': _("Includes %(count)s payment(s)", count=str(len(batch_payment.payment_ids.filtered(lambda p: p.state == 'in_process')))),
|
||||||
|
'date': batch_payment.date,
|
||||||
|
'name': batch_payment.name,
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_amls_vals_from_payment(self, payment):
|
||||||
|
amls_line_vals = []
|
||||||
|
amls_domain = self.st_line_id._get_default_amls_matching_domain()
|
||||||
|
if payment.move_id:
|
||||||
|
liquidity_lines, _counterpart_lines, _writeoff_lines = payment._seek_for_lines()
|
||||||
|
return [Command.create(self._lines_prepare_new_aml_line(aml)) for aml in liquidity_lines.filtered_domain(amls_domain)]
|
||||||
|
elif payment.invoice_ids:
|
||||||
|
invoices_amls = payment.invoice_ids.line_ids.filtered(lambda line: line.account_id.account_type in payment._get_valid_payment_account_types())
|
||||||
|
payment_residual = payment.amount
|
||||||
|
comp_curr = self.company_id.currency_id
|
||||||
|
for aml in invoices_amls.sorted(lambda aml: aml.date_maturity):
|
||||||
|
if payment.currency_id.compare_amounts(payment_residual, 0) <= 0:
|
||||||
|
break
|
||||||
|
if aml.company_currency_id.is_zero(aml.amount_residual):
|
||||||
|
continue
|
||||||
|
|
||||||
|
amls_line_vals.append(Command.create(self._lines_prepare_new_aml_line(aml)))
|
||||||
|
if payment.currency_id == aml.currency_id:
|
||||||
|
payment_residual -= aml.amount_residual
|
||||||
|
elif payment.currency_id == comp_curr:
|
||||||
|
payment_residual -= aml.currency_id._convert(aml.amount_residual_currency, payment.currency_id, self.company_id, self.st_line_id.date)
|
||||||
|
else:
|
||||||
|
|
||||||
|
payment_residual -= comp_curr._convert(aml.amount_residual, payment.currency_id, self.company_id, self.st_line_id.date)
|
||||||
|
return amls_line_vals
|
||||||
|
|
||||||
|
def _get_amls_vals_from_batch(self, batch_payment):
|
||||||
|
amls_line_vals = []
|
||||||
|
for payment in batch_payment.payment_ids:
|
||||||
|
amls_line_vals += self._get_amls_vals_from_payment(payment)
|
||||||
|
return amls_line_vals
|
||||||
|
|
||||||
|
def _lines_load_new_batch_payments(self, batch_payments, reco_model=None):
|
||||||
|
""" Create counterpart lines for the batch payments passed as parameter."""
|
||||||
|
line_ids_commands = []
|
||||||
|
kwargs = {'reconcile_model_id': reco_model.id} if reco_model else {}
|
||||||
|
for batch in batch_payments:
|
||||||
|
if self._check_for_epd(batch):
|
||||||
|
line_ids_commands += self._get_amls_vals_from_batch(batch)
|
||||||
|
else:
|
||||||
|
aml_line_vals = self._lines_prepare_new_batch_line(batch, **kwargs)
|
||||||
|
line_ids_commands.append(Command.create(aml_line_vals))
|
||||||
|
|
||||||
|
if not line_ids_commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.line_ids = line_ids_commands
|
||||||
|
|
||||||
|
def _get_key_mapping_aml_and_exchange_diff(self, line):
|
||||||
|
if line.flag in ('new_batch', 'exchange_diff') and line.source_batch_payment_id:
|
||||||
|
return 'source_batch_payment_id', line.source_batch_payment_id.id
|
||||||
|
return super()._get_key_mapping_aml_and_exchange_diff(line)
|
||||||
|
|
||||||
|
def _lines_get_exchange_diff_values(self, line):
|
||||||
|
if line.flag != 'new_batch':
|
||||||
|
return super()._lines_get_exchange_diff_values(line)
|
||||||
|
exchange_diff_values = []
|
||||||
|
currency_x_exchange = {}
|
||||||
|
for currency, balance, amount_currency in [
|
||||||
|
(aml.currency_id, -aml.amount_residual, -aml.amount_residual_currency)
|
||||||
|
for aml in self._get_amls_from_batch_payments(line.source_batch_payment_id)
|
||||||
|
] + [
|
||||||
|
(payment.currency_id, -payment.amount_company_currency_signed, -payment.amount_signed)
|
||||||
|
for payment in line.source_batch_payment_id.payment_ids.filtered(lambda p: not p.move_id)
|
||||||
|
]:
|
||||||
|
account, exchange_diff_balance = self._lines_get_account_balance_exchange_diff(
|
||||||
|
currency,
|
||||||
|
balance,
|
||||||
|
amount_currency,
|
||||||
|
)
|
||||||
|
if exchange_diff_balance != 0.0:
|
||||||
|
currency_exch_amounts = currency_x_exchange.get((currency, account), {
|
||||||
|
'amount_currency': 0.0,
|
||||||
|
'balance': 0.0,
|
||||||
|
})
|
||||||
|
currency_exch_amounts['amount_currency'] += exchange_diff_balance if currency == self.company_currency_id else 0.0
|
||||||
|
currency_exch_amounts['balance'] += exchange_diff_balance
|
||||||
|
currency_x_exchange[currency, account] = currency_exch_amounts
|
||||||
|
|
||||||
|
for (currency, account), exch_amounts in currency_x_exchange.items():
|
||||||
|
if not currency.is_zero(exch_amounts['balance']):
|
||||||
|
exchange_diff_values.append({
|
||||||
|
'flag': 'exchange_diff',
|
||||||
|
'source_batch_payment_id': line.source_batch_payment_id.id,
|
||||||
|
'name': _("Exchange Difference: %(batch_name)s - %(currency)s", batch_name=line.source_batch_payment_id.name, currency=currency.name),
|
||||||
|
'account_id': account.id,
|
||||||
|
'currency_id': currency.id,
|
||||||
|
'amount_currency': exch_amounts['amount_currency'],
|
||||||
|
'balance': exch_amounts['balance'],
|
||||||
|
})
|
||||||
|
return exchange_diff_values
|
||||||
|
|
||||||
|
def _validation_lines_vals(self, line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile):
|
||||||
|
source2exchange = self.line_ids.filtered(lambda l: l.flag == 'exchange_diff').grouped('source_batch_payment_id')
|
||||||
|
|
||||||
|
batch_lines = self.line_ids.filtered(lambda x: x.flag == 'new_batch')
|
||||||
|
valid_payment_states = batch_lines.source_batch_payment_id._valid_payment_states()
|
||||||
|
for line in batch_lines:
|
||||||
|
for payment in line.source_batch_payment_id.payment_ids.filtered(lambda p: p.state in valid_payment_states):
|
||||||
|
account2amount = defaultdict(float)
|
||||||
|
account2lines = defaultdict(list)
|
||||||
|
term_lines = iter(payment.invoice_ids.line_ids.filtered(lambda l: l.display_type == 'payment_term' and not l.reconciled).sorted('date'))
|
||||||
|
remaining = payment.amount_signed
|
||||||
|
select_amount_func = min if payment.payment_type == 'inbound' else max
|
||||||
|
while remaining and (term_line := next(term_lines, None)):
|
||||||
|
current = select_amount_func(remaining, term_line.currency_id._convert(
|
||||||
|
from_amount=term_line.amount_currency,
|
||||||
|
to_currency=payment.currency_id,
|
||||||
|
))
|
||||||
|
remaining -= current
|
||||||
|
account2amount[term_line.account_id] -= current
|
||||||
|
account2lines[term_line.account_id].append(term_line.id)
|
||||||
|
if remaining:
|
||||||
|
partner_account = (
|
||||||
|
payment.partner_id.property_account_payable_id
|
||||||
|
if payment.payment_type == "outbound"
|
||||||
|
else payment.partner_id.property_account_receivable_id
|
||||||
|
)
|
||||||
|
account2amount[partner_account] -= remaining
|
||||||
|
for account, amount in account2amount.items():
|
||||||
|
line_ids_create_command_list.append(Command.create(line._get_aml_values(
|
||||||
|
sequence=len(line_ids_create_command_list) + 1,
|
||||||
|
partner_id=payment.partner_id.id,
|
||||||
|
account_id=account.id,
|
||||||
|
currency_id=payment.currency_id.id,
|
||||||
|
amount_currency=amount,
|
||||||
|
balance=payment.currency_id._convert(from_amount=amount, to_currency=self.env.company.currency_id, date=payment.date),
|
||||||
|
)))
|
||||||
|
if lines := self.env['account.move.line'].browse(account2lines[account]):
|
||||||
|
to_reconcile.append((len(line_ids_create_command_list), lines))
|
||||||
|
exchange_diff_vals = source2exchange.get(line.source_batch_payment_id, [])
|
||||||
|
for exchange_diff in exchange_diff_vals:
|
||||||
|
aml_to_exchange_diff_vals[len(line_ids_create_command_list) + 1] = {
|
||||||
|
'amount_residual': exchange_diff.balance,
|
||||||
|
'amount_residual_currency': exchange_diff.amount_currency,
|
||||||
|
'analytic_distribution': exchange_diff.analytic_distribution,
|
||||||
|
}
|
||||||
|
line_ids_create_command_list.append(Command.create(exchange_diff._get_aml_values(
|
||||||
|
sequence=len(line_ids_create_command_list) + 1,
|
||||||
|
)))
|
||||||
|
|
||||||
|
batch_lines.source_batch_payment_id.payment_ids.filtered(lambda p: not p.move_id and p.state in valid_payment_states).action_validate()
|
||||||
|
self.line_ids -= batch_lines
|
||||||
|
super()._validation_lines_vals(line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile)
|
||||||
|
|
||||||
|
def _check_for_epd(self, batch_payment):
|
||||||
|
|
||||||
|
valid_payment_states = batch_payment._valid_payment_states()
|
||||||
|
no_move_payments = batch_payment.payment_ids.filtered(lambda payment: not payment.move_id)
|
||||||
|
if no_move_payments.invoice_ids.currency_id == self.transaction_currency_id:
|
||||||
|
for payment in no_move_payments:
|
||||||
|
if (
|
||||||
|
len(payment.invoice_ids) == 1
|
||||||
|
and payment.state in valid_payment_states
|
||||||
|
and payment.invoice_ids._is_eligible_for_early_payment_discount(self.transaction_currency_id, self.st_line_id.date)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _process_restore_lines_ids(self, initial_commands):
|
||||||
|
commands = []
|
||||||
|
for command in super()._process_restore_lines_ids(initial_commands):
|
||||||
|
match command:
|
||||||
|
case (Command.CREATE, _, values) if values.get('flag') == 'new_batch':
|
||||||
|
batch = self.env['account.batch.payment'].browse(values['source_batch_payment_id'])
|
||||||
|
commands.append(Command.create(self._lines_prepare_new_batch_line(batch)))
|
||||||
|
case _:
|
||||||
|
commands.append(command)
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def _action_validate(self):
|
||||||
|
self.ensure_one()
|
||||||
|
batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
|
||||||
|
batches_to_expand = batches.filtered('payment_ids.move_id')
|
||||||
|
self._action_expand_batch_payments(batches_to_expand)
|
||||||
|
super()._action_validate()
|
||||||
|
|
||||||
|
def _action_add_new_batched_amls(self, batch_payments, reco_model=None, allow_partial=True):
|
||||||
|
self.ensure_one()
|
||||||
|
existing_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
|
||||||
|
batch_payments = batch_payments - existing_batches
|
||||||
|
if not batch_payments:
|
||||||
|
return
|
||||||
|
existing_batch_new_amls = self.line_ids.filtered(lambda x: x.flag == 'new_aml' and x.source_batch_payment_id in batch_payments)
|
||||||
|
self._action_remove_lines(existing_batch_new_amls)
|
||||||
|
self._lines_load_new_batch_payments(batch_payments, reco_model=reco_model)
|
||||||
|
added_lines = self.line_ids.filtered(lambda x: x.flag in ('new_batch', 'new_aml') and x.source_batch_payment_id in batch_payments)
|
||||||
|
self._lines_recompute_exchange_diff(added_lines)
|
||||||
|
if not self._lines_check_apply_early_payment_discount():
|
||||||
|
self._lines_check_apply_partial_matching()
|
||||||
|
self._lines_add_auto_balance_line()
|
||||||
|
self._action_clear_manual_operations_form()
|
||||||
|
|
||||||
|
def _action_add_new_batch_payments(self, batch_payments):
|
||||||
|
self.ensure_one()
|
||||||
|
mounted_batches = self.line_ids.filtered(lambda x: x.flag == 'new_batch').source_batch_payment_id
|
||||||
|
self._action_add_new_batched_amls(batch_payments - mounted_batches, allow_partial=False)
|
||||||
|
|
||||||
|
def _js_action_add_new_batch_payment(self, batch_payment_id):
|
||||||
|
self.ensure_one()
|
||||||
|
batch_payment = self.env['account.batch.payment'].browse(batch_payment_id)
|
||||||
|
self._action_add_new_batch_payments(batch_payment)
|
||||||
|
|
||||||
|
def _action_remove_new_batch_payments(self, batch_payments):
|
||||||
|
self.ensure_one()
|
||||||
|
lines = self.line_ids.filtered(lambda x: x.flag in ('new_aml', 'new_batch') and x.source_batch_payment_id in batch_payments)
|
||||||
|
self._action_remove_lines(lines)
|
||||||
|
|
||||||
|
def _js_action_remove_new_batch_payment(self, batch_payment_id):
|
||||||
|
self.ensure_one()
|
||||||
|
batch_payment = self.env['account.batch.payment'].browse(batch_payment_id)
|
||||||
|
self._action_remove_new_batch_payments(batch_payment)
|
||||||
|
|
||||||
|
def _action_remove_lines(self, lines):
|
||||||
|
self.ensure_one()
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
has_new_batch = any(line.flag == 'new_batch' for line in lines)
|
||||||
|
has_new_aml = any(line.flag == 'new_aml' for line in lines)
|
||||||
|
super()._action_remove_lines(lines)
|
||||||
|
if has_new_batch and not has_new_aml:
|
||||||
|
self._lines_check_apply_partial_matching()
|
||||||
|
self._lines_add_auto_balance_line()
|
||||||
|
|
||||||
|
def _action_expand_batch_payments(self, batch_payments):
|
||||||
|
self.ensure_one()
|
||||||
|
if not batch_payments:
|
||||||
|
return
|
||||||
|
batch_lines = self.line_ids.filtered(lambda x: x.flag == 'new_batch' and x.source_batch_payment_id in batch_payments)
|
||||||
|
if not batch_lines:
|
||||||
|
return
|
||||||
|
batch_unlink_commands = []
|
||||||
|
for batch_line in batch_lines:
|
||||||
|
batch_unlink_commands.append(Command.unlink(batch_line.id))
|
||||||
|
self.line_ids = batch_unlink_commands
|
||||||
|
self._remove_related_exchange_diff_lines(batch_lines)
|
||||||
|
self._action_add_new_amls(self._get_amls_from_batch_payments(batch_payments), allow_partial=False)
|
||||||
|
|
||||||
|
def _js_action_redirect_to_move(self, form_index):
|
||||||
|
self.ensure_one()
|
||||||
|
line = self.line_ids.filtered(lambda x: x.index == form_index)
|
||||||
|
if line.source_batch_payment_id:
|
||||||
|
self.return_todo_command = clean_action(line.source_batch_payment_id._get_records_action(), self.env)
|
||||||
|
else:
|
||||||
|
return super()._js_action_redirect_to_move(form_index)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class BankRecWidgetLine(models.Model):
|
||||||
|
_inherit = 'bank.rec.widget.line'
|
||||||
|
|
||||||
|
source_batch_payment_id = fields.Many2one(comodel_name='account.batch.payment')
|
||||||
|
flag = fields.Selection(selection_add=[('new_batch', 'new_batch')])
|
||||||
|
source_batch_payment_name = fields.Char()
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_account_batch_payment_rejection,account.batch.payment.rejection,model_account_batch_payment_rejection,account.group_account_user,1,1,1,0
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordNotebookBatchPayments">
|
||||||
|
<div class="bank_rec_widget_form_batch_payments_list_anchor" t-if="this.state.bankRecEmbeddedViewsData">
|
||||||
|
<BankRecViewEmbedder viewProps="this.notebookBatchPaymentsListViewProps()" t-key="data.st_line_id[0]"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordForm"
|
||||||
|
t-inherit="account_accountant.BankRecRecordForm"
|
||||||
|
t-inherit-mode="extension"
|
||||||
|
>
|
||||||
|
<xpath expr="//t[@t-set-slot='amls_tab']" position="after">
|
||||||
|
<t t-set-slot="batch_payments_tab"
|
||||||
|
name="'batch_payments_tab'"
|
||||||
|
title.translate="Batch Payments"
|
||||||
|
isVisible="['valid', 'invalid'].includes(data.state)">
|
||||||
|
<t t-call="odex30_account_accountant_batch_payment.BankRecRecordNotebookBatchPayments"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="odex30_account_accountant_batch_payment.BankRecRecordFormLineIds"
|
||||||
|
t-inherit="account_accountant.BankRecRecordFormLineIds"
|
||||||
|
t-inherit-mode="extension"
|
||||||
|
>
|
||||||
|
<xpath expr="//td[@field='account_id']" position="replace">
|
||||||
|
<t t-if="line.data.flag === 'new_batch'">
|
||||||
|
<td field="source_batch_payment_name">
|
||||||
|
<span t-out="line.data.source_batch_payment_name"/>
|
||||||
|
</td>
|
||||||
|
</t>
|
||||||
|
<t t-else="">$0</t>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//td[@field='name']" position="replace">
|
||||||
|
<t t-if="line.data.flag === 'new_batch'">
|
||||||
|
<td field="name" t-att-colspan="line_ids_columns.length - (display_currency_columns ? 5 : 3) + (hasGroupReadOnly ? 0 : 1)">
|
||||||
|
<span class="o_form_uri fst-italic"
|
||||||
|
t-out="line.data.name"
|
||||||
|
t-on-click="() => this.actionRedirectToSourceMove(line)"/>
|
||||||
|
</td>
|
||||||
|
</t>
|
||||||
|
<t t-else="">$0</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { EmbeddedListView } from "@odex30_account_accountant/components/bank_reconciliation/embedded_list_view";
|
||||||
|
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||||
|
import { useState, onWillUnmount } from "@odoo/owl";
|
||||||
|
|
||||||
|
export class BankRecBatchPaymentsRenderer extends ListRenderer {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
this.globalState = useState(this.env.methods.getState());
|
||||||
|
|
||||||
|
onWillUnmount(this.saveSearchState);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowClass(record) {
|
||||||
|
const classes = super.getRowClass(record);
|
||||||
|
const batchId = this.globalState.bankRecRecordData.selected_batch_payment_ids.currentIds.find((x) => x === record.resId);
|
||||||
|
if (batchId){
|
||||||
|
return `${classes} o_rec_widget_list_selected_item table-info`;
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async onCellClicked(record, column, ev) {
|
||||||
|
const batchId = this.globalState.bankRecRecordData.selected_batch_payment_ids.currentIds.find((x) => x === record.resId);
|
||||||
|
if (batchId) {
|
||||||
|
this.env.config.actionRemoveNewBatchPayment(record.resId);
|
||||||
|
} else {
|
||||||
|
this.env.config.actionAddNewBatchPayment(record.resId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearchState() {
|
||||||
|
const initParams = this.globalState.bankRecEmbeddedViewsData.batch_payments;
|
||||||
|
const searchModel = this.env.searchModel;
|
||||||
|
initParams.exportState = {searchModel: JSON.stringify(searchModel.exportState())};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BankRecBatchPayments = {
|
||||||
|
...EmbeddedListView,
|
||||||
|
Renderer: BankRecBatchPaymentsRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("bank_rec_batch_payments_list_view", BankRecBatchPayments);
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
import { BankRecKanbanController } from "@odex30_account_accountant/components/bank_reconciliation/kanban";
|
||||||
|
|
||||||
|
patch(BankRecKanbanController.prototype, {
|
||||||
|
|
||||||
|
|
||||||
|
getChildSubEnv(){
|
||||||
|
const env = super.getChildSubEnv(...arguments);
|
||||||
|
|
||||||
|
env.methods.actionAddNewBatchPayment = this.actionAddNewBatchPayment.bind(this);
|
||||||
|
env.methods.actionRemoveNewBatchPayment = this.actionRemoveNewBatchPayment.bind(this);
|
||||||
|
|
||||||
|
return env;
|
||||||
|
},
|
||||||
|
|
||||||
|
notebookBatchPaymentsListViewProps(){
|
||||||
|
const initParams = this.state.bankRecEmbeddedViewsData.batch_payments;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "list",
|
||||||
|
noBreadcrumbs: true,
|
||||||
|
resModel: "account.batch.payment",
|
||||||
|
searchMenuTypes: ["filter"],
|
||||||
|
domain: initParams.domain,
|
||||||
|
dynamicFilters: initParams.dynamic_filters,
|
||||||
|
context: initParams.context,
|
||||||
|
allowSelectors: false,
|
||||||
|
searchViewId: false,
|
||||||
|
globalState: initParams.exportState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getBankRecLineInvalidFields(line){
|
||||||
|
if (line.data.flag === 'new_batch') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return super.getBankRecLineInvalidFields(line);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async actionAddNewBatchPayment(batchId){
|
||||||
|
await this.execProtectedBankRecAction(async () => {
|
||||||
|
await this.withNewState(async (newState) => {
|
||||||
|
await this.onchange(newState, "add_new_batch_payment", [batchId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async actionRemoveNewBatchPayment(batchId){
|
||||||
|
await this.execProtectedBankRecAction(async () => {
|
||||||
|
await this.withNewState(async (newState) => {
|
||||||
|
await this.onchange(newState, "remove_new_batch_payment", [batchId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async actionValidateOnCloseWizard(){
|
||||||
|
await this.execProtectedBankRecAction(async () => {
|
||||||
|
await this.withNewState(async (newState) => {
|
||||||
|
const { return_todo_command: result } = await this.onchange(newState, "validate_no_batch_payment_check");
|
||||||
|
if(result.done){
|
||||||
|
await this.moveToNextLine(newState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async _actionValidate(newState){
|
||||||
|
const result = await super._actionValidate(...arguments);
|
||||||
|
|
||||||
|
if(!result){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.open_batch_rejection_wizard){
|
||||||
|
const validateFunc = this.actionValidateOnCloseWizard.bind(this);
|
||||||
|
this.action.doAction(
|
||||||
|
result.open_batch_rejection_wizard,
|
||||||
|
{
|
||||||
|
onClose: async (nextAction) => {
|
||||||
|
if(nextAction === "validate"){
|
||||||
|
await validateFunc();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { stepUtils } from "@web_tour/tour_service/tour_utils";
|
||||||
|
import { accountTourSteps } from "@account/js/tours/account";
|
||||||
|
|
||||||
|
registry.category("web_tour.tours").add("account_accountant_batch_payment_bank_rec_widget", {
|
||||||
|
url: "/odoo",
|
||||||
|
steps: () => [
|
||||||
|
stepUtils.showAppsMenuItem(),
|
||||||
|
...accountTourSteps.goToAccountMenu("Open the accounting module"),
|
||||||
|
|
||||||
|
{
|
||||||
|
trigger: ".o_breadcrumb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Open the bank reconciliation widget",
|
||||||
|
trigger: "button.btn-secondary[name='action_open_reconcile']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "The 'line1' should be selected by default",
|
||||||
|
trigger: "div[name='line_ids'] td[field='name']:contains('line1')",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
content: "Click on the 'batch_payments_tab'",
|
||||||
|
trigger: "a[name='batch_payments_tab']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Mount BATCH0001",
|
||||||
|
trigger:
|
||||||
|
"div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table td[name='name']:contains('BATCH0001')",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "The batch should be selected",
|
||||||
|
trigger:
|
||||||
|
"div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table tr.o_rec_widget_list_selected_item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Open the batch",
|
||||||
|
trigger: "div[name='line_ids'] .o_bank_rec_second_line .o_form_uri",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Open the payment of 100.0",
|
||||||
|
trigger: "div[name='payment_ids'] tbody tr.o_data_row:last .o_list_record_open_form_view button",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Reject it",
|
||||||
|
trigger: "button[name='action_reject']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Go back to the reconciliation widget",
|
||||||
|
trigger: "a[href$='/reconciliation']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: "div[name='line_ids'] td[field='name']:contains('line1')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: "button.btn-primary:contains('Validate')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Validate",
|
||||||
|
trigger: "button:contains('Validate')",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
...stepUtils.toggleHomeMenu(),
|
||||||
|
...accountTourSteps.goToAccountMenu("Reset back to accounting module"),
|
||||||
|
{
|
||||||
|
content: "check that we're back on the dashboard",
|
||||||
|
trigger: 'a:contains("Customer Invoices")',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.category("web_tour.tours").add("account_accountant_batch_payment_bank_rec_widget_batch_line_clickable", {
|
||||||
|
url: "/odoo",
|
||||||
|
steps: () => [
|
||||||
|
stepUtils.showAppsMenuItem(),
|
||||||
|
...accountTourSteps.goToAccountMenu("Open the accounting module"),
|
||||||
|
{
|
||||||
|
trigger: ".o_breadcrumb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Open the bank reconciliation widget",
|
||||||
|
trigger: "button.btn-secondary[name='action_open_reconcile']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Click on the 'batch_payments_tab'",
|
||||||
|
trigger: "a[name='batch_payments_tab']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Mount BATCH0001",
|
||||||
|
trigger: "div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table td[name='name']:contains('BATCH0001')",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "The batch should be selected",
|
||||||
|
trigger: "div.bank_rec_widget_form_batch_payments_list_anchor table.o_list_table tr.o_rec_widget_list_selected_item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Click batch row for BATCH0001",
|
||||||
|
trigger: ".o_data_row.o_selected_row.o_list_no_open.o_bank_rec_second_line:contains('BATCH0001')",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Wait for Manual Operations tab to open",
|
||||||
|
trigger: "div[name='analytic_distribution']:not(:visible)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
from . import test_batch_payment
|
||||||
|
from . import test_bank_rec_widget
|
||||||
|
from . import test_bank_rec_widget_tour
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,77 @@
|
||||||
|
from odoo import Command
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.addons.account.tests.common import AccountTestMockOnlineSyncCommon
|
||||||
|
from odoo.addons.odex30_account_accountant.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestBankRecWidgetTour(TestBankRecWidgetCommon, AccountTestMockOnlineSyncCommon):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
cls.env['account.reconcile.model']\
|
||||||
|
.search([('company_id', '=', cls.company.id)])\
|
||||||
|
.write({'past_months_limit': None})
|
||||||
|
|
||||||
|
def test_tour_bank_rec_widget(self):
|
||||||
|
self._create_st_line(500.0, payment_ref="line1", sequence=1)
|
||||||
|
self._create_st_line(100.0, payment_ref="line2", sequence=2)
|
||||||
|
self._create_st_line(100.0, payment_ref="line3", sequence=3)
|
||||||
|
self._create_st_line(1000.0, payment_ref="line_credit", sequence=4, journal_id=self.company_data['default_journal_credit'].id)
|
||||||
|
|
||||||
|
payment_method_line = self.company_data['default_journal_bank'].inbound_payment_method_line_ids\
|
||||||
|
.filtered(lambda l: l.code == 'batch_payment')
|
||||||
|
payment_method_line.payment_account_id = self.inbound_payment_method_line.payment_account_id
|
||||||
|
|
||||||
|
payments = self.env['account.payment'].create([
|
||||||
|
{
|
||||||
|
'date': '2020-01-01',
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'partner_type': 'customer',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'payment_method_line_id': payment_method_line.id,
|
||||||
|
'amount': i * 100.0,
|
||||||
|
}
|
||||||
|
for i in range(1, 4)
|
||||||
|
])
|
||||||
|
payments.action_post()
|
||||||
|
|
||||||
|
batch = self.env['account.batch.payment'].create({
|
||||||
|
'name': "BATCH0001",
|
||||||
|
'date': '2020-01-01',
|
||||||
|
'journal_id': self.company_data['default_journal_bank'].id,
|
||||||
|
'payment_ids': [Command.set(payments.ids)],
|
||||||
|
'payment_method_id': payment_method_line.payment_method_id.id,
|
||||||
|
})
|
||||||
|
batch.validate_batch()
|
||||||
|
|
||||||
|
self.start_tour('/odoo', 'account_accountant_batch_payment_bank_rec_widget', login=self.env.user.login)
|
||||||
|
|
||||||
|
def test_batch_line_clickable(self):
|
||||||
|
self._create_st_line(500.0, payment_ref="line1", sequence=1)
|
||||||
|
|
||||||
|
payments = self.env['account.payment'].create([
|
||||||
|
{
|
||||||
|
'date': '2020-01-01',
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'partner_type': 'customer',
|
||||||
|
'partner_id': self.partner_a.id,
|
||||||
|
'amount': i * 100.0,
|
||||||
|
}
|
||||||
|
for i in range(1, 3)
|
||||||
|
])
|
||||||
|
payments.action_post()
|
||||||
|
|
||||||
|
batch = self.env['account.batch.payment'].create({
|
||||||
|
'name': "BATCH0001",
|
||||||
|
'date': '2020-01-01',
|
||||||
|
'journal_id': self.company_data['default_journal_bank'].id,
|
||||||
|
'payment_ids': [Command.set(payments.ids)],
|
||||||
|
})
|
||||||
|
self.env.user.write({'groups_id': [Command.link(self.env.ref('analytic.group_analytic_accounting').id)]})
|
||||||
|
|
||||||
|
batch.validate_batch()
|
||||||
|
|
||||||
|
self.start_tour('/odoo', 'account_accountant_batch_payment_bank_rec_widget_batch_line_clickable', login=self.env.user.login)
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestBatchPayment(AccountTestInvoicingCommon):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
cls.journal = cls.company_data['default_journal_bank']
|
||||||
|
cls.batch_deposit_method = cls.env.ref('odex30_account_batch_payment.account_payment_method_batch_deposit')
|
||||||
|
cls.batch_deposit = cls.journal.inbound_payment_method_line_ids.filtered(lambda l: l.code == 'batch_payment')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createPayment(cls, partner, amount):
|
||||||
|
payment = cls.env['account.payment'].create({
|
||||||
|
'journal_id': cls.journal.id,
|
||||||
|
'payment_method_line_id': cls.batch_deposit.id,
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'date': time.strftime('%Y') + '-07-15',
|
||||||
|
'amount': amount,
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'partner_type': 'customer',
|
||||||
|
})
|
||||||
|
payment.action_post()
|
||||||
|
return payment
|
||||||
|
|
||||||
|
def test_zero_amount_payment(self):
|
||||||
|
zero_payment = self.createPayment(self.partner_a, 0)
|
||||||
|
batch_vals = {
|
||||||
|
'journal_id': self.journal.id,
|
||||||
|
'payment_ids': [(4, zero_payment.id, None)],
|
||||||
|
'payment_method_id': self.batch_deposit_method.id,
|
||||||
|
}
|
||||||
|
self.assertRaises(ValidationError, self.env['account.batch.payment'].create, batch_vals)
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_account_batch_payment_rejection_form" model="ir.ui.view">
|
||||||
|
<field name="name">account.batch.payment.rejection.form</field>
|
||||||
|
<field name="model">account.batch.payment.rejection</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Batch Payment">
|
||||||
|
<field name="in_reconcile_payment_ids" invisible="1"/>
|
||||||
|
<field name="rejected_payment_ids" invisible="1"/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span invisible="1 not in nb_batch_payment_ids"><field name="nb_rejected_payment_ids"/> payments from the batch have been removed.</span>
|
||||||
|
<span invisible="1 in nb_batch_payment_ids"><field name="nb_rejected_payment_ids"/> payments from <field name="nb_batch_payment_ids"/> batches have been removed.</span>
|
||||||
|
<br/>
|
||||||
|
<span>Do you want to cancel payments to retry them later or keep the batch open with unprocess payments, if you expect them later.</span>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button string="Cancel Payments"
|
||||||
|
name="button_cancel_payments"
|
||||||
|
type="object"
|
||||||
|
class="btn btn-primary"
|
||||||
|
close="1"
|
||||||
|
data-hotkey="q"/>
|
||||||
|
<button string="Expect Payments Later"
|
||||||
|
name="button_continue"
|
||||||
|
type="object"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
close="1"
|
||||||
|
data-hotkey="l"/>
|
||||||
|
<button string="Cancel"
|
||||||
|
name="button_cancel"
|
||||||
|
type="object"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
close="1"
|
||||||
|
data-hotkey="x"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_account_batch_payment_search_bank_rec_widget" model="ir.ui.view">
|
||||||
|
<field name="name">account.batch.payment.search.bank_rec_widget</field>
|
||||||
|
<field name="model">account.batch.payment</field>
|
||||||
|
<field name="priority">999</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"
|
||||||
|
string="Batch Payment"
|
||||||
|
filter_domain="[('name', 'ilike', self)]"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="journal_id"/>
|
||||||
|
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="amount_received" string="Received" domain="[('batch_type', '=', 'inbound')]"/>
|
||||||
|
<filter name="amount_paid" string="Paid" domain="[('batch_type', '=', 'outbound')]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="unreconciled" string="Unreconciled" domain="[('state', '!=', 'reconciled')]"/>
|
||||||
|
<separator name="inject_after"/>
|
||||||
|
<filter name="date" string="Date" date="date"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_account_batch_payment_list_bank_rec_widget" model="ir.ui.view">
|
||||||
|
<field name="name">account.batch.payment.list.bank_rec_widget</field>
|
||||||
|
<field name="model">account.batch.payment</field>
|
||||||
|
<field name="priority">999</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Suggestions"
|
||||||
|
create="false"
|
||||||
|
edit="false"
|
||||||
|
limit="40"
|
||||||
|
js_class="bank_rec_batch_payments_list_view">
|
||||||
|
<!-- Invisible fields -->
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
<field name="company_currency_id" column_invisible="True"/>
|
||||||
|
<field name="state" column_invisible="True"/>
|
||||||
|
|
||||||
|
<field name="date" readonly="state != 'draft'"/>
|
||||||
|
<field name="name" readonly="state != 'draft'"/>
|
||||||
|
<field name="journal_id"
|
||||||
|
optional="hidden" readonly="state != 'draft'"/>
|
||||||
|
<field name="amount_residual_currency"
|
||||||
|
string="Amount Due (in currency)"/>
|
||||||
|
<field name="amount_residual"
|
||||||
|
string="Amount Due"
|
||||||
|
groups="base.group_multi_currency"
|
||||||
|
optional="hidden"/>
|
||||||
|
|
||||||
|
<button name="action_open_batch_payment"
|
||||||
|
type="object"
|
||||||
|
string="View"
|
||||||
|
class="btn btn-sm btn-secondary"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'ODEX Account: Advanced Check Processing',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Account/Accounting',
|
||||||
|
'author': 'ODEX',
|
||||||
|
'summary': 'Advanced verification and reconciliation for issued checks.',
|
||||||
|
'description': """
|
||||||
|
This system provides advanced tools for managing financial check distributions.
|
||||||
|
It enables seamless verification between your internal accounting records and
|
||||||
|
issued physical checks within the financial management interface.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Seamless verification of check payments.
|
||||||
|
- Real-time reconciliation within the accounting workspace.
|
||||||
|
- Enhanced audit trails for distributed checks.
|
||||||
|
""",
|
||||||
|
'depends': ['odex30_account_accountant', 'account_check_printing'],
|
||||||
|
'data': [
|
||||||
|
'views/bank_rec_widget_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_accountant_check_printing
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_check_printing.field_account_move_line__check_number
|
||||||
|
msgid "Check Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_check_printing.model_account_move_line
|
||||||
|
msgid "Journal Item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model.fields,help:odex30_account_accountant_check_printing.field_account_move_line__check_number
|
||||||
|
msgid ""
|
||||||
|
"The selected journal is configured to print check numbers. If your pre-"
|
||||||
|
"printed check paper already has numbers or if the current numbering is "
|
||||||
|
"wrong, you can change it in the journal configuration page."
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_accountant_check_printing
|
||||||
|
#
|
||||||
|
# Translators:
|
||||||
|
# Wil ODEX, 2024
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
|
||||||
|
"Last-Translator: Wil ODEX, 2024\n"
|
||||||
|
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Language: ar\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_accountant_check_printing.field_account_move_line__check_number
|
||||||
|
msgid "Check Number"
|
||||||
|
msgstr "رقم الشيك "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model,name:odex30_account_accountant_check_printing.model_account_move_line
|
||||||
|
msgid "Journal Item"
|
||||||
|
msgstr "عنصر دفتر اليومية "
|
||||||
|
|
||||||
|
#. module: odex30_account_accountant_check_printing
|
||||||
|
#: model:ir.model.fields,help:odex30_account_accountant_check_printing.field_account_move_line__check_number
|
||||||
|
msgid ""
|
||||||
|
"The selected journal is configured to print check numbers. If your pre-"
|
||||||
|
"printed check paper already has numbers or if the current numbering is "
|
||||||
|
"wrong, you can change it in the journal configuration page."
|
||||||
|
msgstr ""
|
||||||
|
"تمت تهيئة قيد اليومية المُختار لطباعة أرقام الشيكات. إذا كان لشيكاتك "
|
||||||
|
"المطبوعة مسبقًا أرقام بالفعل أو إذا كان الترقيم الحالي خاطئًا، فيمكنك تغييره"
|
||||||
|
" في صفحة تهيئة دفتر اليومية. "
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import account_move_line
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMoveLine(models.Model):
|
||||||
|
_name = "account.move.line"
|
||||||
|
_inherit = "account.move.line"
|
||||||
|
|
||||||
|
check_number = fields.Char(
|
||||||
|
string="Check Number",
|
||||||
|
related='payment_id.check_number',
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="view_account_move_line_list_bank_rec_widget" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.line.list.bank_rec_widget</field>
|
||||||
|
<field name="model">account.move.line</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_accountant.view_account_move_line_list_bank_rec_widget"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="check_number"
|
||||||
|
optional="hidden"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_account_move_line_search_bank_rec_widget" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.line.search.bank_rec_widget</field>
|
||||||
|
<field name="model">account.move.line</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_accountant.view_account_move_line_search_bank_rec_widget"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="check_number"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
<t t-name="odex30_account_accountant_fleet.BankRecRecordFormLineIds" t-inherit="account_accountant.BankRecRecordFormLineIds" t-inherit-mode="extension">
|
<t t-name="odex30_account_accountant_fleet.BankRecRecordFormLineIds" t-inherit="odex30_account_accountant.BankRecRecordFormLineIds" t-inherit-mode="extension">
|
||||||
<xpath expr="//t[@name='col_taxes']" position="after">
|
<xpath expr="//t[@name='col_taxes']" position="after">
|
||||||
<t t-if="column[0] === 'vehicle'" name="col_vehicle">
|
<t t-if="column[0] === 'vehicle'" name="col_vehicle">
|
||||||
<td class="o_data_cell o_field_cell o_field_widget o_list_many2one"
|
<td class="o_data_cell o_field_cell o_field_widget o_list_many2one"
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<t t-name="odex30_account_accountant_fleet.BankRecRecordNotebookManualOperations" t-inherit="account_accountant.BankRecRecordNotebookManualOperations" t-inherit-mode="extension">
|
<t t-name="odex30_account_accountant_fleet.BankRecRecordNotebookManualOperations" t-inherit="odex30_account_accountant.BankRecRecordNotebookManualOperations" t-inherit-mode="extension">
|
||||||
<xpath expr="//div[@name='suggestion']" position="before">
|
<xpath expr="//div[@name='suggestion']" position="before">
|
||||||
<div name="vehicle"
|
<div name="vehicle"
|
||||||
t-if="!['liquidity', 'new_batch'].includes(line.data.flag)"
|
t-if="!['liquidity', 'new_batch'].includes(line.data.flag)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'ODEX Account: Asset-Fleet Integration',
|
||||||
|
'category': 'Odex30-Accounting/Odex30-Accounting',
|
||||||
|
'author': "Expert Co. Ltd.",
|
||||||
|
'website': "http://www.exp-sa.com",
|
||||||
|
'summary': 'Advanced integration between fixed assets and vehicle fleet management.',
|
||||||
|
'description': """
|
||||||
|
This module provides a robust bridge between your fixed asset records and
|
||||||
|
the company's vehicle fleet management system.
|
||||||
|
|
||||||
|
It allows for:
|
||||||
|
- Direct linkage of assets to specific fleet vehicles.
|
||||||
|
- Unified tracking of depreciation and maintenance costs for transport assets.
|
||||||
|
- Integrated reporting across financial and operational fleet data.
|
||||||
|
""",
|
||||||
|
'version': '1.0',
|
||||||
|
'depends': ['account_fleet', 'odex30_account_asset'],
|
||||||
|
'data': [
|
||||||
|
'views/account_asset_views.xml',
|
||||||
|
'views/account_move_views.xml',
|
||||||
|
],
|
||||||
|
'auto_install': True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_asset_fleet
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_asset_fleet/models/account_asset.py:0
|
||||||
|
msgid "All the lines should be from the same vehicle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_account_asset
|
||||||
|
msgid "Asset/Revenue Recognition"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_fleet_vehicle_log_services
|
||||||
|
msgid "Services for vehicles"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_asset_fleet.field_account_asset__vehicle_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_asset_fleet.view_odex30_account_asset_fleet_form
|
||||||
|
msgid "Vehicle"
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Translation of ODEX Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_asset_fleet
|
||||||
|
#
|
||||||
|
# Translators:
|
||||||
|
# Wil ODEX, 2024
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ODEX Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:43+0000\n"
|
||||||
|
"Last-Translator: Wil ODEX, 2024\n"
|
||||||
|
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Language: ar\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_asset_fleet/models/account_asset.py:0
|
||||||
|
msgid "All the lines should be from the same vehicle"
|
||||||
|
msgstr "يجب أن تكون كافة البنود من نفس المركبة "
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_account_asset
|
||||||
|
msgid "Asset/Revenue Recognition"
|
||||||
|
msgstr "إثبات الأصل/الإيرادات "
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "قيد اليومية"
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model,name:odex30_account_asset_fleet.model_fleet_vehicle_log_services
|
||||||
|
msgid "Services for vehicles"
|
||||||
|
msgstr "خدمات المركبات "
|
||||||
|
|
||||||
|
#. module: odex30_account_asset_fleet
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_asset_fleet.field_account_asset__vehicle_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_asset_fleet.view_odex30_account_asset_fleet_form
|
||||||
|
msgid "Vehicle"
|
||||||
|
msgstr "المركبة"
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import account_asset
|
||||||
|
from . import account_move
|
||||||
|
from . import fleet_vehicle_log_services
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAsset(models.Model):
|
||||||
|
_inherit = 'account.asset'
|
||||||
|
|
||||||
|
vehicle_id = fields.Many2one('fleet.vehicle', compute='_compute_vehicle_id', readonly=False, store=True)
|
||||||
|
|
||||||
|
@api.depends('original_move_line_ids')
|
||||||
|
def _compute_vehicle_id(self):
|
||||||
|
for record in self:
|
||||||
|
if len(record.original_move_line_ids.vehicle_id) > 1:
|
||||||
|
raise UserError(_("All the lines should be from the same vehicle"))
|
||||||
|
record.vehicle_id = record.original_move_line_ids.vehicle_id
|
||||||
|
|
||||||
|
def action_open_vehicle(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'fleet.vehicle',
|
||||||
|
'res_id': self.vehicle_id.id,
|
||||||
|
'view_ids': [(False, 'form')],
|
||||||
|
'view_mode': 'form',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = 'account.move'
|
||||||
|
|
||||||
|
def _prepare_move_for_asset_depreciation(self, vals):
|
||||||
|
# Overridden in order to link the depreciation entries with the vehicle_id
|
||||||
|
move_vals = super()._prepare_move_for_asset_depreciation(vals)
|
||||||
|
if vals['asset_id'].vehicle_id:
|
||||||
|
for _command, _id, line_vals in move_vals['line_ids']:
|
||||||
|
line_vals['vehicle_id'] = vals['asset_id'].vehicle_id.id
|
||||||
|
return move_vals
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Part of ODEX. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
class FleetVehicleLogServices(models.Model):
|
||||||
|
_inherit = 'fleet.vehicle.log.services'
|
||||||
|
|
||||||
|
@api.depends('account_move_line_id.price_subtotal',
|
||||||
|
'account_move_line_id.non_deductible_tax_value',
|
||||||
|
'account_move_line_id.account_id.multiple_assets_per_line')
|
||||||
|
def _compute_amount(self):
|
||||||
|
for log_service in self:
|
||||||
|
if not log_service.account_move_line_id:
|
||||||
|
continue
|
||||||
|
account_move_line_id = log_service.account_move_line_id
|
||||||
|
quantity = 1
|
||||||
|
if account_move_line_id.account_id.multiple_assets_per_line:
|
||||||
|
quantity = account_move_line_id.quantity
|
||||||
|
log_service.amount = account_move_line_id.currency_id.round(
|
||||||
|
(account_move_line_id.debit + account_move_line_id.non_deductible_tax_value) / quantity)
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_odex30_account_asset_fleet_form" model="ir.ui.view">
|
||||||
|
<field name="name">account.asset.fleet.form</field>
|
||||||
|
<field name="model">account.asset</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_asset.view_account_asset_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//sheet/div[@name='button_box']" position="inside">
|
||||||
|
<field name='vehicle_id' invisible="1"/>
|
||||||
|
<button class="oe_stat_button" string="Vehicle" name="action_open_vehicle" type="object" icon="fa-car" invisible="not vehicle_id"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet/notebook/page[@name='related_items']//field[@name='account_id']" position="after">
|
||||||
|
<field name='vehicle_id' optional='hidden'/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='already_depreciated_amount_import']" position="after">
|
||||||
|
<field name='vehicle_id'/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_account_move_fleet_form" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.fleet.form</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account_fleet.view_move_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='line_ids']//field[@name='vehicle_id']" position="attributes">
|
||||||
|
<attribute name="column_invisible">parent.move_type not in ('entry', 'in_invoice', 'in_refund')</attribute>
|
||||||
|
<attribute name="required">need_vehicle and parent.move_type in ('in_invoice', 'in_refund')</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
||||||
|
|
||||||
|
def _post_init_hook(env):
|
||||||
|
if companies := env['res.company'].search([('chart_template', '=', 'generic_coa')], order="parent_path"):
|
||||||
|
avatax_fiscal_position = env['account.chart.template']._get_us_avatax_fiscal_position()
|
||||||
|
for company in companies:
|
||||||
|
Template = env['account.chart.template'].with_company(company)
|
||||||
|
Template._load_data({'account.fiscal.position': avatax_fiscal_position})
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
'name': 'Avatax',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Accounting/Accounting',
|
||||||
|
'website': 'http://exp-sa.com',
|
||||||
|
'author': 'Expert Co. Ltd.',
|
||||||
|
'countries': ['us', 'ca'],
|
||||||
|
'depends': ['payment', 'odex30_account_external_tax'],
|
||||||
|
'auto_install': ['payment'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/product.avatax.category.csv',
|
||||||
|
'data/fiscal_position.xml',
|
||||||
|
'views/account_fiscal_position_views.xml',
|
||||||
|
'views/account_move_views.xml',
|
||||||
|
'views/avatax_category_views.xml',
|
||||||
|
'views/avatax_exemption_views.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'views/product_views.xml',
|
||||||
|
'wizard/avatax_validate_address_views.xml',
|
||||||
|
'wizard/avatax_connection_test_result_views.xml',
|
||||||
|
'reports/account_invoice.xml',
|
||||||
|
],
|
||||||
|
'license': 'OEEL-1',
|
||||||
|
'post_init_hook': '_post_init_hook',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
|
||||||
|
<record id="account_fiscal_position_avatax_us" model="account.fiscal.position">
|
||||||
|
<field name="name">Automatic Tax Mapping (AvaTax)</field>
|
||||||
|
<field name="is_avatax" eval="True"/>
|
||||||
|
<field name="auto_apply" eval="False"/>
|
||||||
|
<field name="country_id" ref="base.us"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- disable AvaTax
|
||||||
|
UPDATE res_company
|
||||||
|
SET avalara_environment = 'sandbox';
|
||||||
|
UPDATE account_fiscal_position
|
||||||
|
SET is_avatax = false;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,724 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * odex30_account_avatax
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-01-15 21:10+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-01-15 21:10+0000\n"
|
||||||
|
"Last-Translator: Odoo ERP Developer\n"
|
||||||
|
"Language-Team: Arabic (Saudi Arabia)\n"
|
||||||
|
"Language: ar_SA\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid "- %(partner_name)s (ID: %(partner_id)s) on %(record_list)s"
|
||||||
|
msgstr "- %(partner_name)s (المعرف: %(partner_id)s) على %(record_list)s"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i class=\"oi oi-fw oi-arrow-right\"/>\n"
|
||||||
|
" How to Get Credentials"
|
||||||
|
msgstr ""
|
||||||
|
"<i class=\"oi oi-fw oi-arrow-right\"/>\n"
|
||||||
|
" كيفية الحصول على بيانات الاعتماد"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i title=\"Go to Avatax portal\" role=\"img\" aria-label=\"Go to Avatax portal\" class=\"fa fa-external-link-square fa-fw\"/>\n"
|
||||||
|
" Avatax portal"
|
||||||
|
msgstr ""
|
||||||
|
"<i title=\"الانتقال إلى بوابة Avatax\" role=\"img\" aria-label=\"الانتقال إلى بوابة Avatax\" class=\"fa fa-external-link-square fa-fw\"/>\n"
|
||||||
|
" بوابة Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i title=\"Show logs\" role=\"img\" aria-label=\"Show logs\" class=\"fa fa-file-text-o\"/>\n"
|
||||||
|
" Show logs"
|
||||||
|
msgstr ""
|
||||||
|
"<i title=\"عرض السجلات\" role=\"img\" aria-label=\"عرض السجلات\" class=\"fa fa-file-text-o\"/>\n"
|
||||||
|
" عرض السجلات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i title=\"Start logging for 30 minutes\" role=\"img\" aria-label=\"Start logging for 30 minutes\" class=\"fa fa-file-text-o\"/>\n"
|
||||||
|
" Start logging for 30 minutes"
|
||||||
|
msgstr ""
|
||||||
|
"<i title=\"بدء التسجيل لمدة 30 دقيقة\" role=\"img\" aria-label=\"بدء التسجيل لمدة 30 دقيقة\" class=\"fa fa-file-text-o\"/>\n"
|
||||||
|
" بدء التسجيل لمدة 30 دقيقة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i title=\"Sync Parameters\" role=\"img\" aria-label=\"Sync Parameters\" class=\"fa fa-refresh\"/>\n"
|
||||||
|
" Sync Parameters"
|
||||||
|
msgstr ""
|
||||||
|
"<i title=\"مزامنة المعلمات\" role=\"img\" aria-label=\"مزامنة المعلمات\" class=\"fa fa-refresh\"/>\n"
|
||||||
|
" مزامنة المعلمات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid ""
|
||||||
|
"<i title=\"Test connection\" role=\"img\" aria-label=\"Test connection\" class=\"fa fa-plug fa-fw\"/>\n"
|
||||||
|
" Test connection"
|
||||||
|
msgstr ""
|
||||||
|
"<i title=\"اختبار الاتصال\" role=\"img\" aria-label=\"اختبار الاتصال\" class=\"fa fa-plug fa-fw\"/>\n"
|
||||||
|
" اختبار الاتصال"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "API ID"
|
||||||
|
msgstr "معرّف واجهة البرمجة (API ID)"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "API KEY"
|
||||||
|
msgstr "مفتاح واجهة البرمجة (API Key)"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_account_chart_template
|
||||||
|
msgid "Account Chart Template"
|
||||||
|
msgstr "قالب شجرة الحسابات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_fiscal_position__avatax_invoice_account_id
|
||||||
|
msgid "Account that will be used by Avatax taxes for invoices."
|
||||||
|
msgstr "الحساب الذي سيتم استخدامه من قبل ضرائب Avatax للفواتير."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_fiscal_position__avatax_refund_account_id
|
||||||
|
msgid "Account that will be used by Avatax taxes for refunds."
|
||||||
|
msgstr "الحساب الذي سيتم استخدامه من قبل ضرائب Avatax للمردودات."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Address Validation"
|
||||||
|
msgstr "التحقق من العنوان"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/wizard/avatax_validate_address.py:0
|
||||||
|
msgid "Address validation is only supported for North American addresses."
|
||||||
|
msgstr "التحقق من العنوان مدعوم فقط للعناوين في أمريكا الشمالية."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/res_company.py:0
|
||||||
|
msgid "Authentication failed."
|
||||||
|
msgstr "فشل في المصادقة."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/res_company.py:0
|
||||||
|
msgid "Authentication success."
|
||||||
|
msgstr "تمت المصادقة بنجاح."
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:account.fiscal.position,name:odex30_account_avatax.account_fiscal_position_avatax_us
|
||||||
|
msgid "Automatic Tax Mapping (AvaTax)"
|
||||||
|
msgstr "تعيين الضرائب تلقائياً (AvaTax)"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Automatically compute tax rates in the US and Canada."
|
||||||
|
msgstr "حساب معدلات الضرائب تلقائياً في الولايات المتحدة وكندا."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "AvaTax"
|
||||||
|
msgstr "AvaTax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_api_id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_api_id
|
||||||
|
msgid "Avalara API ID"
|
||||||
|
msgstr "معرّف واجهة Avalara API"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_api_key
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_api_key
|
||||||
|
msgid "Avalara API KEY"
|
||||||
|
msgstr "مفتاح واجهة Avalara API"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_address_validation
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_address_validation
|
||||||
|
msgid "Avalara Address Validation"
|
||||||
|
msgstr "التحقق من عنوان Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_avatax_unique_code__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avatax_unique_code
|
||||||
|
msgid "Avalara Code"
|
||||||
|
msgstr "رمز Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_partner_code
|
||||||
|
msgid "Avalara Company Code"
|
||||||
|
msgstr "رمز الشركة في Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_environment
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_environment
|
||||||
|
msgid "Avalara Environment"
|
||||||
|
msgstr "بيئة Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_exemption_id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_exemption_id
|
||||||
|
msgid "Avalara Exemption"
|
||||||
|
msgstr "إعفاء Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.actions.act_window,name:odex30_account_avatax.ir_logging_avalara_action
|
||||||
|
msgid "Avalara Logging"
|
||||||
|
msgstr "سجلات Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_partner_code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_partner_code
|
||||||
|
msgid "Avalara Partner Code"
|
||||||
|
msgstr "رمز الشريك في Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_partner__avalara_show_address_validation
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_users__avalara_show_address_validation
|
||||||
|
msgid "Avalara Show Address Validation"
|
||||||
|
msgstr "إظهار التحقق من عنوان Avalara"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.account_fiscal_position_form_inherit
|
||||||
|
msgid "Avatax"
|
||||||
|
msgstr "Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_category__avatax_category_id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_product__avatax_category_id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_template__avatax_category_id
|
||||||
|
msgid "Avatax Category"
|
||||||
|
msgstr "فئة Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__avatax_tax_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__avatax_tax_date
|
||||||
|
msgid "Avatax Date"
|
||||||
|
msgstr "تاريخ Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__avatax_invoice_account_id
|
||||||
|
msgid "Avatax Invoice Account"
|
||||||
|
msgstr "حساب فاتورة Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_avatax_exemption
|
||||||
|
msgid "Avatax Partner Exemption Codes"
|
||||||
|
msgstr "رموز إعفاء شركاء Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_product_avatax_category
|
||||||
|
msgid "Avatax Product Category"
|
||||||
|
msgstr "فئة منتجات Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__avatax_refund_account_id
|
||||||
|
msgid "Avatax Refund Account"
|
||||||
|
msgstr "حساب مردودات Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_bank_statement_line__avatax_tax_date
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_move__avatax_tax_date
|
||||||
|
msgid ""
|
||||||
|
"Avatax will use this date to calculate the tax on this invoice. If not "
|
||||||
|
"specified it will use the Invoice Date."
|
||||||
|
msgstr ""
|
||||||
|
"سيستخدم Avatax هذا التاريخ لحساب الضريبة على هذه الفاتورة. إذا لم يتم "
|
||||||
|
"تحديده، سيستخدم تاريخ الفاتورة."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "إلغاء"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__city
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "City"
|
||||||
|
msgstr "المدينة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_connection_test_result_view_form
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "إغلاق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__code
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__code
|
||||||
|
msgid "Code"
|
||||||
|
msgstr "الرمز"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Commit Transactions"
|
||||||
|
msgstr "تأكيد المعاملات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_commit
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_commit
|
||||||
|
msgid "Commit in Avatax"
|
||||||
|
msgstr "تأكيد في Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_res_company
|
||||||
|
msgid "Companies"
|
||||||
|
msgstr "الشركات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__company_id
|
||||||
|
msgid "Company"
|
||||||
|
msgstr "الشركة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Company Code"
|
||||||
|
msgstr "رمز الشركة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_res_config_settings
|
||||||
|
msgid "Config Settings"
|
||||||
|
msgstr "إعدادات التكوين"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_res_partner
|
||||||
|
msgid "Contact"
|
||||||
|
msgstr "الاتصال"
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__country_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Country"
|
||||||
|
msgstr "البلد"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr "أُنشئ بواسطة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__create_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__create_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__create_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr "تاريخ الإنشاء"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_partner__avalara_partner_code
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_users__avalara_partner_code
|
||||||
|
msgid "Customer Code set in Avalara for this partner."
|
||||||
|
msgstr "رمز العميل المُعيَّن في Avalara لهذا الشريك."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__description
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__description
|
||||||
|
msgid "Description"
|
||||||
|
msgstr "الوصف"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__display_name
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__display_name
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__display_name
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "اسم العرض"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid ""
|
||||||
|
"EXP could not change the state of the transaction related to %(document)s in AvaTax\n"
|
||||||
|
"Please check the status of `%(technical)s` in the AvaTax portal."
|
||||||
|
msgstr ""
|
||||||
|
"لم يتمكن EXP من تغيير حالة المعاملة المتعلقة بـ %(document)s في AvaTax\n"
|
||||||
|
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid ""
|
||||||
|
"EXP could not fetch the taxes related to %(document)s.\n"
|
||||||
|
"Please check the status of `%(technical)s` in the AvaTax portal."
|
||||||
|
msgstr ""
|
||||||
|
"لم يتمكن EXP من جلب الضرائب المتعلقة بـ %(document)s.\n"
|
||||||
|
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid ""
|
||||||
|
"EXP could not void the transaction related to %(document)s in AvaTax\n"
|
||||||
|
"Please check the status of `%(technical)s` in the AvaTax portal."
|
||||||
|
msgstr ""
|
||||||
|
"لم يتمكن EXP من إلغاء المعاملة المتعلقة بـ %(document)s في AvaTax\n"
|
||||||
|
"يرجى التحقق من حالة `%(technical)s` في بوابة AvaTax."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Environment"
|
||||||
|
msgstr "البيئة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/wizard/avatax_validate_address.py:0
|
||||||
|
msgid "Exp could not validate the address of %(partner)s with Avalara."
|
||||||
|
msgstr "لم يتمكن Exp من التحقق من عنوان %(partner)s مع Avalara."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_account_fiscal_position
|
||||||
|
msgid "Fiscal Position"
|
||||||
|
msgstr "المركز الضريبي"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_latitude
|
||||||
|
msgid "Geo Latitude"
|
||||||
|
msgstr "خط العرض الجغرافي"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_longitude
|
||||||
|
msgid "Geo Longitude"
|
||||||
|
msgstr "خط الطول الجغرافي"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid "Go to the configuration panel"
|
||||||
|
msgstr "الانتقال إلى لوحة الإعدادات"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__id
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "المعرف"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__is_already_valid
|
||||||
|
msgid "Is Already Valid"
|
||||||
|
msgstr "صالح مسبقاً"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_bank_statement_line__is_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_external_tax_mixin__is_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_move__is_avatax
|
||||||
|
msgid "Is Avatax"
|
||||||
|
msgstr "هو Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "قيد اليومية"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr "آخر تحديث بواسطة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__write_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__write_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__write_date
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_product_avatax_category__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr "تاريخ آخر تحديث"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Latitude"
|
||||||
|
msgstr "خط العرض"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Longitude"
|
||||||
|
msgstr "خط الطول"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_account_avatax_unique_code
|
||||||
|
msgid "Mixin to generate unique ids for Avatax"
|
||||||
|
msgstr "مزيج لتوليد معرفات فريدة لـ Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_account_external_tax_mixin
|
||||||
|
msgid "Mixin to manage common parts of external tax calculation"
|
||||||
|
msgstr "مزيج لإدارة الأجزاء المشتركة لحساب الضرائب الخارجية"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__name
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "الاسم"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/res_company.py:0
|
||||||
|
msgid "Odoo could not fetch the exemption codes of %(company)s"
|
||||||
|
msgstr "لم يتمكن Odoo من جلب رموز الإعفاء لـ %(company)s"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Original Address"
|
||||||
|
msgstr "العنوان الأصلي"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__partner_id
|
||||||
|
msgid "Partner"
|
||||||
|
msgstr "الشريك"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid "Please add your AvaTax credentials"
|
||||||
|
msgstr "يرجى إضافة بيانات اعتماد AvaTax الخاصة بك"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_product_template
|
||||||
|
msgid "Product"
|
||||||
|
msgstr "المنتج"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_product_category
|
||||||
|
msgid "Product Category"
|
||||||
|
msgstr "فئة المنتج"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_product_product
|
||||||
|
msgid "Product Variant"
|
||||||
|
msgstr "متغير المنتج"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_avatax.selection__res_company__avalara_environment__production
|
||||||
|
msgid "Production"
|
||||||
|
msgstr "الإنتاج"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields.selection,name:odex30_account_avatax.selection__res_company__avalara_environment__sandbox
|
||||||
|
msgid "Sandbox"
|
||||||
|
msgstr "بيئة الاختبار"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Save Validated"
|
||||||
|
msgstr "حفظ المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_avatax_unique_code.py:0
|
||||||
|
msgid "Search operation not supported"
|
||||||
|
msgstr "عملية البحث غير مدعومة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_connection_test_result__server_response
|
||||||
|
msgid "Server Response"
|
||||||
|
msgstr "استجابة الخادم"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__state_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "State"
|
||||||
|
msgstr "الولاية"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__street
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Street"
|
||||||
|
msgstr "الشارع"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__street2
|
||||||
|
msgid "Street2"
|
||||||
|
msgstr "الشارع 2"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_avatax_validate_address
|
||||||
|
msgid "Suggests validated addresses from Avatax"
|
||||||
|
msgstr "يقترح عناوين مُصَدَّقة من Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Synchronize the exemption codes from Avatax"
|
||||||
|
msgstr "مزامنة رموز الإعفاء من Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/res_company.py:0
|
||||||
|
msgid "Test Result"
|
||||||
|
msgstr "نتيجة الاختبار"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model,name:odex30_account_avatax.model_avatax_connection_test_result
|
||||||
|
msgid "Test connection with avatax"
|
||||||
|
msgstr "اختبار الاتصال مع Avatax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_partner_code
|
||||||
|
msgid ""
|
||||||
|
"The Avalara Company Code for this company. Avalara will interpret as DEFAULT"
|
||||||
|
" if it is not set."
|
||||||
|
msgstr ""
|
||||||
|
"رمز الشركة في Avalara لهذه الشركة. سيتعامل Avalara معه كـ 'DEFAULT' إذا لم "
|
||||||
|
"يتم تعيينه."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid ""
|
||||||
|
"The Avalara Tax Code is required for %(name)s (#%(id)s)\n"
|
||||||
|
"See https://taxcode.avatax.avalara.com/"
|
||||||
|
msgstr ""
|
||||||
|
"رمز ضريبة Avalara مطلوب لـ %(name)s (#%(id)s)\n"
|
||||||
|
"انظر https://taxcode.avatax.avalara.com/"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/account_external_tax_mixin.py:0
|
||||||
|
msgid ""
|
||||||
|
"The following customer(s) need to have a zip, state and country when using "
|
||||||
|
"Avatax:"
|
||||||
|
msgstr ""
|
||||||
|
"يحتاج العملاء التاليون إلى رمز بريدي وولاية وبلد عند استخدام Avatax:"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_commit
|
||||||
|
msgid "The transactions will be committed for reporting in Avatax."
|
||||||
|
msgstr "ستُؤَكَّد المعاملات للإبلاغ في Avatax."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "This is already a valid address."
|
||||||
|
msgstr "هذا عنوان صالح بالفعل."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__setting_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__setting_account_avatax
|
||||||
|
msgid "Use AvaTax"
|
||||||
|
msgstr "استخدام AvaTax"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_account_fiscal_position__is_avatax
|
||||||
|
msgid "Use AvaTax API"
|
||||||
|
msgstr "استخدام واجهة AvaTax API"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_company__avalara_use_upc
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_res_config_settings__avalara_use_upc
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_config_settings_view_form
|
||||||
|
msgid "Use UPC"
|
||||||
|
msgstr "استخدام UPC"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_use_upc
|
||||||
|
msgid "Use Universal Product Code instead of custom defined codes in Avalara."
|
||||||
|
msgstr "استخدام رمز المنتج العالمي بدلاً من الرموز المخصصة في Avalara."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_avatax_unique_code__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_bank_statement_line__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_account_move__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_partner__avatax_unique_code
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_users__avatax_unique_code
|
||||||
|
msgid "Use this code to cross-reference in the Avalara portal."
|
||||||
|
msgstr "استخدم هذا الرمز للإحالة المتبادلة في بوابة Avalara."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_exemption__valid_country_ids
|
||||||
|
msgid "Valid Country"
|
||||||
|
msgstr "البلد الصالح"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.res_partner_form_inherit
|
||||||
|
msgid "Validate"
|
||||||
|
msgstr "التحقق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/res_partner.py:0
|
||||||
|
msgid "Validate address of %s"
|
||||||
|
msgstr "التحقق من عنوان %s"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_res_config_settings__avalara_address_validation
|
||||||
|
msgid ""
|
||||||
|
"Validate and correct the addresses of partners in North America with "
|
||||||
|
"Avalara."
|
||||||
|
msgstr ""
|
||||||
|
"التحقق من عناوين الشركاء في أمريكا الشمالية وتصحيحها مع Avalara."
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Validated Address"
|
||||||
|
msgstr "العنوان المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_city
|
||||||
|
msgid "Validated City"
|
||||||
|
msgstr "المدينة المُصَدَّقة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_country_id
|
||||||
|
msgid "Validated Country"
|
||||||
|
msgstr "البلد المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_state_id
|
||||||
|
msgid "Validated State"
|
||||||
|
msgstr "الولاية المُصَدَّقة"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_street
|
||||||
|
msgid "Validated Street"
|
||||||
|
msgstr "الشارع المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_street2
|
||||||
|
msgid "Validated Street2"
|
||||||
|
msgstr "الشارع 2 المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__validated_zip
|
||||||
|
msgid "Validated Zip Code"
|
||||||
|
msgstr "رمز البريد المُصَدَّق"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,field_description:odex30_account_avatax.field_avatax_validate_address__zip
|
||||||
|
#: model_terms:ir.ui.view,arch_db:odex30_account_avatax.avatax_validate_address_view_form
|
||||||
|
msgid "Zip Code"
|
||||||
|
msgstr "رمز البريد"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/odex30_account_avatax/models/product.py:0
|
||||||
|
msgid "[%(code)s] %(description)s"
|
||||||
|
msgstr "[%(code)s] %(description)s"
|
||||||
|
|
||||||
|
#. module: odex30_account_avatax
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_product_category__avatax_category_id
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_product_product__avatax_category_id
|
||||||
|
#: model:ir.model.fields,help:odex30_account_avatax.field_product_template__avatax_category_id
|
||||||
|
msgid "https://taxcode.avatax.avalara.com/"
|
||||||
|
msgstr "https://taxcode.avatax.avalara.com/"
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pformat
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
str_type = (str, type(None))
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AvataxClient:
|
||||||
|
def __init__(self, app_name=None, app_version=None, machine_name=None,
|
||||||
|
environment=None, timeout_limit=None):
|
||||||
|
if not all(isinstance(i, str_type) for i in [app_name,
|
||||||
|
machine_name,
|
||||||
|
environment]):
|
||||||
|
raise ValueError('Input(s) must be string or none type object')
|
||||||
|
self.base_url = 'https://sandbox-rest.avatax.com'
|
||||||
|
self.is_production = environment and environment.lower() == 'production'
|
||||||
|
if self.is_production:
|
||||||
|
self.base_url = 'https://rest.avatax.com'
|
||||||
|
self.auth = None
|
||||||
|
self.app_name = app_name
|
||||||
|
self.app_version = app_version
|
||||||
|
self.machine_name = machine_name
|
||||||
|
self.client_id = '{}; {}; Python SDK; 18.5; {};'.format(app_name,
|
||||||
|
app_version,
|
||||||
|
machine_name)
|
||||||
|
self.client_header = {'X-Avalara-Client': self.client_id}
|
||||||
|
self.timeout_limit = timeout_limit
|
||||||
|
|
||||||
|
def add_credentials(self, username=None, password=None):
|
||||||
|
if not all(isinstance(i, str_type) for i in [username, password]):
|
||||||
|
raise ValueError('Input(s) must be string or none type object')
|
||||||
|
if username and not password:
|
||||||
|
self.client_header['Authorization'] = 'Bearer ' + username
|
||||||
|
else:
|
||||||
|
self.auth = HTTPBasicAuth(username, password)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def request(self, method, endpoint, params, json):
|
||||||
|
|
||||||
|
start = str(datetime.utcnow())
|
||||||
|
url = '{}/api/v2/{}'.format(self.base_url, endpoint)
|
||||||
|
response = requests.request(
|
||||||
|
method, url,
|
||||||
|
auth=self.auth,
|
||||||
|
headers=self.client_header,
|
||||||
|
timeout=self.timeout_limit if self.timeout_limit else 1200,
|
||||||
|
params=params,
|
||||||
|
json=json
|
||||||
|
).json()
|
||||||
|
end = str(datetime.utcnow())
|
||||||
|
if hasattr(self, 'logger'):
|
||||||
|
self.logger(
|
||||||
|
f"{method}\nstart={start}\nend={end}\nargs={pformat(url)}\nparams={pformat(params)}\njson={pformat(json)}\n"
|
||||||
|
f"response={pformat(response)}"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def create_transaction(self, model, include=None):
|
||||||
|
return self.request('POST', 'transactions/createoradjust', params=include, json={'createTransactionModel': model})
|
||||||
|
|
||||||
|
def uncommit_transaction(self, companyCode, transactionCode, include=None):
|
||||||
|
return self.request('POST', 'companies/{}/transactions/{}/uncommit'.format(companyCode, transactionCode),
|
||||||
|
params=include, json=None)
|
||||||
|
|
||||||
|
def void_transaction(self, companyCode, transactionCode, model, include=None):
|
||||||
|
return self.request('POST', 'companies/{}/transactions/{}/void'.format(companyCode, transactionCode),
|
||||||
|
params=include, json=model)
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
return self.request('GET', 'utilities/ping', params=None, json=None)
|
||||||
|
|
||||||
|
def resolve_address(self, model=None):
|
||||||
|
return self.request('POST', 'addresses/resolve', params=None, json=model)
|
||||||
|
|
||||||
|
def list_entity_use_codes(self, include=None):
|
||||||
|
return self.request('GET', 'definitions/entityusecodes', params=include, json=None)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from . import account_avatax_unique_code
|
||||||
|
from . import account_chart_template
|
||||||
|
from . import product
|
||||||
|
from . import avatax_exemption
|
||||||
|
from . import res_partner
|
||||||
|
from . import res_company
|
||||||
|
from . import account_move
|
||||||
|
from . import account_fiscal_position
|
||||||
|
from . import account_external_tax_mixin
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from odoo import models, fields, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.osv import expression
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAvataxUniqueCode(models.AbstractModel):
|
||||||
|
|
||||||
|
_name = 'account.avatax.unique.code'
|
||||||
|
_description = 'Mixin to generate unique ids for Avatax'
|
||||||
|
|
||||||
|
avatax_unique_code = fields.Char(
|
||||||
|
"Avalara Code",
|
||||||
|
compute="_compute_avatax_unique_code",
|
||||||
|
search="_search_avatax_unique_code",
|
||||||
|
store=False,
|
||||||
|
help="Use this code to cross-reference in the Avalara portal."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_avatax_description(self):
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _compute_avatax_unique_code(self):
|
||||||
|
for record in self:
|
||||||
|
record.avatax_unique_code = '%s %s' % (record._get_avatax_description(), record.id)
|
||||||
|
|
||||||
|
def _search_avatax_unique_code(self, operator, value):
|
||||||
|
unsupported_operators = ('in', 'not in', '<', '<=', '>', '>=')
|
||||||
|
if operator in unsupported_operators or not isinstance(value, str):
|
||||||
|
raise UserError(_("Search operation not supported"))
|
||||||
|
|
||||||
|
value = value.lower()
|
||||||
|
|
||||||
|
prefix = self._get_avatax_description().lower() + " "
|
||||||
|
if value.startswith(prefix):
|
||||||
|
value = value[len(prefix):]
|
||||||
|
|
||||||
|
if operator in ('=', '!=') and not value.isdigit():
|
||||||
|
return expression.FALSE_DOMAIN
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
return expression.FALSE_DOMAIN
|
||||||
|
|
||||||
|
return [('id', operator, value)]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from odoo import models
|
||||||
|
from odoo.addons.account.models.chart_template import template
|
||||||
|
|
||||||
|
|
||||||
|
class AccountChartTemplate(models.AbstractModel):
|
||||||
|
_inherit = 'account.chart.template'
|
||||||
|
|
||||||
|
@template('generic_coa', 'account.fiscal.position')
|
||||||
|
def _get_us_avatax_fiscal_position(self):
|
||||||
|
return {
|
||||||
|
'account_fiscal_position_avatax_us': {
|
||||||
|
'name': 'Automatic Tax Mapping (AvaTax)',
|
||||||
|
'is_avatax': True,
|
||||||
|
'auto_apply': False,
|
||||||
|
'country_id': self.env.ref('base.us').id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,357 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
from odoo import models, api, fields, _
|
||||||
|
from odoo.addons.odex30_account_avatax.lib.avatax_client import AvataxClient
|
||||||
|
from odoo.exceptions import UserError, ValidationError, RedirectWarning
|
||||||
|
from odoo.release import version
|
||||||
|
from odoo.tools import float_round, format_list
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountExternalTaxMixin(models.AbstractModel):
|
||||||
|
_inherit = 'account.external.tax.mixin'
|
||||||
|
|
||||||
|
is_avatax = fields.Boolean(compute='_compute_is_avatax')
|
||||||
|
|
||||||
|
@api.depends('fiscal_position_id')
|
||||||
|
def _compute_is_avatax(self):
|
||||||
|
for record in self:
|
||||||
|
record.is_avatax = record.fiscal_position_id.is_avatax
|
||||||
|
|
||||||
|
def _compute_is_tax_computed_externally(self):
|
||||||
|
super()._compute_is_tax_computed_externally()
|
||||||
|
self.filtered('is_avatax').is_tax_computed_externally = True
|
||||||
|
|
||||||
|
def _get_external_taxes(self):
|
||||||
|
""" Override. """
|
||||||
|
def find_or_create_tax(doc, detail):
|
||||||
|
def repartition_line(repartition_type, account=None):
|
||||||
|
return (0, 0, {
|
||||||
|
'repartition_type': repartition_type,
|
||||||
|
'tag_ids': [],
|
||||||
|
'company_id': doc.company_id.id,
|
||||||
|
'account_id': account and account.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
fixed = detail.get('unitOfBasis') == 'FlatAmount'
|
||||||
|
rate = detail['rate'] if fixed else detail['rate'] * 100
|
||||||
|
name_precision = 4
|
||||||
|
rounded_rate = float_round(rate, name_precision)
|
||||||
|
tax_group_name = detail['taxName'].removesuffix(' TAX')
|
||||||
|
tax_name = '%s %s' % (
|
||||||
|
tax_group_name,
|
||||||
|
("$ %.4g" if fixed else "%.4g%%") % rounded_rate,
|
||||||
|
)
|
||||||
|
group_key = (tax_group_name, doc.company_id)
|
||||||
|
if group_key not in tax_group_cache:
|
||||||
|
tax_group_cache[group_key] = self.env['account.tax.group'].search([
|
||||||
|
*self.env['account.tax.group']._check_company_domain(doc.company_id),
|
||||||
|
('name', '=', tax_group_name),
|
||||||
|
], limit=1) or self.env['account.tax.group'].sudo().with_company(doc.company_id).create({
|
||||||
|
'name': tax_group_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
key = (tax_name, doc.company_id)
|
||||||
|
if key not in tax_cache:
|
||||||
|
tax_cache[key] = self.env['account.tax'].search([
|
||||||
|
*self.env['account.tax']._check_company_domain(doc.company_id),
|
||||||
|
('name', '=', tax_name),
|
||||||
|
], limit=1) or self.env['account.tax'].sudo().with_company(self._find_avatax_credentials_company(doc.company_id)).create({
|
||||||
|
'name': tax_name,
|
||||||
|
'tax_group_id': tax_group_cache[group_key].id,
|
||||||
|
'amount': rate,
|
||||||
|
'amount_type': 'fixed' if fixed else 'percent',
|
||||||
|
'refund_repartition_line_ids': [
|
||||||
|
repartition_line('base'),
|
||||||
|
repartition_line('tax', doc.fiscal_position_id.avatax_refund_account_id),
|
||||||
|
],
|
||||||
|
'invoice_repartition_line_ids': [
|
||||||
|
repartition_line('base'),
|
||||||
|
repartition_line('tax', doc.fiscal_position_id.avatax_invoice_account_id),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return tax_cache[key]
|
||||||
|
|
||||||
|
details, summary = super()._get_external_taxes()
|
||||||
|
tax_cache = {}
|
||||||
|
tax_group_cache = {}
|
||||||
|
|
||||||
|
query_results = self.filtered('is_avatax')._query_avatax_taxes()
|
||||||
|
errors = []
|
||||||
|
for document, query_result in query_results.items():
|
||||||
|
error = self._handle_response(query_result, _(
|
||||||
|
'EXP could not fetch the taxes related to %(document)s.\n'
|
||||||
|
'Please check the status of `%(technical)s` in the AvaTax portal.',
|
||||||
|
document=document.display_name,
|
||||||
|
technical=document.avatax_unique_code,
|
||||||
|
))
|
||||||
|
if error:
|
||||||
|
errors.append(error)
|
||||||
|
if errors:
|
||||||
|
raise UserError('\n\n'.join(errors))
|
||||||
|
|
||||||
|
for document, query_result in query_results.items():
|
||||||
|
is_return = document._get_avatax_document_type() == 'ReturnInvoice'
|
||||||
|
line_amounts_sign = -1 if is_return else 1
|
||||||
|
|
||||||
|
for line_result in query_result['lines']:
|
||||||
|
record_id = line_result['lineNumber'].split(',')
|
||||||
|
record = self.env[record_id[0]].browse(int(record_id[1]))
|
||||||
|
details.setdefault(record, {})
|
||||||
|
details[record]['total'] = line_amounts_sign * line_result['lineAmount']
|
||||||
|
details[record]['tax_amount'] = line_amounts_sign * line_result['tax']
|
||||||
|
for detail in line_result['details']:
|
||||||
|
tax = find_or_create_tax(document, detail)
|
||||||
|
details[record].setdefault('tax_ids', self.env['account.tax'])
|
||||||
|
details[record]['tax_ids'] += tax
|
||||||
|
|
||||||
|
summary[document] = defaultdict(float)
|
||||||
|
for summary_line in query_result['summary']:
|
||||||
|
tax = find_or_create_tax(document, summary_line)
|
||||||
|
|
||||||
|
summary[document][tax] += -summary_line['tax']
|
||||||
|
|
||||||
|
return details, summary
|
||||||
|
|
||||||
|
|
||||||
|
@api.constrains('partner_id', 'fiscal_position_id')
|
||||||
|
def _check_address(self):
|
||||||
|
incomplete_partner_to_records = self._get_partners_with_incomplete_information()
|
||||||
|
|
||||||
|
if incomplete_partner_to_records:
|
||||||
|
error = _("The following customer(s) need to have a zip, state and country when using Avatax:")
|
||||||
|
partner_errors = [
|
||||||
|
_(
|
||||||
|
"- %(partner_name)s (ID: %(partner_id)s) on %(record_list)s",
|
||||||
|
partner_name=partner.display_name,
|
||||||
|
partner_id=partner.id,
|
||||||
|
record_list=format_list(self.env, [record.display_name for record in records]),
|
||||||
|
)
|
||||||
|
for partner, records in incomplete_partner_to_records.items()
|
||||||
|
]
|
||||||
|
raise ValidationError(error + "\n" + "\n".join(partner_errors))
|
||||||
|
|
||||||
|
def _get_partners_with_incomplete_information(self, partner=None):
|
||||||
|
|
||||||
|
incomplete_partner_to_records = {}
|
||||||
|
for record in self.filtered(lambda r: r._perform_address_validation()):
|
||||||
|
partner = partner or record.partner_id
|
||||||
|
country = partner.country_id
|
||||||
|
if (
|
||||||
|
partner and partner != self.env.ref('base.public_partner')
|
||||||
|
and (
|
||||||
|
not country
|
||||||
|
or (country.zip_required and not partner.zip)
|
||||||
|
or (country.state_required and not partner.state_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
incomplete_partner_to_records.setdefault(partner, []).append(record)
|
||||||
|
|
||||||
|
return incomplete_partner_to_records
|
||||||
|
|
||||||
|
|
||||||
|
def _get_avatax_dates(self):
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _get_avatax_document_type(self):
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _get_avatax_ship_to_partner(self):
|
||||||
|
|
||||||
|
return self.partner_shipping_id or self.partner_id
|
||||||
|
|
||||||
|
def _perform_address_validation(self):
|
||||||
|
|
||||||
|
return self.fiscal_position_id.is_avatax
|
||||||
|
|
||||||
|
|
||||||
|
def _get_avatax_invoice_line(self, line_data):
|
||||||
|
|
||||||
|
product = line_data['product_id']
|
||||||
|
if not product._get_avatax_category_id():
|
||||||
|
raise UserError(_(
|
||||||
|
'The Avalara Tax Code is required for %(name)s (#%(id)s)\n'
|
||||||
|
'See https://taxcode.avatax.avalara.com/',
|
||||||
|
name=product.display_name,
|
||||||
|
id=product.id,
|
||||||
|
))
|
||||||
|
item_code = product.code or ""
|
||||||
|
if self.env.company.avalara_use_upc and product.barcode:
|
||||||
|
item_code = f'UPC:{product.barcode}'
|
||||||
|
return {
|
||||||
|
'amount': -line_data["price_subtotal"] if line_data["is_refund"] else line_data["price_subtotal"],
|
||||||
|
'description': product.display_name,
|
||||||
|
'quantity': abs(line_data["qty"]),
|
||||||
|
'taxCode': product._get_avatax_category_id().code,
|
||||||
|
'itemCode': item_code,
|
||||||
|
'number': "%s,%s" % (line_data["model_name"], line_data["id"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _find_avatax_credentials_company(self, company):
|
||||||
|
has_avatax_credentials = bool(company.sudo().avalara_api_id and company.sudo().avalara_api_key)
|
||||||
|
if has_avatax_credentials:
|
||||||
|
return company
|
||||||
|
elif company.parent_id:
|
||||||
|
return self._find_avatax_credentials_company(company.parent_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_avatax_ref(self):
|
||||||
|
return self.name or ''
|
||||||
|
|
||||||
|
def _get_avatax_address_from_partner(self, partner):
|
||||||
|
|
||||||
|
incomplete_partner = self._get_partners_with_incomplete_information(partner)
|
||||||
|
if incomplete_partner.get(partner):
|
||||||
|
res = {
|
||||||
|
'latitude': partner.partner_latitude,
|
||||||
|
'longitude': partner.partner_longitude,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
'city': partner.city,
|
||||||
|
'country': partner.country_id.code,
|
||||||
|
'region': partner.state_id.code,
|
||||||
|
'postalCode': partner.zip,
|
||||||
|
'line1': partner.street,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _get_avatax_addresses(self, partner):
|
||||||
|
|
||||||
|
res = {
|
||||||
|
'shipFrom': self._get_avatax_address_from_partner(self.company_id.partner_id),
|
||||||
|
'shipTo': self._get_avatax_address_from_partner(partner),
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _get_avatax_invoice_lines(self):
|
||||||
|
return [self._get_avatax_invoice_line(line_data) for line_data in self._get_line_data_for_external_taxes()]
|
||||||
|
|
||||||
|
def _get_avatax_taxes(self, commit):
|
||||||
|
|
||||||
|
self.ensure_one()
|
||||||
|
partner = self.partner_id.commercial_partner_id
|
||||||
|
document_date, tax_date = self._get_avatax_dates()
|
||||||
|
taxes = {
|
||||||
|
'addresses': self._get_avatax_addresses(self._get_avatax_ship_to_partner()),
|
||||||
|
'companyCode': self.company_id.partner_id.avalara_partner_code or '',
|
||||||
|
'customerCode': partner.avalara_partner_code or partner.avatax_unique_code,
|
||||||
|
'entityUseCode': partner.with_company(self.company_id).avalara_exemption_id.code or '',
|
||||||
|
'businessIdentificationNo': partner.vat or '',
|
||||||
|
'date': (document_date or fields.Date.today()).isoformat(),
|
||||||
|
'lines': self._get_avatax_invoice_lines(),
|
||||||
|
'type': self._get_avatax_document_type(),
|
||||||
|
'code': self.avatax_unique_code,
|
||||||
|
'referenceCode': self._get_avatax_ref(),
|
||||||
|
'currencyCode': self.currency_id.name or '',
|
||||||
|
'commit': commit and self.company_id.avalara_commit,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tax_date:
|
||||||
|
taxes['taxOverride'] = {
|
||||||
|
'type': 'taxDate',
|
||||||
|
'reason': 'Manually changed the tax calculation date',
|
||||||
|
'taxDate': tax_date.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return taxes
|
||||||
|
|
||||||
|
def _commit_avatax_taxes(self):
|
||||||
|
self._query_avatax_taxes(commit=True)
|
||||||
|
|
||||||
|
def _query_avatax_taxes(self, commit=False):
|
||||||
|
|
||||||
|
if not self:
|
||||||
|
return {}
|
||||||
|
client = self._get_client(self.company_id)
|
||||||
|
transactions = {record: record._get_avatax_taxes(commit) for record in self}
|
||||||
|
return {
|
||||||
|
record: client.create_transaction(transaction, include='Lines')
|
||||||
|
for record, transaction in transactions.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _uncommit_external_taxes(self):
|
||||||
|
for record in self.filtered('is_avatax'):
|
||||||
|
if not record.company_id.avalara_commit:
|
||||||
|
continue
|
||||||
|
client = self._get_client(record.company_id)
|
||||||
|
query_result = client.uncommit_transaction(
|
||||||
|
companyCode=record.company_id.partner_id.avalara_partner_code,
|
||||||
|
transactionCode=record.avatax_unique_code,
|
||||||
|
)
|
||||||
|
error = self._handle_response(query_result, _(
|
||||||
|
'EXP could not change the state of the transaction related to %(document)s in'
|
||||||
|
' AvaTax\nPlease check the status of `%(technical)s` in the AvaTax portal.',
|
||||||
|
document=record.display_name,
|
||||||
|
technical=record.avatax_unique_code,
|
||||||
|
))
|
||||||
|
if error:
|
||||||
|
raise UserError(error)
|
||||||
|
|
||||||
|
return super()._uncommit_external_taxes()
|
||||||
|
|
||||||
|
def _void_external_taxes(self):
|
||||||
|
for record in self.filtered('is_avatax'):
|
||||||
|
if not record.company_id.avalara_commit:
|
||||||
|
continue
|
||||||
|
client = self._get_client(record.company_id)
|
||||||
|
query_result = client.void_transaction(
|
||||||
|
companyCode=record.company_id.partner_id.avalara_partner_code,
|
||||||
|
transactionCode=record.avatax_unique_code,
|
||||||
|
model={"code": "DocVoided"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if query_result.get('error', {}).get('code') == 'EntityNotFoundError':
|
||||||
|
_logger.info(pformat(query_result))
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = self._handle_response(query_result, _(
|
||||||
|
'EXP could not void the transaction related to %(document)s in AvaTax\nPlease '
|
||||||
|
'check the status of `%(technical)s` in the AvaTax portal.',
|
||||||
|
document=record.display_name,
|
||||||
|
technical=record.avatax_unique_code,
|
||||||
|
))
|
||||||
|
if error:
|
||||||
|
raise UserError(error)
|
||||||
|
|
||||||
|
return super()._void_external_taxes()
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_response(self, response, title):
|
||||||
|
if response.get('errors'):
|
||||||
|
_logger.warning(pformat(response), stack_info=True)
|
||||||
|
return '%s\n%s' % (title, response.get('title', ''))
|
||||||
|
if response.get('error'):
|
||||||
|
_logger.warning(pformat(response), stack_info=True)
|
||||||
|
messages = '\n'.join(detail['message'] for detail in response['error']['details'])
|
||||||
|
return '%s\n%s' % (title, messages)
|
||||||
|
|
||||||
|
def _get_client(self, company):
|
||||||
|
company = self._find_avatax_credentials_company(company)
|
||||||
|
if not company:
|
||||||
|
raise RedirectWarning(
|
||||||
|
_('Please add your AvaTax credentials'),
|
||||||
|
self.env.ref('base_setup.action_general_configuration').id,
|
||||||
|
_("Go to the configuration panel"),
|
||||||
|
)
|
||||||
|
|
||||||
|
client = AvataxClient(
|
||||||
|
app_name='Odoo',
|
||||||
|
app_version=version,
|
||||||
|
environment=company.avalara_environment,
|
||||||
|
)
|
||||||
|
client.add_credentials(
|
||||||
|
company.sudo().avalara_api_id or '',
|
||||||
|
company.sudo().avalara_api_key or '',
|
||||||
|
)
|
||||||
|
client.logger = lambda message: self._log_external_tax_request(
|
||||||
|
'Avatax US', 'odex30_account_avatax.log.end.date', message
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountFiscalPosition(models.Model):
|
||||||
|
_inherit = 'account.fiscal.position'
|
||||||
|
|
||||||
|
def _default_avatax_invoice_account_id(self):
|
||||||
|
return self.env.company.account_sale_tax_id.invoice_repartition_line_ids.account_id
|
||||||
|
|
||||||
|
def _default_avatax_refund_account_id(self):
|
||||||
|
return self.env.company.account_sale_tax_id.refund_repartition_line_ids.account_id
|
||||||
|
|
||||||
|
is_avatax = fields.Boolean(string="Use AvaTax API")
|
||||||
|
avatax_invoice_account_id = fields.Many2one(
|
||||||
|
comodel_name='account.account',
|
||||||
|
default=_default_avatax_invoice_account_id,
|
||||||
|
help="Account that will be used by Avatax taxes for invoices.",
|
||||||
|
)
|
||||||
|
avatax_refund_account_id = fields.Many2one(
|
||||||
|
comodel_name='account.account',
|
||||||
|
default=_default_avatax_refund_account_id,
|
||||||
|
help="Account that will be used by Avatax taxes for refunds.",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_name = 'account.move'
|
||||||
|
_inherit = ['account.avatax.unique.code', 'account.move']
|
||||||
|
|
||||||
|
avatax_tax_date = fields.Date(
|
||||||
|
string="Avatax Date",
|
||||||
|
help="Avatax will use this date to calculate the tax on this invoice. "
|
||||||
|
"If not specified it will use the Invoice Date.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _post(self, soft=True):
|
||||||
|
res = super()._post(soft=soft)
|
||||||
|
self.filtered(
|
||||||
|
lambda move: move.is_avatax and move.move_type in ('out_invoice', 'out_refund') and not move._is_downpayment()
|
||||||
|
)._commit_avatax_taxes()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _get_avatax_dates(self):
|
||||||
|
external_tax_date = self._get_date_for_external_taxes()
|
||||||
|
if self.reversed_entry_id:
|
||||||
|
reversed_override_date = self.reversed_entry_id.avatax_tax_date or self.reversed_entry_id._get_date_for_external_taxes()
|
||||||
|
return external_tax_date, reversed_override_date
|
||||||
|
return external_tax_date, self.avatax_tax_date
|
||||||
|
|
||||||
|
def _get_avatax_document_type(self):
|
||||||
|
return {
|
||||||
|
'out_invoice': 'SalesInvoice',
|
||||||
|
'out_refund': 'ReturnInvoice',
|
||||||
|
'in_invoice': 'PurchaseInvoice',
|
||||||
|
'in_refund': 'ReturnInvoice',
|
||||||
|
'entry': 'Any',
|
||||||
|
}[self.move_type]
|
||||||
|
|
||||||
|
def _get_avatax_description(self):
|
||||||
|
return 'Journal Entry'
|
||||||
|
|
||||||
|
def _perform_address_validation(self):
|
||||||
|
|
||||||
|
moves = self.filtered(lambda m: m.move_type in ('out_invoice', 'out_refund'))
|
||||||
|
return super(AccountMove, moves)._perform_address_validation() and not moves.origin_payment_id
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AvataxExemption(models.Model):
|
||||||
|
_name = 'avatax.exemption'
|
||||||
|
_description = "Avatax Partner Exemption Codes"
|
||||||
|
_rec_names_search = ['name', 'code']
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
code = fields.Char(required=True)
|
||||||
|
description = fields.Char()
|
||||||
|
valid_country_ids = fields.Many2many('res.country')
|
||||||
|
company_id = fields.Many2one('res.company', required=True)
|
||||||
|
|
||||||
|
@api.depends('code')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for record in self:
|
||||||
|
record.display_name = f'[{record.code}] {record.name}'
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class ProductAvataxCategory(models.Model):
|
||||||
|
_name = 'product.avatax.category'
|
||||||
|
_description = "Avatax Product Category"
|
||||||
|
_rec_name = 'code'
|
||||||
|
_rec_names_search = ['description', 'code']
|
||||||
|
|
||||||
|
code = fields.Char(required=True)
|
||||||
|
description = fields.Char(required=True)
|
||||||
|
|
||||||
|
@api.depends('code', 'description')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for category in self:
|
||||||
|
category.display_name = _('[%(code)s] %(description)s', code=category.code, description=(category.description or '')[:50])
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCategory(models.Model):
|
||||||
|
_inherit = 'product.category'
|
||||||
|
|
||||||
|
avatax_category_id = fields.Many2one(
|
||||||
|
'product.avatax.category',
|
||||||
|
help="https://taxcode.avatax.avalara.com/",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_avatax_category_id(self):
|
||||||
|
categ = self
|
||||||
|
while categ and not categ.avatax_category_id:
|
||||||
|
categ = categ.parent_id
|
||||||
|
return categ.avatax_category_id
|
||||||
|
|
||||||
|
|
||||||
|
class ProductTemplate(models.Model):
|
||||||
|
_inherit = 'product.template'
|
||||||
|
|
||||||
|
avatax_category_id = fields.Many2one(
|
||||||
|
'product.avatax.category',
|
||||||
|
help="https://taxcode.avatax.avalara.com/",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_avatax_category_id(self):
|
||||||
|
return self.avatax_category_id or self.categ_id._get_avatax_category_id()
|
||||||
|
|
||||||
|
|
||||||
|
class ProductProduct(models.Model):
|
||||||
|
_inherit = 'product.product'
|
||||||
|
|
||||||
|
avatax_category_id = fields.Many2one(
|
||||||
|
'product.avatax.category',
|
||||||
|
help="https://taxcode.avatax.avalara.com/",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_avatax_category_id(self):
|
||||||
|
return self.avatax_category_id or self.product_tmpl_id._get_avatax_category_id()
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from odoo import fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
avalara_api_id = fields.Char(string='Avalara API ID', groups='base.group_system')
|
||||||
|
avalara_api_key = fields.Char(string='Avalara API KEY', groups='base.group_system')
|
||||||
|
avalara_environment = fields.Selection(
|
||||||
|
string="Avalara Environment",
|
||||||
|
selection=[
|
||||||
|
('sandbox', 'Sandbox'),
|
||||||
|
('production', 'Production'),
|
||||||
|
],
|
||||||
|
required=True,
|
||||||
|
default='sandbox',
|
||||||
|
)
|
||||||
|
avalara_commit = fields.Boolean(string="Commit in Avatax")
|
||||||
|
avalara_address_validation = fields.Boolean(string="Avalara Address Validation")
|
||||||
|
avalara_use_upc = fields.Boolean(string="Use UPC", default=True)
|
||||||
|
setting_account_avatax = fields.Boolean(string='Use AvaTax', store=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
avalara_api_id = fields.Char(
|
||||||
|
related='company_id.avalara_api_id',
|
||||||
|
readonly=False,
|
||||||
|
string='Avalara API ID',
|
||||||
|
)
|
||||||
|
avalara_api_key = fields.Char(
|
||||||
|
related='company_id.avalara_api_key',
|
||||||
|
readonly=False,
|
||||||
|
string='Avalara API KEY',
|
||||||
|
)
|
||||||
|
avalara_partner_code = fields.Char(
|
||||||
|
related='company_id.partner_id.avalara_partner_code',
|
||||||
|
readonly=False,
|
||||||
|
string='Avalara Company Code',
|
||||||
|
help="The Avalara Company Code for this company. Avalara will interpret as DEFAULT if it"
|
||||||
|
" is not set.",
|
||||||
|
)
|
||||||
|
avalara_environment = fields.Selection(
|
||||||
|
related='company_id.avalara_environment',
|
||||||
|
readonly=False,
|
||||||
|
string="Avalara Environment",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
avalara_commit = fields.Boolean(
|
||||||
|
related='company_id.avalara_commit',
|
||||||
|
readonly=False,
|
||||||
|
string='Commit in Avatax',
|
||||||
|
help="The transactions will be committed for reporting in Avatax.",
|
||||||
|
)
|
||||||
|
avalara_address_validation = fields.Boolean(
|
||||||
|
related='company_id.avalara_address_validation',
|
||||||
|
string='Avalara Address Validation',
|
||||||
|
readonly=False,
|
||||||
|
help="Validate and correct the addresses of partners in North America with Avalara.",
|
||||||
|
)
|
||||||
|
avalara_use_upc = fields.Boolean(
|
||||||
|
related='company_id.avalara_use_upc',
|
||||||
|
readonly=False,
|
||||||
|
string="Use UPC",
|
||||||
|
help="Use Universal Product Code instead of custom defined codes in Avalara.",
|
||||||
|
)
|
||||||
|
setting_account_avatax = fields.Boolean(
|
||||||
|
related='company_id.setting_account_avatax',
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def avatax_sync_company_params(self):
|
||||||
|
def get_countries(code_list):
|
||||||
|
uncached = set(code_list) - set(country_cache)
|
||||||
|
if uncached:
|
||||||
|
country_cache.update({
|
||||||
|
country.code: country.id
|
||||||
|
for country in self.env['res.country'].search([('code', 'in', tuple(uncached))])
|
||||||
|
})
|
||||||
|
return self.env['res.country'].browse([country_cache[code] for code in code_list])
|
||||||
|
country_cache = {'*': False}
|
||||||
|
|
||||||
|
existing = {
|
||||||
|
exempt['code'] for exempt in self.env['avatax.exemption'].search_read(
|
||||||
|
domain=[('company_id', '=', self.company_id.id)],
|
||||||
|
fields=['code'],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
client = self.env['account.external.tax.mixin']._get_client(self.company_id)
|
||||||
|
response = client.list_entity_use_codes()
|
||||||
|
error = self.env['account.external.tax.mixin']._handle_response(response, _(
|
||||||
|
"Odoo could not fetch the exemption codes of %(company)s",
|
||||||
|
company=self.company_id.display_name,
|
||||||
|
))
|
||||||
|
if error:
|
||||||
|
raise UserError(error)
|
||||||
|
self.env['avatax.exemption'].create([
|
||||||
|
{
|
||||||
|
'code': vals['code'],
|
||||||
|
'description': vals['description'],
|
||||||
|
'name': vals['name'],
|
||||||
|
'valid_country_ids': [(6, 0, get_countries(vals['validCountries']).ids)],
|
||||||
|
'company_id': self.company_id.id,
|
||||||
|
}
|
||||||
|
for vals in response['value']
|
||||||
|
if vals['code'] not in existing
|
||||||
|
])
|
||||||
|
return True
|
||||||
|
|
||||||
|
def avatax_ping(self):
|
||||||
|
|
||||||
|
client = self.env['account.external.tax.mixin']._get_client(self.company_id)
|
||||||
|
query_result = client.ping()
|
||||||
|
|
||||||
|
html_content = self._format_response(query_result)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': _('Test Result'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'avatax.connection.test.result',
|
||||||
|
'res_id': self.env['avatax.connection.test.result'].create({'server_response': html_content}).id,
|
||||||
|
'target': 'new',
|
||||||
|
'views': [(False, 'form')],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_response(self, query_result):
|
||||||
|
html_content = _("Authentication success.") if query_result['authenticated'] else _("Authentication failed.")
|
||||||
|
|
||||||
|
html_content += '<ul>'
|
||||||
|
for key, value in query_result.items():
|
||||||
|
html_content += f'<li><span class="fw-bold">{key.capitalize()}:</span> {value}</li>'
|
||||||
|
html_content += '</ul>'
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
def avatax_log(self):
|
||||||
|
self.env['account.external.tax.mixin']._enable_external_tax_logging('odex30_account_avatax.log.end.date')
|
||||||
|
return True
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ADDRESS_FIELDS = ('street', 'street2', 'city', 'state_id', 'zip', 'country_id')
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_name = 'res.partner'
|
||||||
|
_inherit = ['res.partner', 'account.avatax.unique.code']
|
||||||
|
|
||||||
|
avalara_partner_code = fields.Char(
|
||||||
|
string='Avalara Partner Code',
|
||||||
|
help="Customer Code set in Avalara for this partner.",
|
||||||
|
)
|
||||||
|
avalara_exemption_id = fields.Many2one(
|
||||||
|
comodel_name='avatax.exemption',
|
||||||
|
company_dependent=True,
|
||||||
|
domain="['|', ('valid_country_ids', 'in', country_id), ('valid_country_ids', '=', False)]",
|
||||||
|
)
|
||||||
|
avalara_show_address_validation = fields.Boolean(
|
||||||
|
compute='_compute_avalara_show_address_validation',
|
||||||
|
store=False,
|
||||||
|
string='Avalara Show Address Validation',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('country_id')
|
||||||
|
def _compute_avalara_show_address_validation(self):
|
||||||
|
for partner in self:
|
||||||
|
company = partner.company_id or self.env.company
|
||||||
|
partner.avalara_show_address_validation = company.avalara_address_validation and partner.street and (not partner.country_id or partner.fiscal_country_codes in ('US', 'CA'))
|
||||||
|
|
||||||
|
def _get_avatax_description(self):
|
||||||
|
return 'Contact'
|
||||||
|
|
||||||
|
def action_open_validation_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': _('Validate address of %s', self.display_name),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'avatax.validate.address',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_partner_id': self.id},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="report_invoice_document" inherit_id="account.report_invoice_document">
|
||||||
|
<!-- TODO not the best thing to inherit... -->
|
||||||
|
<xpath expr="//span[@id='line_tax_ids']/.." position="attributes">
|
||||||
|
<attribute name="t-if">not o.is_avatax</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//th[@name='th_taxes']" position="attributes">
|
||||||
|
<attribute name="t-if">not o.is_avatax</attribute>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_product_avatax_category,access_product_avatax_category,model_product_avatax_category,base.group_user,1,0,0,0
|
||||||
|
access_avatax_exemption,access_avatax_exemption,model_avatax_exemption,base.group_user,1,0,0,0
|
||||||
|
access_avatax_exemption_admin,access_avatax_exemption_admin,model_avatax_exemption,base.group_system,1,1,1,1
|
||||||
|
access_avatax_validate_address,access_avatax_validate_address,model_avatax_validate_address,base.group_user,1,1,1,1
|
||||||
|
access_avatax_connection_test_result,access_avatax_connection_test_result,model_avatax_connection_test_result,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
from . import test_avatax
|
||||||
|
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
test_address_validation,
|
||||||
|
|
||||||
|
test_refunds,
|
||||||
|
test_use_tax,
|
||||||
|
test_vat,
|
||||||
|
test_avatax_unique_code,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager, ExitStack
|
||||||
|
from unittest import SkipTest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from odoo import Command
|
||||||
|
from odoo.addons.account_avatax.lib.avatax_client import AvataxClient
|
||||||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from .mocked_invoice_1_response import generate_response as generate_response_invoice_1
|
||||||
|
from .mocked_invoice_2_response import generate_response as generate_response_invoice_2
|
||||||
|
from .mocked_invoice_3_response import generate_response as generate_response_invoice_3
|
||||||
|
|
||||||
|
NOTHING = object()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAvataxCommon(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
res = super().setUpClass()
|
||||||
|
cls.env.company.avalara_api_id = os.getenv("AVALARA_LOGIN_ID") or "AVALARA_LOGIN_ID"
|
||||||
|
cls.env.company.avalara_api_key = os.getenv("AVALARA_API_KEY") or "AVALARA_API_KEY"
|
||||||
|
cls.env.company.avalara_environment = 'sandbox'
|
||||||
|
cls.env.company.avalara_commit = True
|
||||||
|
|
||||||
|
company = cls.env.user.company_id
|
||||||
|
company.write({
|
||||||
|
'street': "250 Executive Park Blvd",
|
||||||
|
'city': "San Francisco",
|
||||||
|
'state_id': cls.env.ref("base.state_us_5").id,
|
||||||
|
'country_id': cls.env.ref("base.us").id,
|
||||||
|
'zip': "94134",
|
||||||
|
})
|
||||||
|
company.partner_id.avalara_partner_code = os.getenv("AVALARA_COMPANY_CODE") or "DEFAULT"
|
||||||
|
|
||||||
|
cls.fp_avatax = cls.env['account.fiscal.position'].create({
|
||||||
|
'name': 'Avatax',
|
||||||
|
'is_avatax': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.partner = cls.env["res.partner"].create({
|
||||||
|
'name': "Sale Partner",
|
||||||
|
'street': "2280 Market St",
|
||||||
|
'city': "San Francisco",
|
||||||
|
'state_id': cls.env.ref("base.state_us_5").id,
|
||||||
|
'country_id': cls.env.ref("base.us").id,
|
||||||
|
'zip': "94114",
|
||||||
|
'avalara_partner_code': 'CUST123456',
|
||||||
|
'property_account_position_id': cls.fp_avatax.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@contextmanager
|
||||||
|
def _client_patched(cls, create_transaction_details=None, **kwargs):
|
||||||
|
if kwargs.get('create_transaction') is None and create_transaction_details is not None:
|
||||||
|
def create_transaction(self, transaction, include=None):
|
||||||
|
return {
|
||||||
|
'lines': [{
|
||||||
|
'lineNumber': line['number'],
|
||||||
|
'details': create_transaction_details,
|
||||||
|
} for line in transaction['lines']],
|
||||||
|
'summary': create_transaction_details,
|
||||||
|
}
|
||||||
|
|
||||||
|
if kwargs.get('uncommit_transaction') is None:
|
||||||
|
def uncommit_transaction(self, companyCode, transactionCode, include=None):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def request(self, method, *args, **kwargs):
|
||||||
|
assert False, "Request not authorized in mock"
|
||||||
|
|
||||||
|
fnames = {fname for fname in dir(AvataxClient) if not fname.startswith('_')} - {
|
||||||
|
'add_credentials',
|
||||||
|
}
|
||||||
|
methods = {**{fname: None for fname in fnames}, **kwargs, **locals()}
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for _patch in [
|
||||||
|
patch(f'{AvataxClient.__module__}.AvataxClient.{fname}', methods[fname])
|
||||||
|
for fname in fnames
|
||||||
|
]:
|
||||||
|
stack.enter_context(_patch)
|
||||||
|
yield
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@contextmanager
|
||||||
|
def _capture_request(cls, return_value=NOTHING, return_func=NOTHING):
|
||||||
|
class Capture:
|
||||||
|
val = None
|
||||||
|
|
||||||
|
def capture_request(self, method, *args, **kwargs):
|
||||||
|
self.val = kwargs
|
||||||
|
if return_value is NOTHING:
|
||||||
|
return return_func(method, *args, **kwargs)
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
capture = Capture()
|
||||||
|
with patch(f'{AvataxClient.__module__}.AvataxClient.request', capture.capture_request):
|
||||||
|
yield capture
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@contextmanager
|
||||||
|
def _skip_no_credentials(cls):
|
||||||
|
if not os.getenv("AVALARA_LOGIN_ID") or not os.getenv("AVALARA_API_KEY") or not os.getenv("AVALARA_COMPANY_CODE"):
|
||||||
|
raise SkipTest("no Avalara credentials")
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountAvataxCommon(TestAvataxCommon, AccountTestInvoicingCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
res = super().setUpClass()
|
||||||
|
cls.product = cls.env["product.product"].create({
|
||||||
|
'name': "Product",
|
||||||
|
'default_code': 'PROD1',
|
||||||
|
'barcode': '123456789',
|
||||||
|
'list_price': 15.00,
|
||||||
|
'standard_price': 15.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
cls.product_user = cls.env["product.product"].create({
|
||||||
|
'name': "Odoo User",
|
||||||
|
'list_price': 35.00,
|
||||||
|
'standard_price': 35.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
cls.product_user_discound = cls.env["product.product"].create({
|
||||||
|
'name': "Odoo User Initial Discount",
|
||||||
|
'list_price': -5.00,
|
||||||
|
'standard_price': -5.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
cls.product_accounting = cls.env["product.product"].create({
|
||||||
|
'name': "Accounting",
|
||||||
|
'list_price': 30.00,
|
||||||
|
'standard_price': 30.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
cls.product_expenses = cls.env["product.product"].create({
|
||||||
|
'name': "Expenses",
|
||||||
|
'list_price': 15.00,
|
||||||
|
'standard_price': 15.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
cls.product_invoicing = cls.env["product.product"].create({
|
||||||
|
'name': "Invoicing",
|
||||||
|
'list_price': 15.00,
|
||||||
|
'standard_price': 15.00,
|
||||||
|
'supplier_taxes_id': None,
|
||||||
|
'avatax_category_id': cls.env.ref('odex30_account_avatax.DC010000').id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
cls.example_tax = cls.env["account.tax"].create({
|
||||||
|
'name': 'CA STATE 6%',
|
||||||
|
'company_id': cls.env.user.company_id.id,
|
||||||
|
'amount': 1,
|
||||||
|
'amount_type': 'percent',
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_invoice(cls, post=True, **kwargs):
|
||||||
|
invoice = cls.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'partner_id': cls.partner.id,
|
||||||
|
'fiscal_position_id': cls.fp_avatax.id,
|
||||||
|
'invoice_date': '2020-01-01',
|
||||||
|
'invoice_line_ids': [
|
||||||
|
(0, 0, {'product_id': cls.product.id, 'price_unit': 100}),
|
||||||
|
],
|
||||||
|
**kwargs,
|
||||||
|
})
|
||||||
|
if post:
|
||||||
|
invoice.action_post()
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_invoice_01_and_expected_response(cls):
|
||||||
|
invoice = cls.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'partner_id': cls.partner.id,
|
||||||
|
'fiscal_position_id': cls.fp_avatax.id,
|
||||||
|
'invoice_date': '2021-01-01',
|
||||||
|
'invoice_line_ids': [
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_user.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_user.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_user_discound.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_user_discound.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_accounting.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_accounting.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_expenses.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_expenses.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_invoicing.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_invoicing.list_price,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
response = generate_response_invoice_1(invoice.invoice_line_ids)
|
||||||
|
return invoice, response
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_invoice_02_and_expected_response(cls):
|
||||||
|
invoice = cls.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'partner_id': cls.partner.id,
|
||||||
|
'fiscal_position_id': cls.fp_avatax.id,
|
||||||
|
'invoice_line_ids': [
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_user.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_user.list_price,
|
||||||
|
'discount': 1 / 7 * 100,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_accounting.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_accounting.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_expenses.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_expenses.list_price,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'product_id': cls.product_invoicing.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_invoicing.list_price,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
response = generate_response_invoice_2(invoice.invoice_line_ids)
|
||||||
|
return invoice, response
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_invoice_03_and_expected_response(cls):
|
||||||
|
invoice = cls.env['account.move'].create({
|
||||||
|
'move_type': 'out_invoice',
|
||||||
|
'partner_id': cls.partner.id,
|
||||||
|
'fiscal_position_id': cls.fp_avatax.id,
|
||||||
|
'invoice_line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'product_id': cls.product_accounting.id,
|
||||||
|
'tax_ids': None,
|
||||||
|
'price_unit': cls.product_accounting.list_price,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
response = generate_response_invoice_3(invoice.invoice_line_ids)
|
||||||
|
return invoice, response
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
response = {'address': {'city': '',
|
||||||
|
'country': 'US',
|
||||||
|
'line1': '250 executiv prk blvd',
|
||||||
|
'line2': '3400',
|
||||||
|
'postalCode': '94134',
|
||||||
|
'region': '',
|
||||||
|
'textCase': 'Mixed'},
|
||||||
|
'coordinates': {'latitude': 37.71116, 'longitude': -122.391717},
|
||||||
|
'resolutionQuality': 'Intersection',
|
||||||
|
'taxAuthorities': [{'avalaraId': '275',
|
||||||
|
'jurisdictionName': 'SAN FRANCISCO',
|
||||||
|
'jurisdictionType': 'County',
|
||||||
|
'signatureCode': 'AIUQ'},
|
||||||
|
{'avalaraId': '5000531',
|
||||||
|
'jurisdictionName': 'CALIFORNIA',
|
||||||
|
'jurisdictionType': 'State',
|
||||||
|
'signatureCode': 'AGAM'},
|
||||||
|
{'avalaraId': '2001061430',
|
||||||
|
'jurisdictionName': 'SAN FRANCISCO COUNTY DISTRICT TAX SP',
|
||||||
|
'jurisdictionType': 'Special',
|
||||||
|
'signatureCode': 'EMBE'},
|
||||||
|
{'avalaraId': '2001061792',
|
||||||
|
'jurisdictionName': 'SAN FRANCISCO CO LOCAL TAX SL',
|
||||||
|
'jurisdictionType': 'Special',
|
||||||
|
'signatureCode': 'EMTV'},
|
||||||
|
{'avalaraId': '2001067344',
|
||||||
|
'jurisdictionName': 'MOSCONE EXPANSION DISTRICT ZONE 2',
|
||||||
|
'jurisdictionType': 'Special',
|
||||||
|
'signatureCode': 'MHZT'},
|
||||||
|
{'avalaraId': '2001077295',
|
||||||
|
'jurisdictionName': 'SAN FRANCISCO TOURISM IMPROVEMENT '
|
||||||
|
'DISTRICT (ZONE 2)',
|
||||||
|
'jurisdictionType': 'Special',
|
||||||
|
'signatureCode': 'NQPB'}],
|
||||||
|
'validatedAddresses': [{'addressType': 'HighRiseOrBusinessComplex',
|
||||||
|
'city': 'San Francisco',
|
||||||
|
'country': 'US',
|
||||||
|
'latitude': 37.71116,
|
||||||
|
'line1': '250 Executive Park Blvd Ste 3400',
|
||||||
|
'line2': '',
|
||||||
|
'line3': '',
|
||||||
|
'longitude': -122.391717,
|
||||||
|
'postalCode': '94134-3349',
|
||||||
|
'region': 'CA'}]}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue