diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index ea7d6da0b053c..9fbd6e83813f3 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Invoicing', @@ -91,7 +90,6 @@ 'demo': [ 'demo/account_demo.xml', ], - 'installable': True, 'application': True, 'post_init_hook': '_account_post_init', 'assets': { diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index 88392017b1d86..e6cbe9a3a08dc 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 19.0\n" +"Project-Id-Version: Odoo Server 19.1a1+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-12 14:42+0000\n" -"PO-Revision-Date: 2025-09-12 14:42+0000\n" +"POT-Creation-Date: 2025-10-30 08:38+0000\n" +"PO-Revision-Date: 2025-10-30 08:38+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -201,18 +201,13 @@ msgstr "" msgid "%d moves" msgstr "" -#. module: account -#. odoo-python -#: code:addons/account/models/account_tax.py:0 -msgid "%s (Copy)" -msgstr "" - #. module: account #. odoo-python #: code:addons/account/models/account_account.py:0 #: code:addons/account/models/account_journal.py:0 #: code:addons/account/models/account_payment_term.py:0 #: code:addons/account/models/account_reconcile_model.py:0 +#: code:addons/account/models/account_tax.py:0 msgid "%s (copy)" msgstr "" @@ -1019,8 +1014,8 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:account.portal_my_details msgid "" "\n" -" You can choose how you want us to send your invoices, and with which electronic format.\n" -" " +" You can choose how you want us to send your invoices, and with which electronic format.\n" +" " msgstr "" #. module: account @@ -1577,7 +1572,7 @@ msgid "" " You have received new electronic invoices!a new electronic invoice!\n" " \n" " \n" -" \"Odoo\"\n" +" \"Odoo\"\n" " \n" " \n" " \n" @@ -2041,7 +2036,6 @@ msgid "Account Number" msgstr "" #. module: account -#: model:account.account,name:account.1_payable #: model:ir.model.fields,field_description:account.field_res_partner__property_account_payable_id #: model:ir.model.fields,field_description:account.field_res_users__property_account_payable_id msgid "Account Payable" @@ -2058,17 +2052,11 @@ msgid "Account Properties" msgstr "" #. module: account -#: model:account.account,name:account.1_receivable #: model:ir.model.fields,field_description:account.field_res_partner__property_account_receivable_id #: model:ir.model.fields,field_description:account.field_res_users__property_account_receivable_id msgid "Account Receivable" msgstr "" -#. module: account -#: model:account.account,name:account.1_pos_receivable -msgid "Account Receivable (PoS)" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__is_account_reconcile msgid "Account Reconcile" @@ -2352,6 +2340,7 @@ msgid "Accounts Mapping of Fiscal Position" msgstr "" #. module: account +#: model:account.account,name:account.1_payable #: model:account.account,name:account.2_account_account_us_payable msgid "Accounts Payable" msgstr "" @@ -2368,11 +2357,13 @@ msgid "Accounts Prefixes" msgstr "" #. module: account +#: model:account.account,name:account.1_receivable #: model:account.account,name:account.2_account_account_us_receivable msgid "Accounts Receivable" msgstr "" #. module: account +#: model:account.account,name:account.1_pos_receivable #: model:account.account,name:account.2_account_account_us_pos_receivable msgid "Accounts Receivable (PoS)" msgstr "" @@ -2398,8 +2389,8 @@ msgstr "" #. odoo-python #: code:addons/account/wizard/accrued_orders.py:0 msgid "" -"Accrual entry created on %(date)s: %(accrual_entry)s. And" -" its reverse entry: %(reverse_entry)s." +"Accrual entry created on %(date)s: %(accrual_entry)s. And its" +" reverse entry: %(reverse_entry)s." msgstr "" #. module: account @@ -2510,6 +2501,17 @@ msgstr "" msgid "Activity Exception Decoration" msgstr "" +#. module: account +#: model:ir.model.fields,field_description:account.field_account_account__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_account_bank_statement_line__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_account_journal__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_account_move__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_account_payment__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__activity_plans_ids +#: model:ir.model.fields,field_description:account.field_res_partner_bank__activity_plans_ids +msgid "Activity Plans" +msgstr "" + #. module: account #: model:ir.model.fields,field_description:account.field_account_account__activity_state #: model:ir.model.fields,field_description:account.field_account_bank_statement_line__activity_state @@ -3138,11 +3140,6 @@ msgstr "" msgid "Analytic Distribution Models" msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_account_report__filter_analytic -msgid "Analytic Filter" -msgstr "" - #. module: account #: model:ir.ui.menu,name:account.menu_action_analytic_lines_tree msgid "Analytic Items" @@ -3160,7 +3157,7 @@ msgstr "" #. module: account #: model:ir.model,name:account.model_account_analytic_applicability -msgid "Analytic Plan's Applicabilities" +msgid "Analytic Plan's Applicability" msgstr "" #. module: account @@ -3514,6 +3511,11 @@ msgstr "" msgid "Automatic Entry Default Journal" msgstr "" +#. module: account +#: model:account.fiscal.position,name:account.1_account_fiscal_position_avatax_us +msgid "Automatic Tax Mapping (AvaTax)" +msgstr "" + #. module: account #: model:ir.model,name:account.model_sequence_mixin msgid "Automatic sequence" @@ -3531,6 +3533,11 @@ msgstr "" msgid "Automation" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_665 +msgid "Automobile Expenses" +msgstr "" + #. module: account #. odoo-python #: code:addons/account/models/account_move.py:0 @@ -3654,8 +3661,8 @@ msgstr "" #. odoo-python #: code:addons/account/models/account_journal.py:0 #: code:addons/account/models/chart_template.py:0 -#: model:account.account,name:account.1_bank_journal_default_account_44 -#: model:account.account,name:account.2_bank_journal_default_account_149 +#: model:account.account,name:account.1_bank_journal_default_account_43 +#: model:account.account,name:account.2_bank_journal_default_account_148 #: model:account.journal,name:account.1_bank #: model:account.journal,name:account.2_bank #: model:ir.model.fields,field_description:account.field_account_journal__bank_id @@ -3682,6 +3689,7 @@ msgid "Bank & Cash accounts cannot be shared between companies." msgstr "" #. module: account +#: model:ir.model,name:account.model_res_partner_bank #: model:ir.model.fields,field_description:account.field_account_journal__bank_account_id #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form msgid "Bank Account" @@ -3733,7 +3741,6 @@ msgstr "" #. module: account #: model:ir.actions.act_window,name:account.action_account_supplier_accounts -#: model:ir.model,name:account.model_res_partner_bank msgid "Bank Accounts" msgstr "" @@ -3778,6 +3785,11 @@ msgstr "" msgid "Bank Reconciliation Move preset" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_650 +msgid "Bank Service Charges" +msgstr "" + #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view msgid "Bank Setup" @@ -3930,11 +3942,6 @@ msgstr "" msgid "Based on Payment" msgstr "" -#. module: account -#: model:res.groups,name:account.group_account_basic -msgid "Basic" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_company__batch_payment_sequence_id msgid "Batch Payment Sequence" @@ -3945,6 +3952,22 @@ msgstr "" msgid "Batch Payments" msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/wizard/account_move_send_batch_wizard.py:0 +msgid "" +"Batch invoice sending is unavailable. Please, activate the cron to enable " +"batch sending of invoices." +msgstr "" + +#. module: account +#. odoo-python +#: code:addons/account/wizard/account_move_send_batch_wizard.py:0 +msgid "" +"Batch invoice sending is unavailable. Please, contact your system " +"administrator to activate the cron to enable batch sending of invoices." +msgstr "" + #. module: account #. odoo-javascript #: code:addons/account/static/src/components/account_resequence/account_resequence.xml:0 @@ -4075,6 +4098,11 @@ msgstr "" msgid "Body content is the same as the template" msgstr "" +#. module: account +#: model:res.groups,name:account.group_account_user +msgid "Bookkeeper" +msgstr "" + #. module: account #: model:ir.model.fields.selection,name:account.selection__account_report_column__figure_type__boolean #: model:ir.model.fields.selection,name:account.selection__account_report_expression__figure_type__boolean @@ -4109,6 +4137,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_buildings +#: model:account.asset,name:account.2_account_asset_us_buildings msgid "Buildings" msgstr "" @@ -4344,7 +4373,6 @@ msgstr "" #. module: account #. odoo-python #: code:addons/account/models/account_journal.py:0 -#: model:account.account,name:account.1_cash_journal_default_account_160 #: model:ir.model.fields.selection,name:account.selection__account_journal__type__cash #: model_terms:ir.ui.view,arch_db:account.view_account_move_filter #: model_terms:ir.ui.view,arch_db:account.view_account_move_line_filter @@ -4356,16 +4384,6 @@ msgstr "" msgid "Cash Account" msgstr "" -#. module: account -#: model:account.account,name:account.1_cash_journal_default_account_157 -msgid "Cash Bakery" -msgstr "" - -#. module: account -#: model:account.account,name:account.1_cash_journal_default_account_159 -msgid "Cash Bar" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__tax_exigibility msgid "Cash Basis" @@ -4403,11 +4421,6 @@ msgstr "" msgid "Cash Basis Transition Account" msgstr "" -#. module: account -#: model:account.account,name:account.1_cash_journal_default_account_156 -msgid "Cash Clothes Shop" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_company__default_cash_difference_expense_account_id msgid "Cash Difference Expense" @@ -4465,22 +4478,12 @@ msgstr "" msgid "Cash Discount Write-Off Loss Account" msgstr "" -#. module: account -#: model:account.account,name:account.1_cash_journal_default_account_155 -msgid "Cash Furn. Shop" -msgstr "" - #. module: account #: model:ir.actions.act_window,name:account.action_view_bank_statement_tree #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view msgid "Cash Registers" msgstr "" -#. module: account -#: model:account.account,name:account.1_cash_journal_default_account_158 -msgid "Cash Restaurant" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__group_cash_rounding msgid "Cash Rounding" @@ -4715,6 +4718,7 @@ msgstr "" #: model:ir.model.fields,field_description:account.field_account_payment__payment_method_code #: model:ir.model.fields,field_description:account.field_account_payment_method__code #: model:ir.model.fields,field_description:account.field_account_payment_method_line__code +#: model:ir.model.fields,field_description:account.field_account_payment_register__payment_method_code #: model:ir.model.fields,field_description:account.field_account_report_line__code #: model_terms:ir.ui.view,arch_db:account.view_account_form #: model_terms:ir.ui.view,arch_db:account.view_account_list @@ -4815,7 +4819,6 @@ msgid "Communication history" msgstr "" #. module: account -#: model:ir.model,name:account.model_res_company #: model:ir.model.fields,field_description:account.field_account_account__company_ids #: model:ir.model.fields,field_description:account.field_account_merge_wizard_line__company_ids msgid "Companies" @@ -4828,6 +4831,7 @@ msgid "Companies that refers to partner" msgstr "" #. module: account +#: model:ir.model,name:account.model_res_company #: model:ir.model.fields,field_description:account.field_account_accrued_orders_wizard__company_id #: model:ir.model.fields,field_description:account.field_account_automatic_entry_wizard__company_id #: model:ir.model.fields,field_description:account.field_account_bank_statement__company_id @@ -4908,11 +4912,6 @@ msgstr "" msgid "Company Fiscal Country Code" msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_res_company__company_registry_placeholder -msgid "Company Registry Placeholder" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__is_storno msgid "Company Storno Accounting" @@ -5047,6 +5046,14 @@ msgstr "" msgid "Consider paying the %(btn_start)sfull amount%(btn_end)s." msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/wizard/account_payment_register.py:0 +msgid "" +"Consider paying the amount with %(btn_start)searly payment " +"discount%(btn_end)s instead." +msgstr "" + #. module: account #: model:account.account,description:account.2_account_account_us_buildings msgid "Construction of buildings and costs associated to buildings" @@ -5104,6 +5111,7 @@ msgstr "" #. module: account #: model:account.account,name:account.1_cost_of_goods_sold #: model:account.account,name:account.2_account_account_us_cost_of_goods_sold +#: model:account.group,name:account.2_us_group_5 #: model:ir.model.fields.selection,name:account.selection__account_move_line__display_type__cogs msgid "Cost of Goods Sold" msgstr "" @@ -5255,11 +5263,6 @@ msgstr "" msgid "Create Payments" msgstr "" -#. module: account -#: model:ir.model,website_form_label:account.model_res_partner -msgid "Create a Customer" -msgstr "" - #. module: account #. odoo-python #: code:addons/account/wizard/account_move_send_wizard.py:0 @@ -6269,6 +6272,12 @@ msgstr "" msgid "Default taxes used when selling the product" msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/models/account_move.py:0 +msgid "Default value for 'company_id' for %(record)s is not an integer" +msgstr "" + #. module: account #: model:account.account,name:account.1_deferred_revenue #: model:account.account,name:account.2_account_account_us_deferred_revenue @@ -7097,11 +7106,6 @@ msgid "" "between the standard price and the bill price." msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_reports -msgid "Dynamic Reports" -msgstr "" - #. module: account #: model:ir.model.fields,help:account.field_account_tax_repartition_line__tag_ids_domain msgid "Dynamic domain used for the tag that can be set on tax" @@ -7231,7 +7235,7 @@ msgstr "" #. module: account #: model:ir.model,name:account.model_mail_template -msgid "Email Templates" +msgid "Email Template" msgstr "" #. module: account @@ -7623,6 +7627,12 @@ msgstr "" msgid "Expired" msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/models/account_move.py:0 +msgid "Export ZIP" +msgstr "" + #. module: account #: model:ir.model.fields,field_description:account.field_account_report_column__expression_label msgid "Expression Label" @@ -7938,10 +7948,6 @@ msgstr "" #. module: account #: model:account.account,name:account.1_fixed_assets -msgid "Fixed Asset" -msgstr "" - -#. module: account #: model:ir.model.fields.selection,name:account.selection__account_account__account_type__asset_fixed #: model_terms:ir.ui.view,arch_db:account.view_account_search msgid "Fixed Assets" @@ -8266,12 +8272,16 @@ msgid "Full access, including configuration rights." msgstr "" #. module: account +#. odoo-python +#: code:addons/account/models/chart_template.py:0 +#: model:account.account,name:account.1_transfer_account_id #: model:account.account,name:account.2_transfer_account_id msgid "Funds in Transit" msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_furniture +#: model:account.asset,name:account.2_account_asset_us_furniture msgid "Furniture & Fixtures" msgstr "" @@ -8327,7 +8337,7 @@ msgstr "" #. module: account #. odoo-python #: code:addons/account/models/template_generic_coa.py:0 -msgid "Generic Chart of Accounts" +msgid "Generic (Minimal) Chart of Accounts" msgstr "" #. module: account @@ -8352,6 +8362,12 @@ msgstr "" msgid "Global Lock Date" msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/wizard/account_move_send_batch_wizard.py:0 +msgid "Go to cron configuration" +msgstr "" + #. module: account #. odoo-python #: code:addons/account/models/account_move.py:0 @@ -8476,6 +8492,12 @@ msgstr "" msgid "Has Iban Warning" msgstr "" +#. module: account +#: model:ir.model.fields,field_description:account.field_account_bank_statement__journal_has_invalid_statements +#: model:ir.model.fields,field_description:account.field_account_journal__has_invalid_statements +msgid "Has Invalid Statements" +msgstr "" + #. module: account #: model:ir.model.fields,field_description:account.field_account_account__has_message #: model:ir.model.fields,field_description:account.field_account_bank_statement_line__has_message @@ -9026,6 +9048,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_improvements +#: model:account.asset,name:account.2_account_asset_us_improvements msgid "Improvements" msgstr "" @@ -9242,6 +9265,11 @@ msgstr "" msgid "Installments Switch Html" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_655 +msgid "Insurance Expenses" +msgstr "" + #. module: account #: model:account.account,description:account.2_account_account_us_professional_liability_insurance msgid "Insurance for damage caused to third parties" @@ -9390,10 +9418,8 @@ msgid "" msgstr "" #. module: account -#. odoo-python -#: code:addons/account/models/account_report.py:0 -msgid "" -"Invalid domain for expression '%(label)s' of line '%(line)s': %(formula)s" +#: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view +msgid "Invalid Statement(s)" msgstr "" #. module: account @@ -9403,9 +9429,15 @@ msgid "Invalid fiscal year last day" msgstr "" #. module: account +#. odoo-python +#: code:addons/account/models/account_report.py:0 +msgid "" +"Invalid formula for expression '%(label)s' of line '%(line)s': %(formula)s" +msgstr "" + +#. module: account +#: model:account.account,name:account.1_stock_valuation #: model:account.account,name:account.2_account_account_us_inventory_valuation -#: model:account.journal,name:account.1_inventory_valuation -#: model:account.journal,name:account.2_inventory_valuation msgid "Inventory Valuation" msgstr "" @@ -9677,6 +9709,7 @@ msgstr "" #. module: account #. odoo-python +#: code:addons/account/controllers/download_docs.py:0 #: code:addons/account/controllers/portal.py:0 #: model:ir.actions.act_window,name:account.action_move_out_invoice #: model:ir.actions.act_window,name:account.action_move_out_invoice_type @@ -9724,7 +9757,7 @@ msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 msgid "Invoices in error" msgstr "" @@ -9741,13 +9774,13 @@ msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 msgid "Invoices sent" msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 msgid "Invoices sent successfully." msgstr "" @@ -9783,6 +9816,11 @@ msgstr "" msgid "Invoicing" msgstr "" +#. module: account +#: model:res.groups,name:account.group_account_basic +msgid "Invoicing & Banks" +msgstr "" + #. module: account #: model:ir.model.fields.selection,name:account.selection__account_invoice_report__payment_state__invoicing_legacy #: model:ir.model.fields.selection,name:account.selection__account_move__payment_state__invoicing_legacy @@ -10598,13 +10636,6 @@ msgstr "" msgid "Liquidity" msgstr "" -#. module: account -#. odoo-python -#: code:addons/account/models/chart_template.py:0 -#: model:account.account,name:account.1_transfer_account_id -msgid "Liquidity Transfer" -msgstr "" - #. module: account #: model:ir.model.fields,help:account.field_account_tax__original_tax_ids msgid "" @@ -10622,6 +10653,11 @@ msgstr "" msgid "Loan Interest Expense" msgstr "" +#. module: account +#: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_loan_extract +msgid "Loans Digitization" +msgstr "" + #. module: account #: model:account.account,description:account.2_account_account_us_property_tax msgid "Local taxes that have to be paid due to the ownership of property" @@ -10705,6 +10741,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_machines +#: model:account.asset,name:account.2_account_asset_us_machines msgid "Machines & Tools" msgstr "" @@ -10876,6 +10913,11 @@ msgstr "" msgid "Mark as fully paid" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_67 +msgid "Marketing Expenses" +msgstr "" + #. module: account #: model:ir.model.fields.selection,name:account.selection__account_reconcile_model__match_label__match_regex msgid "Match Regex" @@ -11376,17 +11418,6 @@ msgstr "" msgid "Next Activity" msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_account_account__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_account_bank_statement_line__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_account_journal__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_account_move__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_account_payment__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__activity_calendar_event_id -#: model:ir.model.fields,field_description:account.field_res_partner_bank__activity_calendar_event_id -msgid "Next Activity Calendar Event" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_account_account__activity_date_deadline #: model:ir.model.fields,field_description:account.field_account_bank_statement_line__activity_date_deadline @@ -11811,6 +11842,11 @@ msgstr "" msgid "Off-Balance Sheet" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_651 +msgid "Office Expenses" +msgstr "" + #. module: account #: model:account.account.tag,name:account.demo_office_furniture_account msgid "Office Furniture" @@ -11903,7 +11939,7 @@ msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 msgid "One or more invoices couldn't be processed." msgstr "" @@ -11991,7 +12027,7 @@ msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 #: model:ir.model.fields.selection,name:account.selection__account_invoice_report__state__posted msgid "Open" msgstr "" @@ -12054,6 +12090,11 @@ msgstr "" msgid "Operating Activities" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_6 +msgid "Operating Expenses" +msgstr "" + #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_reconcile_model_form msgid "Operation Templates" @@ -12155,6 +12196,7 @@ msgid "Other Employees Benefits" msgstr "" #. module: account +#: model:account.group,name:account.2_us_group_7 #: model:ir.model.fields.selection,name:account.selection__account_account__account_type__expense_other msgid "Other Expenses" msgstr "" @@ -12173,6 +12215,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_other_property +#: model:account.asset,name:account.2_account_asset_us_other_property msgid "Other property" msgstr "" @@ -12447,12 +12490,6 @@ msgstr "" msgid "Partner" msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_res_partner__partner_company_registry_placeholder -#: model:ir.model.fields,field_description:account.field_res_users__partner_company_registry_placeholder -msgid "Partner Company Registry Placeholder" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_partner__contract_ids #: model:ir.model.fields,field_description:account.field_res_users__contract_ids @@ -12563,6 +12600,7 @@ msgstr "" #. odoo-python #: code:addons/account/models/account_move.py:0 #: code:addons/account/models/account_payment.py:0 +#: model:ir.model,name:account.model_account_payment #: model:ir.model.fields,field_description:account.field_account_bank_statement_line__origin_payment_id #: model:ir.model.fields,field_description:account.field_account_move__origin_payment_id #: model_terms:ir.ui.view,arch_db:account.view_account_payment_search @@ -12627,6 +12665,7 @@ msgid "Payment Items" msgstr "" #. module: account +#: model:ir.model,name:account.model_account_payment_method #: model:ir.model.fields,field_description:account.field_account_payment__payment_method_line_id #: model:ir.model.fields,field_description:account.field_account_payment_method_line__payment_method_id #: model:ir.model.fields,field_description:account.field_account_payment_register__payment_method_line_id @@ -12638,6 +12677,7 @@ msgid "Payment Method" msgstr "" #. module: account +#: model:ir.model,name:account.model_account_payment_method_line #: model_terms:ir.ui.view,arch_db:account.view_account_payment_search msgid "Payment Method Line" msgstr "" @@ -12653,8 +12693,6 @@ msgid "Payment Method:" msgstr "" #. module: account -#: model:ir.model,name:account.model_account_payment_method -#: model:ir.model,name:account.model_account_payment_method_line #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form msgid "Payment Methods" msgstr "" @@ -12809,7 +12847,6 @@ msgstr "" #: code:addons/account/models/account_move.py:0 #: code:addons/account/wizard/account_payment_register.py:0 #: model:ir.actions.act_window,name:account.action_account_all_payments -#: model:ir.model,name:account.model_account_payment #: model:ir.model.fields,field_description:account.field_account_move__payment_ids #: model:ir.ui.menu,name:account.menu_action_account_payments_payable #: model:ir.ui.menu,name:account.menu_action_account_payments_receivable @@ -12834,6 +12871,17 @@ msgid "" "Payments related to partners with no bank account specified will be skipped." msgstr "" +#. module: account +#: model:ir.model.fields,help:account.field_account_bank_statement_line__reconciled_payment_ids +#: model:ir.model.fields,help:account.field_account_move__reconciled_payment_ids +msgid "Payments that have been reconciled with this invoice." +msgstr "" + +#. module: account +#: model:account.group,name:account.2_us_group_61 +msgid "Payroll Expenses" +msgstr "" + #. module: account #: model:account.account,name:account.2_account_account_us_payroll_tax msgid "Payroll Tax" @@ -13084,13 +13132,12 @@ msgid "" msgstr "" #. module: account -#: model:account.account,name:account.1_prepaid_expenses +#: model:account.account,name:account.1_prepayments #: model:account.account,name:account.2_account_account_us_prepaid_expenses msgid "Prepaid Expenses" msgstr "" #. module: account -#: model:account.account,name:account.1_prepayments #: model:account.account,name:account.2_account_account_us_prepayments #: model:ir.model.fields.selection,name:account.selection__account_account__account_type__asset_prepayments msgid "Prepayments" @@ -13271,6 +13318,11 @@ msgstr "" msgid "Professional %" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_62 +msgid "Professional Fees" +msgstr "" + #. module: account #: model:account.account,name:account.2_account_account_us_professional_insurance msgid "Professional Insurance" @@ -13492,20 +13544,6 @@ msgstr "" msgid "RD Expenses" msgstr "" -#. module: account -#: model:ir.model.fields,field_description:account.field_account_account__rating_ids -#: model:ir.model.fields,field_description:account.field_account_bank_statement_line__rating_ids -#: model:ir.model.fields,field_description:account.field_account_journal__rating_ids -#: model:ir.model.fields,field_description:account.field_account_move__rating_ids -#: model:ir.model.fields,field_description:account.field_account_payment__rating_ids -#: model:ir.model.fields,field_description:account.field_account_reconcile_model__rating_ids -#: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__rating_ids -#: model:ir.model.fields,field_description:account.field_account_tax__rating_ids -#: model:ir.model.fields,field_description:account.field_res_company__rating_ids -#: model:ir.model.fields,field_description:account.field_res_partner_bank__rating_ids -msgid "Ratings" -msgstr "" - #. module: account #: model:account.account,name:account.2_account_account_us_raw_materials msgid "Raw Materials" @@ -13516,6 +13554,11 @@ msgstr "" msgid "Re-Sequence" msgstr "" +#. module: account +#: model:res.groups,name:account.group_account_readonly +msgid "Read-only" +msgstr "" + #. module: account #: model:ir.model.fields,field_description:account.field_account_lock_exception__reason msgid "Reason" @@ -13625,6 +13668,12 @@ msgstr "" msgid "Reconciled Lines Excluding Exchange Diff" msgstr "" +#. module: account +#: model:ir.model.fields,field_description:account.field_account_bank_statement_line__reconciled_payment_ids +#: model:ir.model.fields,field_description:account.field_account_move__reconciled_payment_ids +msgid "Reconciled Payments" +msgstr "" + #. module: account #: model:ir.model.fields,field_description:account.field_account_payment__reconciled_statement_line_ids msgid "Reconciled Statement Lines" @@ -13689,11 +13738,6 @@ msgid "" " name, etc." msgstr "" -#. module: account -#: model_terms:ir.ui.view,arch_db:account.view_move_form -msgid "Refresh currency rate to the invoice date" -msgstr "" - #. module: account #. odoo-javascript #: code:addons/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js:0 @@ -13915,6 +13959,11 @@ msgstr "" msgid "Resequence" msgstr "" +#. module: account +#: model_terms:ir.ui.view,arch_db:account.view_move_form +msgid "Reset the currency rate to the default accordingly to the invoice date" +msgstr "" + #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_form #: model_terms:ir.ui.view,arch_db:account.view_move_form @@ -13992,6 +14041,7 @@ msgstr "" #. module: account #. odoo-python #: code:addons/account/wizard/accrued_orders.py:0 +#: model:account.group,name:account.2_us_group_4 #: model:ir.model.fields,field_description:account.field_digest_digest__kpi_account_total_revenue #: model:ir.model.fields.selection,name:account.selection__account_automatic_entry_wizard__account_type__income msgid "Revenue" @@ -14586,7 +14636,9 @@ msgid "Selected Payment Method Codes" msgstr "" #. module: account +#: model:ir.model.fields,field_description:account.field_account_bank_statement_line__is_self_billing #: model:ir.model.fields,field_description:account.field_account_journal__is_self_billing +#: model:ir.model.fields,field_description:account.field_account_move__is_self_billing #: model_terms:ir.ui.view,arch_db:account.report_invoice_document msgid "Self Billing" msgstr "" @@ -14723,7 +14775,7 @@ msgstr "" #. module: account #. odoo-python -#: code:addons/account/models/account_move.py:0 +#: code:addons/account/models/account_move_send.py:0 msgid "Sent invoices" msgstr "" @@ -14948,11 +15000,6 @@ msgstr "" msgid "Show Aba Routing" msgstr "" -#. module: account -#: model:res.groups,name:account.group_account_readonly -msgid "Show Accounting Features - Readonly" -msgstr "" - #. module: account #: model:ir.model.fields,field_description:account.field_res_partner__show_credit_limit #: model:ir.model.fields,field_description:account.field_res_users__show_credit_limit @@ -14981,11 +15028,6 @@ msgstr "" msgid "Show E-Invoice Status Buttons" msgstr "" -#. module: account -#: model:res.groups,name:account.group_account_user -msgid "Show Full Accounting Features" -msgstr "" - #. module: account #: model:res.groups,name:account.group_account_secured msgid "Show Inalterability Features" @@ -15316,11 +15358,6 @@ msgstr "" msgid "Step completed!" msgstr "" -#. module: account -#: model:account.account,name:account.1_stock_valuation -msgid "Stock Valuation" -msgstr "" - #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Storno Accounting" @@ -15899,6 +15936,7 @@ msgstr "" #. module: account #. odoo-python #: code:addons/account/models/account_account.py:0 +#: model:account.group,name:account.2_us_group_8 #: model:ir.actions.act_window,name:account.action_tax_form #: model:ir.model.fields,field_description:account.field_account_fiscal_position__tax_ids #: model:ir.model.fields,field_description:account.field_account_move_line__tax_ids @@ -16019,6 +16057,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_technology +#: model:account.asset,name:account.2_account_asset_us_technology msgid "Technology" msgstr "" @@ -17289,7 +17328,9 @@ msgid "This journal entry has been secured." msgstr "" #. module: account +#: model:ir.model.fields,help:account.field_account_bank_statement_line__is_self_billing #: model:ir.model.fields,help:account.field_account_journal__is_self_billing +#: model:ir.model.fields,help:account.field_account_move__is_self_billing msgid "" "This journal is for self-billing invoices. Invoices will be created using a " "different sequence per partner." @@ -17748,6 +17789,11 @@ msgstr "" msgid "Transfer to %s" msgstr "" +#. module: account +#: model:account.group,name:account.2_us_group_661 +msgid "Travel Expenses" +msgstr "" + #. module: account #: model:account.account,description:account.2_account_account_us_public_transportation msgid "Travel expenses done using public transit (metro, bus, tramways, ...)" @@ -17774,6 +17820,10 @@ msgid "True" msgstr "" #. module: account +#. odoo-javascript +#: code:addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml:0 +#: code:addons/account/static/src/components/manyone_banks/many2one_banks.js:0 +#: code:addons/account/static/src/components/manyone_banks/many2one_banks.xml:0 #: model_terms:ir.ui.view,arch_db:account.view_partner_bank_search_inherit msgid "Trusted" msgstr "" @@ -17998,6 +18048,10 @@ msgid "Untaxed amount" msgstr "" #. module: account +#. odoo-javascript +#: code:addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml:0 +#: code:addons/account/static/src/components/manyone_banks/many2one_banks.js:0 +#: code:addons/account/static/src/components/manyone_banks/many2one_banks.xml:0 #: model_terms:ir.ui.view,arch_db:account.view_partner_bank_search_inherit msgid "Untrusted" msgstr "" @@ -18224,6 +18278,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_utilities +#: model:account.group,name:account.2_us_group_64 msgid "Utilities" msgstr "" @@ -18336,6 +18391,7 @@ msgstr "" #. module: account #: model:account.account,name:account.2_account_account_us_vehicles #: model:account.account,name:account.2_account_account_us_vehicles_expense +#: model:account.asset,name:account.2_account_asset_us_vehicles msgid "Vehicles" msgstr "" @@ -18475,12 +18531,6 @@ msgid "" "vendor ?" msgstr "" -#. module: account -#. odoo-javascript -#: code:addons/account/static/src/components/json_checkboxes/json_checkboxes.xml:0 -msgid "Warning" -msgstr "" - #. module: account #. odoo-python #: code:addons/account/models/account_move.py:0 @@ -18697,6 +18747,14 @@ msgid "" "3/ select them all and post or delete them through the action menu" msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/models/account_bank_statement_line.py:0 +msgid "" +"You can not delete a transaction from a valid statement.\n" +"If you want to delete it, please remove the statement first." +msgstr "" + #. module: account #. odoo-python #: code:addons/account/models/account_move_line.py:0 @@ -19397,7 +19455,20 @@ msgstr "" #. module: account #. odoo-python #: code:addons/account/models/account_move.py:0 -msgid "[Partner name]" +msgid "[Partner id]" +msgstr "" + +#. module: account +#: model:res.groups,comment:account.group_account_user +msgid "" +"access to all Accounting features, including reporting, asset management, " +"analytic accounting, without configuration rights." +msgstr "" + +#. module: account +#: model:res.groups,comment:account.group_account_readonly +msgid "" +"access to all the accounting data but in readonly mode, no actions allowed." msgstr "" #. module: account @@ -19416,6 +19487,11 @@ msgstr "" msgid "activate the currency of the invoice" msgstr "" +#. module: account +#: model:res.groups,comment:account.group_account_basic +msgid "adds the accounting dashboard, bank management and follow-up reports." +msgstr "" + #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form msgid "alias" @@ -19824,12 +19900,6 @@ msgstr "" msgid "to create the taxes for this country." msgstr "" -#. module: account -#. odoo-python -#: code:addons/account/models/res_partner_bank.py:0 -msgid "trusted" -msgstr "" - #. module: account #. odoo-javascript #: code:addons/account/static/src/components/bill_guide/bill_guide.xml:0 @@ -19855,12 +19925,6 @@ msgstr "" msgid "until" msgstr "" -#. module: account -#. odoo-python -#: code:addons/account/models/res_partner_bank.py:0 -msgid "untrusted" -msgstr "" - #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_register_form msgid "untrusted bank accounts" diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 29216c839476a..14c169207ef42 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -27,8 +27,8 @@ class AccountBankStatement(models.Model): ) date = fields.Date( - compute='_compute_date_index', store=True, - index=True, readonly=False + compute='_compute_date', store=True, + index=True, readonly=False, ) # The internal index of the first line of a statement, it is used for sorting the statements @@ -36,7 +36,7 @@ class AccountBankStatement(models.Model): # keeping this order is important because the validity of the statements are based on their order first_line_index = fields.Char( comodel_name='account.bank.statement.line', - compute='_compute_date_index', store=True, + compute='_compute_first_line_index', store=True, ) balance_start = fields.Monetary( @@ -94,6 +94,10 @@ class AccountBankStatement(models.Model): search='_search_is_valid', ) + journal_has_invalid_statements = fields.Boolean( + related='journal_id.has_invalid_statements', + ) + problem_description = fields.Text( compute='_compute_problem_description', ) @@ -120,12 +124,18 @@ def _compute_name(self): stmt.name = name +_("Statement %(date)s", date=stmt.date or fields.Date.to_date(stmt.create_date)) @api.depends('line_ids.internal_index', 'line_ids.state') - def _compute_date_index(self): + def _compute_first_line_index(self): for stmt in self: # When we create lines manually from the form view, they don't have any `internal_index` set yet. sorted_lines = stmt.line_ids.filtered("internal_index").sorted('internal_index') stmt.first_line_index = sorted_lines[:1].internal_index - stmt.date = sorted_lines.filtered(lambda l: l.state == 'posted')[-1:].date + + @api.depends('line_ids.internal_index', 'line_ids.state') + def _compute_date(self): + for statement in self: + # When we create lines manually from the form view, they don't have any `internal_index` set yet. + sorted_lines = statement.line_ids.filtered('internal_index').sorted('internal_index') + statement.date = sorted_lines.filtered(lambda l: l.state == 'posted')[-1:].date @api.depends('create_date') def _compute_balance_start(self): @@ -236,21 +246,26 @@ def _get_invalid_statement_ids(self, all_statements=None): self.env['account.bank.statement'].flush_model(['balance_start', 'balance_end_real', 'first_line_index']) self.env.cr.execute(f""" - SELECT st.id - FROM account_bank_statement st - LEFT JOIN res_company co ON st.company_id = co.id - LEFT JOIN account_journal j ON st.journal_id = j.id - LEFT JOIN res_currency currency ON COALESCE(j.currency_id, co.currency_id) = currency.id, - LATERAL ( - SELECT balance_end_real - FROM account_bank_statement st_lookup - WHERE st_lookup.first_line_index < st.first_line_index - AND st_lookup.journal_id = st.journal_id - ORDER BY st_lookup.first_line_index desc - LIMIT 1 - ) prev - WHERE ROUND(prev.balance_end_real, currency.decimal_places) != ROUND(st.balance_start, currency.decimal_places) - {"" if all_statements else "AND st.id IN %(ids)s"} + WITH statements AS ( + SELECT st.id, + st.balance_start, + st.journal_id, + LAG(st.balance_end_real) OVER ( + PARTITION BY st.journal_id + ORDER BY st.first_line_index + ) AS prev_balance_end_real, + currency.decimal_places + FROM account_bank_statement st + LEFT JOIN res_company co ON st.company_id = co.id + LEFT JOIN account_journal j ON st.journal_id = j.id + LEFT JOIN res_currency currency ON COALESCE(j.currency_id, co.currency_id) = currency.id + WHERE st.first_line_index IS NOT NULL + {"" if all_statements else "AND st.id IN %(ids)s"} + ) + SELECT id + FROM statements + WHERE prev_balance_end_real IS NOT NULL + AND ROUND(prev_balance_end_real, decimal_places) != ROUND(balance_start, decimal_places); """, { 'ids': tuple(self.ids) }) diff --git a/addons/account/models/account_bank_statement_line.py b/addons/account/models/account_bank_statement_line.py index 232c55252419d..220f5286618ec 100644 --- a/addons/account/models/account_bank_statement_line.py +++ b/addons/account/models/account_bank_statement_line.py @@ -279,7 +279,7 @@ def _compute_internal_index(self): f'{st_line._origin.id:0>10}' @api.depends('journal_id', 'currency_id', 'amount', 'foreign_currency_id', 'amount_currency', - 'move_id.checked', + 'move_id.review_state', 'move_id.line_ids.account_id', 'move_id.line_ids.amount_currency', 'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.currency_id', 'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids') @@ -292,7 +292,7 @@ def _compute_is_reconciled(self): _liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines() # Compute residual amount - if not st_line.checked: + if st_line.review_state in ('todo', 'anomaly'): st_line.amount_residual = -st_line.amount_currency if st_line.foreign_currency_id else -st_line.amount elif suspense_lines.account_id.reconcile: st_line.amount_residual = sum(suspense_lines.mapped('amount_residual_currency')) @@ -465,7 +465,7 @@ def action_undo_reconciliation(self): for st_line in self: st_line.with_context(force_delete=True, skip_readonly_check=True).write({ - 'checked': True, + 'review_state': 'reviewed', 'line_ids': [Command.clear()] + [ Command.create(line_vals) for line_vals in st_line._prepare_move_line_default_vals()], }) @@ -474,6 +474,12 @@ def action_undo_reconciliation(self): # HELPERS # ------------------------------------------------------------------------- + @api.ondelete(at_uninstall=False) + def _check_allow_unlink(self): + if self.statement_id.filtered(lambda stmt: stmt.is_valid and stmt.is_complete): + raise UserError(_("You can not delete a transaction from a valid statement.\n" + "If you want to delete it, please remove the statement first.")) + def _find_or_create_bank_account(self): self.ensure_one() diff --git a/addons/account/models/account_document_import_mixin.py b/addons/account/models/account_document_import_mixin.py index 7d7db7c1b8a05..6644a2de27b61 100644 --- a/addons/account/models/account_document_import_mixin.py +++ b/addons/account/models/account_document_import_mixin.py @@ -109,7 +109,7 @@ def split_etree_on_tag(tree, tag): def extract_pdf_embedded_files(filename, content): - with io.BytesIO(content) as buffer: + with io.BytesIO(content or b'') as buffer: try: pdf_reader = OdooPdfFileReader(buffer, strict=False) except Exception as e: # noqa: BLE001 diff --git a/addons/account/models/account_full_reconcile.py b/addons/account/models/account_full_reconcile.py index 2a65f3a6462a8..3b00eea15930a 100644 --- a/addons/account/models/account_full_reconcile.py +++ b/addons/account/models/account_full_reconcile.py @@ -23,23 +23,23 @@ def get_ids(commands): partial_ids = [list(get_ids(vals.pop('partial_reconcile_ids'))) for vals in vals_list] fulls = super(AccountFullReconcile, self.with_context(tracking_disable=True)).create(vals_list) + self.env['account.move.line'].invalidate_model(['full_reconcile_id']) + fulls.invalidate_recordset(['reconciled_line_ids'], flush=False) self.env.cr.execute_values(""" UPDATE account_move_line line SET full_reconcile_id = source.full_id FROM (VALUES %s) AS source(full_id, line_ids) WHERE line.id = ANY(source.line_ids) """, [(full.id, line_ids) for full, line_ids in zip(fulls, move_line_ids)], page_size=1000) - fulls.reconciled_line_ids.invalidate_recordset(['full_reconcile_id'], flush=False) - fulls.invalidate_recordset(['reconciled_line_ids'], flush=False) + self.env['account.partial.reconcile'].invalidate_model(['full_reconcile_id']) + fulls.invalidate_recordset(['partial_reconcile_ids'], flush=False) self.env.cr.execute_values(""" UPDATE account_partial_reconcile partial SET full_reconcile_id = source.full_id FROM (VALUES %s) AS source(full_id, partial_ids) WHERE partial.id = ANY(source.partial_ids) """, [(full.id, line_ids) for full, line_ids in zip(fulls, partial_ids)], page_size=1000) - fulls.partial_reconcile_ids.invalidate_recordset(['full_reconcile_id'], flush=False) - fulls.invalidate_recordset(['partial_reconcile_ids'], flush=False) self.env['account.partial.reconcile']._update_matching_number(fulls.reconciled_line_ids) return fulls diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 1d424b152efe0..049668d1025b0 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -23,7 +23,7 @@ class AccountJournalGroup(models.Model): help="Define which company can select the multi-ledger in report filters. If none is provided, available for all companies", default=lambda self: self.env.company, ) - excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals") + excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", context={'active_test': False}) sequence = fields.Integer(default=10) _uniq_name = models.Constraint( @@ -264,6 +264,7 @@ def _get_default_account_domain(self): ) accounting_date = fields.Date(compute='_compute_accounting_date') display_alias_fields = fields.Boolean(compute='_compute_display_alias_fields') + has_invalid_statements = fields.Boolean(compute='_compute_has_invalid_statements') show_fetch_in_einvoices_button = fields.Boolean( string="Show E-Invoice Buttons", @@ -287,6 +288,16 @@ def _get_default_account_domain(self): 'Journal codes must be unique per company.', ) + def _compute_has_invalid_statements(self): + journals_with_invalid_statements = self.env['account.bank.statement'].search([ + ('journal_id', 'in', self.ids), + '|', + ('is_valid', '=', False), + ('is_complete', '=', False), + ]).journal_id + journals_with_invalid_statements.has_invalid_statements = True + (self - journals_with_invalid_statements).has_invalid_statements = False + def _compute_display_alias_fields(self): self.display_alias_fields = self.env['mail.alias.domain'].search_count([], limit=1) @@ -831,7 +842,7 @@ def _ensure_unique_alias(self, vals, company): domain = [('alias_name', '=', alias_name)] if alias_domain_name: - domain.append(('alias_domain', '=', alias_domain_name)) + domain.extend(['|', ('alias_domain', '=', alias_domain_name), ('alias_domain_id', '=', False)]) existing_alias = self.env['mail.alias'].search_count(domain, limit=1) diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 43d72250f8aa5..e48d99bc12154 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -51,6 +51,7 @@ def _compute_last_bank_statement(self): SELECT id, company_id FROM account_bank_statement WHERE journal_id = journal.id + AND first_line_index IS NOT NULL ORDER BY first_line_index DESC LIMIT 1 ) statement ON TRUE @@ -457,7 +458,7 @@ def _fill_bank_cash_dashboard_data(self, dashboard_data): WHERE st_line.journal_id IN %s AND st_line.company_id IN %s AND st_line.is_reconciled IS NOT TRUE - AND st_line_move.checked IS TRUE + AND (st_line_move.review_state IS NULL OR st_line_move.review_state NOT IN ('todo', 'anomaly')) AND st_line_move.state = 'posted' GROUP BY st_line.journal_id """, [tuple(bank_cash_journals.ids), tuple(self.env.companies.ids)]) @@ -505,7 +506,7 @@ def _fill_bank_cash_dashboard_data(self, dashboard_data): domain=[ ('journal_id', 'in', bank_cash_journals.ids), ('move_id.company_id', 'in', self.env.companies.ids), - ('move_id.checked', '=', False), + ('move_id.review_state', 'in', ('todo', 'anomaly')), ('move_id.state', '=', 'posted'), ], groupby=['journal_id'], @@ -526,6 +527,11 @@ def _fill_bank_cash_dashboard_data(self, dashboard_data): 'image': '/account/static/src/img/bank.svg' if journal.type in ('bank', 'credit') else '/web/static/img/rfq.svg', 'text': _('Drop to import transactions'), } + last_statement_visible = ( + not journal.company_id.fiscalyear_lock_date + or journal.last_statement_id.date + and journal.company_id.fiscalyear_lock_date < journal.last_statement_id.date + ) dashboard_data[journal.id].update({ 'number_to_check': number_to_check, @@ -538,6 +544,8 @@ def _fill_bank_cash_dashboard_data(self, dashboard_data): 'nb_lines_outstanding_pay_account_balance': has_outstanding, 'last_balance': currency.format(journal.last_statement_id.balance_end_real), 'last_statement_id': journal.last_statement_id.id, + 'last_statement_visible': last_statement_visible, + 'has_invalid_statements': journal.has_invalid_statements, 'bank_statements_source': journal.bank_statements_source, 'is_sample_data': journal.has_statement_lines, 'nb_misc_operations': number_misc, @@ -744,7 +752,7 @@ def _get_to_check_payment_query(self): query = self.env['account.move']._search([ *self.env['account.move']._check_company_domain(self.env.companies), ('journal_id', 'in', self.ids), - ('checked', '=', False), + ('review_state', 'in', ('todo', 'anomaly')), ('state', '=', 'posted'), ]) selects = [ @@ -802,6 +810,7 @@ def _get_journal_dashboard_bank_running_balance(self): FROM account_bank_statement WHERE journal_id = journal.id AND company_id = ANY(%s) + AND first_line_index IS NOT NULL ORDER BY date DESC, id DESC LIMIT 1 ) statement ON TRUE @@ -1000,7 +1009,7 @@ def to_check_ids(self): return self.env['account.bank.statement.line'].search([ ('journal_id', '=', self.id), ('move_id.company_id', 'in', self.env.companies.ids), - ('move_id.checked', '=', False), + ('move_id.review_state', 'in', ('todo', 'anomaly')), ('move_id.state', '=', 'posted'), ]) @@ -1119,6 +1128,10 @@ def open_bank_difference_action(self): } return action + def open_invalid_statements_action(self): + self.ensure_one() + return self.env["ir.actions.act_window"]._for_xml_id('account.action_bank_statement_tree') + def _show_sequence_holes(self, domain): return { 'type': 'ir.actions.act_window', diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 4578d70769438..d68321d115f30 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3,6 +3,7 @@ import ast import calendar from collections import Counter, defaultdict +from collections.abc import Mapping from contextlib import ExitStack, contextmanager from datetime import date, timedelta from dateutil.relativedelta import relativedelta @@ -53,6 +54,12 @@ ('blocked', 'Blocked'), ('invoicing_legacy', 'Invoicing App Legacy'), ] +REVIEW_STATE_SELECTION = [ + ('todo', "To Review"), + ('reviewed', "Reviewed"), + ('supervised', "Supervised"), + ('anomaly', "Anomaly"), +] TYPE_REVERSE_MAP = { 'entry': 'entry', @@ -310,10 +317,10 @@ def _sequence_year_range_monthly_regex(self): index='btree_not_null', ) hide_post_button = fields.Boolean(compute='_compute_hide_post_button', readonly=True) - checked = fields.Boolean( - string='Reviewed', - compute='_compute_checked', - store=True, readonly=False, tracking=True, copy=False, + review_state = fields.Selection( + string="Review", + selection=REVIEW_STATE_SELECTION, + tracking=True, copy=False, ) posted_before = fields.Boolean(copy=False) suitable_journal_ids = fields.Many2many( @@ -772,7 +779,7 @@ def _sequence_year_range_monthly_regex(self): display_send_button = fields.Boolean(compute='_compute_display_send_button') highlight_send_button = fields.Boolean(compute='_compute_highlight_send_button') - _checked_idx = models.Index("(journal_id) WHERE (checked IS NOT TRUE)") + _checked_idx = models.Index("(journal_id) WHERE (review_state IN ('todo', 'anomaly'))") _payment_idx = models.Index("(journal_id, state, payment_state, move_type, date)") _unique_name = models.UniqueIndex( "(name, journal_id) WHERE (state = 'posted'AND name != '/')", @@ -1169,6 +1176,15 @@ def _compute_direction_sign(self): 'line_ids.full_reconcile_id', 'state') def _compute_amount(self): + self.line_ids.fetch([ + 'debit', + 'balance', + 'amount_currency', + 'amount_residual', + 'amount_residual_currency', + 'display_type', + 'tax_repartition_line_id' + ]) for move in self: total_untaxed, total_untaxed_currency = 0.0, 0.0 total_tax, total_tax_currency = 0.0, 0.0 @@ -1925,7 +1941,7 @@ def _compute_access_url(self): for move in self.filtered(lambda move: move.is_invoice()): move.access_url = '/my/invoices/%s' % (move.id) - @api.depends('move_type', 'partner_id', 'company_id') + @api.depends('move_type', 'partner_id', 'partner_id.lang', 'company_id') def _compute_narration(self): use_invoice_terms = self.env['ir.config_parameter'].sudo().get_bool('account.use_invoice_terms') invoice_to_update_terms = self.filtered(lambda m: use_invoice_terms and m.is_sale_document(include_receipts=True)) @@ -2344,11 +2360,6 @@ def _search_next_payment_date(self, operator, value): return NotImplemented return [('line_ids', 'any', [('reconciled', '=', False), ('payment_date', operator, value)])] - @api.depends('state', 'journal_id.type') - def _compute_checked(self): - for move in self: - move.checked = move.state == 'posted' and (move.journal_id.type == 'general' or move._is_user_able_to_review()) - @api.depends('line_ids.no_followup') def _compute_no_followup(self): for move in self: @@ -2945,7 +2956,7 @@ def _is_eligible_for_early_payment_discount(self, currency, reference_date): not reference_date or not self.invoice_date or ( - (existing_discount_date := next(iter(payment_terms)).discount_date) + (existing_discount_date := payment_terms[0].discount_date) and reference_date <= existing_discount_date ) @@ -3726,6 +3737,18 @@ def _get_copy_message_content(self, default): """ return _('This entry has been reversed from %s', self._get_html_link()) if default.get('reversed_entry_id') else _('This entry has been duplicated from %s', self._get_html_link()) + def _check_user_access(self, vals_list): + if self.env.su: + return + is_user_able_to_supervise = self.env.user.has_group('account.group_account_manager') + is_user_able_to_review = self.env.user.has_group('account.group_account_user') + for vals in vals_list: + if ( + ((vals.get('review_state') == 'reviewed' or not vals.get('review_state', True)) and not is_user_able_to_review) + or (vals.get('review_state') == 'supervised' and not is_user_able_to_supervise) + ): + raise AccessError(_("You don't have the access rights to perform this action.")) + def _sanitize_vals(self, vals): if vals.get('invoice_line_ids') and vals.get('journal_line_ids'): # values can sometimes be in only one of the two fields, sometimes in @@ -3767,6 +3790,7 @@ def _get_protected_vals(self, vals, records): def create(self, vals_list): if any('state' in vals and vals.get('state') == 'posted' for vals in vals_list): raise UserError(_('You cannot create a move already in the posted state. Please create a draft move and post it after.')) + self._check_user_access(vals_list) container = {'records': self} with self._check_balanced(container): with ExitStack() as exit_stack, self._sync_dynamic_lines(container): @@ -3786,12 +3810,36 @@ def write(self, vals): if not vals: return True self._sanitize_vals(vals) + self._check_user_access([vals]) + + unmodifiable_fields = [ + 'line_ids', 'invoice_line_ids', 'journal_line_ids', + 'date', 'invoice_date', + 'partner_id', 'fiscal_position_id', + 'invoice_payment_term_id', + 'currency_id', + 'invoice_cash_rounding_id', + ] + is_user_able_to_supervise = self.env.user.has_group('account.group_account_manager') + is_user_able_to_review = self.env.user.has_group('account.group_account_user') or is_user_able_to_supervise + move_ids_review_done = [] + move_ids_review_todo = [] for move in self: - if vals.get('checked') and not move._is_user_able_to_review(): - raise AccessError(_("You don't have the access rights to perform this action.")) - if vals.get('state') == 'draft' and move.checked and not move._is_user_able_to_review(): - raise ValidationError(_("Validated entries can only be changed by your accountant.")) + modified_accounting_fields = self._field_will_change_list(move, vals, unmodifiable_fields) + if vals.get('state') == 'draft' and ( + ((move.review_state == 'reviewed' or not move.review_state) and not is_user_able_to_review) + or (move.review_state == 'supervised' and not is_user_able_to_supervise) + ): + raise ValidationError(_("This entry has already been reviewed. You need the bookkeeper role to change it.")) + if (vals.get('state') == 'posted' and move.auto_post == 'no') or vals.get('auto_post', 'no') != 'no': + if is_user_able_to_review: + if move.review_state: + move_ids_review_done.append(move.id) + else: + move_ids_review_todo.append(move.id) + if not is_user_able_to_review and move.review_state in ('reviewed', 'supervised') and modified_accounting_fields: + move_ids_review_todo.append(move.id) violated_fields = set(vals).intersection(move._get_integrity_hash_fields() + ['inalterable_hash']) if move.inalterable_hash and violated_fields: @@ -3830,12 +3878,11 @@ def write(self, vals): # Disallow modifying readonly fields on a posted move move_state = vals.get('state', move.state) - unmodifiable_fields = ( - 'invoice_line_ids', 'line_ids', 'invoice_date', 'date', 'partner_id', - 'invoice_payment_term_id', 'currency_id', 'fiscal_position_id', 'invoice_cash_rounding_id') - readonly_fields = [val for val in vals if val in unmodifiable_fields] - if not self.env.context.get('skip_readonly_check') and move_state == "posted" and readonly_fields: - raise UserError(_("You cannot modify the following readonly fields on a posted move: %s", ', '.join(readonly_fields))) + if not self.env.context.get('skip_readonly_check') and move_state == "posted" and modified_accounting_fields: + raise UserError(_("You cannot modify the following readonly fields on a posted move: %s", ', '.join( + self._fields[fname]._description_string(self.env) + for fname in modified_accounting_fields + ))) if move.journal_id.sequence_override_regex and vals.get('name') and vals['name'] != '/' and not re.match(move.journal_id.sequence_override_regex, vals['name']): if not self.env.user.has_group('account.group_account_manager'): @@ -3873,6 +3920,8 @@ def write(self, vals): if vals.get('state') == 'posted': self.flush_recordset() # Ensure that the name is correctly computed self._hash_moves() + super(AccountMove, self.browse(move_ids_review_done)).write({'review_state': 'reviewed'}) + super(AccountMove, self.browse(move_ids_review_todo)).write({'review_state': 'todo'}) self._synchronize_business_models(set(vals.keys())) @@ -4692,6 +4741,7 @@ def _get_fields_to_copy_recurring_entries(self, values): 'auto_post_until': self.auto_post_until, # same as above 'auto_post_origin_id': self.auto_post_origin_id.id, # same as above 'invoice_user_id': self.invoice_user_id.id, # otherwise user would be OdooBot + 'review_state': self.review_state, }) if self.invoice_date: values.update({'invoice_date': self._apply_delta_recurring_entries(self.invoice_date, self.auto_post_origin_id.invoice_date, self.auto_post)}) @@ -4848,7 +4898,7 @@ def tax_details_grouping_function(base_line, tax_data): for grouping_key, values in aggregated_values.items(): if not grouping_key: continue - if isinstance(grouping_key, dict): + if isinstance(grouping_key, Mapping): values.update(grouping_key) tax_details[grouping_key] = values @@ -4857,7 +4907,7 @@ def tax_details_grouping_function(base_line, tax_data): for grouping_key, values in values_per_grouping_key.items(): if not grouping_key: continue - if isinstance(grouping_key, dict): + if isinstance(grouping_key, Mapping): values.update(grouping_key) tax_details[grouping_key] = values @@ -5362,7 +5412,7 @@ def _unlink_or_reverse(self): to_unlink += move to_unlink.filtered(lambda m: m.state in ('posted', 'cancel')).button_draft() to_unlink.filtered(lambda m: m.state == 'draft').unlink() - to_cancel.button_cancel() + to_cancel.filtered(lambda m: m.state != 'cancel').button_cancel() return to_reverse._reverse_moves(cancel=True) def _post(self, soft=True): @@ -5910,15 +5960,12 @@ def js_remove_outstanding_partial(self, partial_id): partial = self.env['account.partial.reconcile'].browse(partial_id) return partial.unlink() - def button_set_checked(self): - self.set_moves_checked() - def check_selected_moves(self): self.env['account.move'].browse(self.env.context.get('active_ids', [])).set_moves_checked() def set_moves_checked(self, is_checked=True): for move in self.filtered(lambda m: m.state == 'posted'): - move.checked = is_checked + move.review_state = 'reviewed' if is_checked else 'todo' def button_draft(self): if any(move.state not in ('cancel', 'posted') for move in self): @@ -6609,10 +6656,6 @@ def _get_available_invoice_template_pdf_report_ids(self): return available_reports - def _is_user_able_to_review(self): - # If only account is installed, we don't check user access rights. - return True - # ------------------------------------------------------------------------- # TOOLING # ------------------------------------------------------------------------- @@ -6646,6 +6689,13 @@ def _cleanup_write_orm_values(self, record, vals): del cleaned_vals[field_name] return cleaned_vals + def _field_will_change_list(self, record, vals, fname_list): + return [ + fname + for fname in fname_list + if self._field_will_change(record, vals, fname) + ] + @contextmanager def _disable_recursion(self, container, key, default=None, target=True): """Apply the context key to all environments inside this context manager. diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 386531765ca20..75f78e06feaf2 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -418,6 +418,7 @@ class AccountMoveLine(models.Model): analytic_distribution = fields.Json( inverse="_inverse_analytic_distribution", ) # add the inverse function used to trigger the creation/update of the analytic lines accordingly (field originally defined in the analytic mixin) + has_invalid_analytics = fields.Boolean(compute='_compute_has_invalid_analytics') # === Early Pay fields === # discount_date = fields.Date( @@ -1646,6 +1647,7 @@ def create(self, vals_list): move_container = {'records': moves} with moves._check_balanced(move_container),\ ExitStack() as exit_stack,\ + self.env.protecting(self.env['account.move']._get_protected_vals({}, moves)), \ moves._sync_dynamic_lines(move_container),\ self._sync_invoice(container): lines = super().create([self._sanitize_vals(vals) for vals in vals_list]) @@ -1904,6 +1906,31 @@ def _compute_display_name(self): for line in self: line.display_name = line._format_aml_name(line.name or line.product_id.display_name, line.ref, line.move_id.name) + @api.depends('account_id', 'company_id', 'move_id', 'product_id', 'display_type', 'analytic_distribution') + def _compute_has_invalid_analytics(self): + SKIPPED_ACCOUNT_TYPES = {'asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card'} + lines_to_validate = self.filtered(lambda line: ( + line.display_type == 'product' and + line.account_id.account_type not in SKIPPED_ACCOUNT_TYPES + )) + (self - lines_to_validate).has_invalid_analytics = False + for line in lines_to_validate: + line.has_invalid_analytics = False + try: + business_domain = ( + 'invoice' if line.move_id.is_sale_document(True) + else 'bill' if line.move_id.is_purchase_document(True) + else 'general' + ) + line.with_context(validate_analytic=True)._validate_distribution( + company_id=line.company_id.id, + product=line.product_id.id, + account=line.account_id.id, + business_domain=business_domain, + ) + except ValidationError: + line.has_invalid_analytics = True + def copy_data(self, default=None): vals_list = super().copy_data(default=default) diff --git a/addons/account/models/account_report.py b/addons/account/models/account_report.py index 540da5e4aed18..3f525870a682b 100644 --- a/addons/account/models/account_report.py +++ b/addons/account/models/account_report.py @@ -588,6 +588,7 @@ class AccountReportExpression(models.Model): ('account_codes', "Prefix of Account Codes"), ('external', "External Value"), ('custom', "Custom Python Function"), + ('text', "Plain Text"), ], required=True ) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index f4e1c89e233d8..3a8d534ab4707 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -607,7 +607,7 @@ def _validate_repartition_lines(self): tax_reps = invoice_repartition_line_ids.filtered(lambda tax_rep: tax_rep.repartition_type == 'tax') total_pos_factor = sum(tax_reps.filtered(lambda tax_rep: tax_rep.factor > 0.0).mapped('factor')) - if float_compare(total_pos_factor, 1.0, precision_digits=2): + if total_pos_factor and float_compare(total_pos_factor, 1.0, precision_digits=2): raise ValidationError(_("Invoice and credit note distribution should have a total factor (+) equals to 100.")) total_neg_factor = sum(tax_reps.filtered(lambda tax_rep: tax_rep.factor < 0.0).mapped('factor')) if total_neg_factor and float_compare(total_neg_factor, -1.0, precision_digits=2): diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index d52f8b81e117c..1d4d04848643f 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -283,7 +283,8 @@ def _pre_reload_data(self, company, template_data, data, force_create=True, forc for prop in list(template_data): if prop.startswith('property_'): template_data.pop(prop) - data.pop('account.reconcile.model', None) + if not force_update: + data.pop('account.reconcile.model', None) if 'res.company' in data and not force_update: data['res.company'][company.id].clear() data['res.company'][company.id].setdefault('anglo_saxon_accounting', company.anglo_saxon_accounting) @@ -409,10 +410,11 @@ def tax_template_changed(tax, template): if rename_idx: tax_to_rename.name = f"[old{rename_idx - 1 if rename_idx > 1 else ''}] {tax_to_rename.name}" else: - fiscal_position_ids = values.get('fiscal_position_ids') - original_tax_ids = values.get('original_tax_ids') - repartition_lines = values.get('repartition_line_ids') - values.clear() + fiscal_position_ids = values.pop('fiscal_position_ids', None) + original_tax_ids = values.pop('original_tax_ids', None) + repartition_lines = values.pop('repartition_line_ids', None) + if not force_update: + values.clear() # taxes will always be (re)linked to fiscal positions (unless the fp doesn't exist and won't be created) if fiscal_position_ids: link_commands = [ diff --git a/addons/account/models/company.py b/addons/account/models/company.py index c57ab37898271..dd3d88aea2c68 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -146,7 +146,7 @@ class ResCompany(models.Model): incoterm_id = fields.Many2one('account.incoterms', string='Default incoterm', help='International Commercial Terms are a series of predefined commercial terms used in international transactions.') - qr_code = fields.Boolean(string='Display QR-code on invoices') + qr_code = fields.Boolean(string='Display QR-code on invoices', compute='_compute_qr_code', store=True, readonly=False) link_qr_code = fields.Boolean(string='Display Link QR-code') display_invoice_amount_total_words = fields.Boolean(string='Total amount of invoice in letters') @@ -464,6 +464,9 @@ def _compute_display_account_storno(self): for company in self: company.display_account_storno = company.account_fiscal_country_id.code in STORNO_MANDATORY_COUNTRIES | STORNO_OPTIONAL_COUNTRIES + def _compute_qr_code(self): + pass + def _initiate_account_onboardings(self): account_onboarding_routes = [ 'account_dashboard', diff --git a/addons/account/models/kpi_provider.py b/addons/account/models/kpi_provider.py index 38d42f7f99b09..ddae6fbd2f2fa 100644 --- a/addons/account/models/kpi_provider.py +++ b/addons/account/models/kpi_provider.py @@ -9,7 +9,7 @@ def get_account_kpi_summary(self): grouped_moves_to_report = self.env['account.move']._read_group( fields.Domain.OR([ [('state', '=', 'draft')], - [('state', '=', 'posted'), ('checked', '=', False)], + [('state', '=', 'posted'), ('review_state', 'in', ('todo', 'anomaly'))], [('state', '=', 'posted'), ('journal_id.type', '=', 'bank'), ('statement_line_id.is_reconciled', '=', False)], ]), ['journal_id'], diff --git a/addons/account/models/onboarding_onboarding_step.py b/addons/account/models/onboarding_onboarding_step.py index 8456210e0c8f1..21f9e479d9f4d 100644 --- a/addons/account/models/onboarding_onboarding_step.py +++ b/addons/account/models/onboarding_onboarding_step.py @@ -60,7 +60,6 @@ def action_open_step_create_invoice(self): @api.model def action_open_step_fiscal_year(self): company = self.env['account.journal'].browse(self.env.context.get('journal_id', None)).company_id or self.env.company - new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id}) view_id = self.env.ref('account.setup_financial_year_opening_form').id return { @@ -69,10 +68,10 @@ def action_open_step_fiscal_year(self): 'view_mode': 'form', 'res_model': 'account.financial.year.op', 'target': 'new', - 'res_id': new_wizard.id, 'views': [[view_id, 'form']], 'context': { 'dialog_size': 'medium', + 'default_company_id': company.id, } } diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 3df6694e49215..6d30276de8161 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -342,7 +342,10 @@ class ResPartner(models.Model): def _compute_fiscal_country_codes(self): for record in self: allowed_companies = record.company_id or self.env.companies - record.fiscal_country_codes = ",".join(allowed_companies.mapped('account_fiscal_country_id.code')) + country_codes = allowed_companies.mapped('account_fiscal_country_id.code') + if record.country_code: + country_codes.append(record.country_code) + record.fiscal_country_codes = ",".join(set(country_codes)) @api.depends('company_id') @api.depends_context('allowed_company_ids') diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 6d26394f9db91..f724dd399be9c 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -96,7 +96,7 @@ def _compute_tax_string(self): def _construct_tax_string(self, price): currency = self.currency_id - res = self.taxes_id.filtered(lambda t: t.company_id == self.env.company).compute_all( + res = self.taxes_id._filter_taxes_by_company(self.env.company).compute_all( price, product=self, partner=self.env['res.partner'] ) joined = [] diff --git a/addons/account/models/res_currency.py b/addons/account/models/res_currency.py index 19cdfb101df97..1bcbdfe4976c3 100644 --- a/addons/account/models/res_currency.py +++ b/addons/account/models/res_currency.py @@ -176,7 +176,7 @@ def _get_table_builder_current(self, period_key, main_company, other_companies, FROM res_company other_company LEFT JOIN res_currency_rate rate ON rate.currency_id = other_company.currency_id - AND rate.name <= %(date_to)s + AND rate.name < %(date_to)s AND rate.company_id = %(main_company_id)s WHERE other_company.id IN %(other_company_ids)s @@ -205,14 +205,14 @@ def _get_table_builder_historical(self, main_company, other_companies, date_to, WHERE other_company.id IN %(other_company_ids)s AND rate.company_id = %(main_company_id)s - AND rate.name <= %(date_to)s + AND rate.name < %(date_to)s %(exclusion_condition)s """, main_company_id=main_company.root_id.id, other_company_ids=tuple(other_companies.ids), main_company_unit_factor=main_company_unit_factor, date_to=date_to, - exclusion_condition=SQL("AND rate.name > %(date_exclude)s", date_exclude=date_exclude) if date_exclude else SQL(), + exclusion_condition=SQL("AND rate.name >= %(date_exclude)s", date_exclude=date_exclude) if date_exclude else SQL(), ) def _get_table_builder_average(self, period_key, main_company, other_companies, date_from, date_to, main_company_unit_factor) -> SQL: @@ -236,15 +236,15 @@ def _get_table_builder_average(self, period_key, main_company, other_companies, EXTRACT ( 'Day' FROM COALESCE( LEAD(rate.name, 1) OVER (PARTITION BY other_company.id, rate.currency_id ORDER BY rate.name ASC)::TIMESTAMP, - %(date_to)s::TIMESTAMP + INTERVAL '1' DAY + %(date_to)s::TIMESTAMP ) - rate.name::TIMESTAMP ) AS number_of_days FROM res_company other_company JOIN res_currency_rate rate ON rate.currency_id = other_company.currency_id WHERE - rate.name <= %(date_to)s - AND rate.name >= %(date_from)s + rate.name < %(date_to)s + AND rate.name >= DATE %(date_from)s - INTERVAL '1 day' AND other_company.id IN %(other_company_ids)s AND rate.company_id = %(main_company_id)s @@ -254,20 +254,20 @@ def _get_table_builder_average(self, period_key, main_company, other_companies, SELECT DISTINCT ON (other_company.id) other_company.id as other_company_id, COALESCE(out_period_rate.rate, 1.0) AS rate, - EXTRACT('Day' FROM COALESCE(in_period_rate.name::TIMESTAMP, %(date_to)s::TIMESTAMP + INTERVAL '1' DAY) - %(date_from)s::TIMESTAMP) AS number_of_days + EXTRACT('Day' FROM COALESCE(in_period_rate.name::TIMESTAMP, %(date_to)s::TIMESTAMP) - %(date_from)s::TIMESTAMP + INTERVAL '1 day') AS number_of_days FROM res_company other_company LEFT JOIN res_currency_rate in_period_rate ON in_period_rate.currency_id = other_company.currency_id - AND in_period_rate.name <= %(date_to)s - AND in_period_rate.name >= %(date_from)s + AND in_period_rate.name < %(date_to)s + AND in_period_rate.name >= DATE %(date_from)s - INTERVAL '1 day' AND in_period_rate.company_id = %(main_company_id)s LEFT JOIN res_currency_rate out_period_rate ON out_period_rate.currency_id = other_company.currency_id AND out_period_rate.company_id = %(main_company_id)s - AND out_period_rate.name < %(date_from)s + AND out_period_rate.name < DATE %(date_from)s - INTERVAL '1 day' WHERE other_company.id IN %(other_company_ids)s diff --git a/addons/account/models/res_partner_bank.py b/addons/account/models/res_partner_bank.py index 87007fdb316e2..8e9506d78d7c2 100644 --- a/addons/account/models/res_partner_bank.py +++ b/addons/account/models/res_partner_bank.py @@ -74,8 +74,10 @@ def _compute_duplicate_bank_partner_ids(self): FROM res_partner_bank this LEFT JOIN res_partner_bank other ON this.acc_number = other.acc_number AND this.id != other.id + AND other.active = TRUE WHERE this.id = ANY(%(ids)s) AND other.partner_id IS NOT NULL + AND this.active = TRUE AND ( ((this.company_id = other.company_id) OR (this.company_id IS NULL AND other.company_id IS NULL)) OR diff --git a/addons/account/static/src/components/account_file_uploader/account_file_uploader.xml b/addons/account/static/src/components/account_file_uploader/account_file_uploader.xml index 8912d0bd758b8..41fcd3d436acd 100644 --- a/addons/account/static/src/components/account_file_uploader/account_file_uploader.xml +++ b/addons/account/static/src/components/account_file_uploader/account_file_uploader.xml @@ -19,7 +19,7 @@ - + diff --git a/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.js b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.js new file mode 100644 index 0000000000000..f05784fac0bc9 --- /dev/null +++ b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.js @@ -0,0 +1,119 @@ +import { registry } from "@web/core/registry"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { Component, onWillStart } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { cookie } from "@web/core/browser/cookie"; +import { user } from "@web/core/user"; + +export class AccountReviewStateSelectionBadge extends Component { + static template = "account_reports.AccountReviewStateSelectionBadgeField"; + static props = { + ...standardFieldProps, + decorations: { type: Object, optional: true }, + options: { type: Object, optional: true }, + class: { type: String, optional: true }, + size: { type: String, optional: true }, + }; + + setup() { + onWillStart(async () => { + this.editableOptions = await this.getEditableOptions(); + }); + } + + static defaultProps = { + size: "normal" + }; + + static components = { + Dropdown, + DropdownItem, + } + + get options() { + return this.props.record.fields[this.props.name].selection; + } + + get value() { + return this.props.record.data[this.props.name]; + } + + get required() { + return this.props.record.fields[this.props.name].required; + } + + get display() { + const result = this.options.filter((val) => val[0] === this.value)[0]; + if(result) { + return result[1]; + } + return null; + } + + async getEditableOptions () { + const editableOptions = [] + if (this.props.options[false] === undefined) { + editableOptions.push(false); + } + + for (let [key, value] of Object.entries(this.props.options)) { + if ( + [true, undefined].includes(value.can_edit) + || (typeof value.can_edit == 'string' && (await Promise.all( + value.can_edit.split(",").map(group => user.hasGroup(group)) + )).some(Boolean)) + ) { + editableOptions.push(key === 'false' ? false : key); + } + } + + return editableOptions; + } + + getDropdownButtonDecoration(value) { + const decoration = this.props.options[value]?.decoration + if (!decoration || decoration === 'muted') { + return 'btn-outline-secondary' + } + return `btn-outline-${decoration}` + } + + getDropdownItemDecoration(value) { + const colorScheme = cookie.get("color_scheme"); + const decoration = this.props.options[value]?.decoration; + if (decoration) { + if (decoration === "muted") { + return colorScheme === 'dark' ? "text-bg-200" : "text-bg-300"; + } + return `text-bg-${decoration}`; + } + return "text-bg-200" + } + + get additionalClassName() { + return this.props.class || ""; + } + + get capsuleStyle() { + return "min-width: 10ch; min-height: 2em;"; + } + + async onChange(value) { + await this.props.record.update( + { [this.props.name]: value }, + { save: true } + ); + this.env.reload?.() + } +} + +export const accountReviewStateSelectionBadge = { + supportedTypes: ["selection"], + component: AccountReviewStateSelectionBadge, + extractProps: ({options}) => { + return { options }; + }, +} + +registry.category("fields").add("account_review_state_selection_badge", accountReviewStateSelectionBadge) diff --git a/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.scss b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.scss new file mode 100644 index 0000000000000..e080306955ba9 --- /dev/null +++ b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.scss @@ -0,0 +1,8 @@ +.o_account_review_state_selection_badge_dropdown_item { + min-height: 27px; +} + +.o_account_review_state_selection_badge_audit_list .btn { + font-size: 0.7rem; + padding: 0 5px; +} diff --git a/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.xml b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.xml new file mode 100644 index 0000000000000..2926921345552 --- /dev/null +++ b/addons/account/static/src/components/account_review_state_selection/account_review_state_selection_badge.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + diff --git a/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml b/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml deleted file mode 100644 index 691dbe18443f9..0000000000000 --- a/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - To review - - - - - diff --git a/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js b/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js deleted file mode 100644 index dd271f165adb6..0000000000000 --- a/addons/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js +++ /dev/null @@ -1,16 +0,0 @@ -import { registry } from "@web/core/registry"; -import { - charWithPlaceholderField, - CharWithPlaceholderField -} from "../char_with_placeholder_field/char_with_placeholder_field"; - -export class CharWithPlaceholderFieldToCheck extends CharWithPlaceholderField { - static template = "account.CharWithPlaceholderField"; -} - -export const charWithPlaceholderFieldToCheck = { - ...charWithPlaceholderField, - component: CharWithPlaceholderFieldToCheck, -}; - -registry.category("fields").add("char_with_placeholder_field_to_check", charWithPlaceholderFieldToCheck); diff --git a/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js b/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js index c9530d7fac7a7..687d2572568f8 100644 --- a/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js +++ b/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js @@ -4,19 +4,21 @@ import { } from "@web/views/fields/many2many_tags/many2many_tags_field"; import { useService } from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; -import { TagsList } from "@web/core/tags_list/tags_list"; +import { BadgeTag } from "@web/core/tags_list/badge_tag"; import { _t } from "@web/core/l10n/translation"; -import { onMounted } from "@odoo/owl"; +import { Component, onMounted } from "@odoo/owl"; -export class FieldMany2ManyTagsBanksTagsList extends TagsList { - static template = "FieldMany2ManyTagsBanksTagsList"; +class BankTag extends Component { + static template = "account.BankTag"; + static components = { BadgeTag }; + static props = ["allowOutPayment?", "color", "onClick", "onDelete" , "text"]; } export class FieldMany2ManyTagsBanks extends Many2ManyTagsFieldColorEditable { static template = "account.FieldMany2ManyTagsBanks"; static components = { - ...FieldMany2ManyTagsBanks.components, - TagsList: FieldMany2ManyTagsBanksTagsList, + ...super.components, + Tag: BankTag, }; setup() { diff --git a/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml b/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml index 6c51d4343bb62..0c5fcebf50d5d 100644 --- a/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml +++ b/addons/account/static/src/components/many2many_tags_banks/many2many_tags_banks.xml @@ -1,12 +1,11 @@ - - - - - - - + + + + +
+ diff --git a/addons/account/static/src/js/tours/account.js b/addons/account/static/src/js/tours/account.js index 557886e84f8d0..ca8e2fd88e5d0 100644 --- a/addons/account/static/src/js/tours/account.js +++ b/addons/account/static/src/js/tours/account.js @@ -63,7 +63,7 @@ registry.category("web_tour.tours").add('account_tour', { run: "click", }, { - trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] .o_field_x2many_list_row_add a`, + trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] .o_field_x2many_list_row_add button`, content: _t("Add a line to your invoice"), run: "click", }, diff --git a/addons/account/static/tests/account_move_form.test.js b/addons/account/static/tests/account_move_form.test.js index 483cba596cab7..56bf17bbf96c9 100644 --- a/addons/account/static/tests/account_move_form.test.js +++ b/addons/account/static/tests/account_move_form.test.js @@ -7,7 +7,7 @@ import { triggerHotkey, } from "@mail/../tests/mail_test_helpers"; import { expect, test } from "@odoo/hoot"; -import { contains, onRpc } from "@web/../tests/web_test_helpers"; +import { contains, defineModels, fields, onRpc, models} from "@web/../tests/web_test_helpers"; import { defineAccountModels } from "./account_test_helpers"; defineAccountModels(); @@ -61,3 +61,63 @@ test("Confirmation dialog on delete contains a warning", async () => { { message: "warning message has been added in the dialog" } ); }); +class AccountMove extends models.Model { + line_ids = fields.One2many({ + string: "Invoice Lines", + relation: "account.move.line", + }) + + _records = [{ id: 1, name: "account.move" }] +} +class AccountMoveLine extends models.Model { + name = fields.Char(); + product_id = fields.Many2one({ + string:"Product", + relation:"product", + }); + move_id = fields.Many2one({ + string: "Account Move", + relation: "account.move", + }) +} +class Product extends models.Model { + name = fields.Char(); + _records = [{ id: 1, name: "testProduct" }]; +} + +defineModels({ Product, AccountMoveLine, AccountMove }); + +test("Update description on product line", async() => { + const pyEnv = await startServer(); + const productId = pyEnv["product"].browse([1]); + const accountMove = pyEnv["account.move"].browse([1]); + pyEnv["account.move.line"].create({ name: productId[0].name, product_id: productId[0].id, move_id: accountMove[0].id }); + await start(); + onRpc("account.move", "web_save", () => { expect.step("save")}); + await openFormView("account.move", accountMove[0].id, { + arch: `
+ + + + + + + + + + + + +
`, + }); + + await click(".o_many2one"); + await contains("#labelVisibilityButtonId").click() + await insertText("textarea[placeholder='Enter a description']", "testDescription"); + await click(".o_form_button_save"); + await expect.waitForSteps(["save"]); + + const line = pyEnv["account.move.line"].browse([1])[0]; + expect(line.name).toBe("testProduct\ntestDescription"); + +}); diff --git a/addons/account/static/tests/account_widgets.test.js b/addons/account/static/tests/account_widgets.test.js index bc545ba4f0b01..0d5af6359b35e 100644 --- a/addons/account/static/tests/account_widgets.test.js +++ b/addons/account/static/tests/account_widgets.test.js @@ -169,7 +169,7 @@ describe("PaymentTermsLineWidget", () => { }); expect(".o_data_row").toHaveCount(2); // click the add button - await contains(".o_field_x2many_list_row_add > a").click(); + await contains(".o_field_x2many_list_row_add > button").click(); // make sure the new record is added expect(".o_data_row").toHaveCount(3); // global click @@ -177,7 +177,7 @@ describe("PaymentTermsLineWidget", () => { // make sure the new record is still there expect(".o_data_row").toHaveCount(3); // click the add button again - await contains(".o_field_x2many_list_row_add > a").click(); + await contains(".o_field_x2many_list_row_add > button").click(); // make sure the new record is added expect(".o_data_row").toHaveCount(4); // click on an existing record diff --git a/addons/account/static/tests/section_and_note.test.js b/addons/account/static/tests/section_and_note.test.js index 71379d69c2c45..4faffd20ac310 100644 --- a/addons/account/static/tests/section_and_note.test.js +++ b/addons/account/static/tests/section_and_note.test.js @@ -931,7 +931,7 @@ test("add a section", async () => { "C1", ]); expect(`.o_note_row`).toHaveCount(0); - await contains(".o_field_x2many_list_row_add a:eq(1)").click(); + await contains(".o_field_x2many_list_row_add button:eq(1)").click(); await edit("D"); await contains(getFixture()).click(); expect(queryAllTexts(".o_data_row")).toEqual([ @@ -995,7 +995,7 @@ test("add note", async () => { "C1", ]); expect(`.o_note_row`).toHaveCount(0); - await contains(".o_field_x2many_list_row_add a:last").click(); + await contains(".o_field_x2many_list_row_add button:last").click(); await edit("this is a note"); await contains(getFixture()).click(); expect(queryAllTexts(".o_data_row")).toEqual([ @@ -1059,7 +1059,7 @@ test("section_and_note_text widget", async () => { "C1", ]); expect(`.o_note_row`).toHaveCount(0); - await contains(".o_field_x2many_list_row_add a:last").click(); + await contains(".o_field_x2many_list_row_add button:last").click(); expect(`.o_is_line_note textarea`).toHaveCount(1); await edit("this is a note\non 2 lines"); await contains(getFixture()).click(); @@ -1123,7 +1123,7 @@ test("sections with required content field", async () => { expect(".o_data_row").toHaveCount(14); await contains(".o_form_button_cancel").click(); expect(".o_data_row").toHaveCount(13); - await contains(".o_field_x2many_list_row_add a:eq(1)").click(); + await contains(".o_field_x2many_list_row_add button:eq(1)").click(); expect(".o_data_row").toHaveCount(14); expect(".o_invalid_cell").toHaveCount(0); await press("Enter"); diff --git a/addons/account/static/tests/tours/deductible_amount_column.js b/addons/account/static/tests/tours/deductible_amount_column.js index f96191f152d7f..4f8d4600efcf9 100644 --- a/addons/account/static/tests/tours/deductible_amount_column.js +++ b/addons/account/static/tests/tours/deductible_amount_column.js @@ -6,7 +6,7 @@ registry.category("web_tour.tours").add("deductible_amount_column", { steps: () => [ { content: "Add item", - trigger: "div[name='invoice_line_ids'] .o_field_x2many_list_row_add a:contains('Add a line')", + trigger: "div[name='invoice_line_ids'] .o_field_x2many_list_row_add button:contains('Add a line')", run: "click", }, { diff --git a/addons/account/static/tests/tours/tax_group_tests.js b/addons/account/static/tests/tours/tax_group_tests.js index fb77b79faa748..3a4e53be487b6 100644 --- a/addons/account/static/tests/tours/tax_group_tests.js +++ b/addons/account/static/tests/tours/tax_group_tests.js @@ -54,7 +54,7 @@ registry.category("web_tour.tours").add('account_tax_group', { // Add First product { content: "Add items", - trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add a:contains("Add a line")', + trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add button:contains("Add a line")', run: "click", }, { diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py index 3c29d780cdca2..bdf017d5dda2e 100644 --- a/addons/account/tests/common.py +++ b/addons/account/tests/common.py @@ -281,8 +281,8 @@ def setup_other_currency(cls, code, **kwargs): if 'rates' not in kwargs: return super().setup_other_currency(code, rates=[ ('1900-01-01', 1.0), - ('2016-01-01', 3.0), - ('2017-01-01', 2.0), + ('2015-12-31', 3.0), + ('2016-12-31', 2.0), ], **kwargs) return super().setup_other_currency(code, **kwargs) @@ -1270,7 +1270,7 @@ def convert_base_line_to_invoice_line(self, document, base_line): def convert_document_to_invoice(self, document): invoice_date = '2020-01-01' currency = document['currency'] - self._ensure_rate(currency, invoice_date, document['rate']) + self._ensure_rate(currency, '2019-12-31', document['rate']) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'invoice_date': invoice_date, diff --git a/addons/account/tests/test_account_bank_statement.py b/addons/account/tests/test_account_bank_statement.py index 08f3d8edcf78e..72c1af969d73f 100644 --- a/addons/account/tests/test_account_bank_statement.py +++ b/addons/account/tests/test_account_bank_statement.py @@ -16,8 +16,8 @@ def setUpClass(cls): cls.currency_1 = cls.company_data['currency'] # We need a third currency as you could have a company's currency != journal's currency != cls.currency_2 = cls.setup_other_currency('EUR') - cls.currency_3 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)]) - cls.currency_4 = cls.setup_other_currency('GBP', rates=[('2016-01-01', 12.0), ('2017-01-01', 8.0)]) + cls.currency_3 = cls.setup_other_currency('CAD', rates=[('2015-12-31', 6.0), ('2016-12-31', 4.0)]) + cls.currency_4 = cls.setup_other_currency('GBP', rates=[('2015-12-31', 12.0), ('2016-12-31', 8.0)]) cls.company_data_2 = cls.setup_other_company() diff --git a/addons/account/tests/test_account_incoming_supplier_invoice.py b/addons/account/tests/test_account_incoming_supplier_invoice.py index 503f3779a1a92..1d481db62e1c2 100644 --- a/addons/account/tests/test_account_incoming_supplier_invoice.py +++ b/addons/account/tests/test_account_incoming_supplier_invoice.py @@ -937,3 +937,13 @@ def test_74_mail_alias_all(self): } }, ) + + def test_75_journal_upload_empty_pdf(self): + empty_pdf_vals = {'name': 'empty.pdf', 'raw': False, 'type': 'binary', 'mimetype': 'application/pdf'} + self.assert_attachment_import( + origin='journal', + attachments_vals=[empty_pdf_vals], + expected_invoices={ + 1: {'empty.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, + }, + ) diff --git a/addons/account/tests/test_account_journal.py b/addons/account/tests/test_account_journal.py index 336d5815da005..0e59ec34e788f 100644 --- a/addons/account/tests/test_account_journal.py +++ b/addons/account/tests/test_account_journal.py @@ -455,3 +455,23 @@ def test_send_email_to_alias_from_other_company(self): msg_id='', ) self.assertTrue(self.env['account.move'].search([('invoice_source_email', '=', 'company_2_user@test.com')])) + + def test_alias_uniqueness_without_domain(self): + """Ensure alias_name is unique even if alias_domain is not defined.""" + default_account = self.env['account.account'].search( + domain=[('account_type', 'in', ('income', 'income_other'))], + limit=1, + ) + with Form(self.env['account.journal']) as journal_form: + journal_form.type = 'sale' + journal_form.code = 'A' + journal_form.name = 'Test Journal 1' + journal_form.default_account_id = default_account + journal_1 = journal_form.save() + with Form(self.env['account.journal']) as journal_form: + journal_form.type = 'sale' + journal_form.code = 'B' + journal_form.name = 'Test Journal 2' + journal_form.default_account_id = default_account + journal_2 = journal_form.save() + self.assertNotEqual(journal_1.alias_id.alias_name, journal_2.alias_id.alias_name) diff --git a/addons/account/tests/test_account_journal_dashboard.py b/addons/account/tests/test_account_journal_dashboard.py index 7bd832f281084..3d0dbd006a33e 100644 --- a/addons/account/tests/test_account_journal_dashboard.py +++ b/addons/account/tests/test_account_journal_dashboard.py @@ -336,7 +336,7 @@ def test_to_check_posted(self): 'move_type': 'out_invoice', 'journal_id': journal.id, 'partner_id': self.partner_a.id, - 'checked': False, + 'review_state': 'todo', 'invoice_line_ids': [ Command.create({ 'product_id': self.product_a.id, @@ -351,7 +351,7 @@ def test_to_check_posted(self): self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(0)) move.action_post() - move.checked = False + move.review_state = 'todo' dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id] self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(100)) @@ -389,7 +389,7 @@ def test_to_check_amount_different_currency(self): 'journal_id': journal.id, 'partner_id': self.partner_a.id, 'currency_id': currency.id, - 'checked': False, + 'review_state': 'todo', 'invoice_line_ids': [ Command.create({ 'product_id': self.product_a.id, @@ -400,7 +400,7 @@ def test_to_check_amount_different_currency(self): ] } for currency in (self.env.ref('base.EUR'), self.env.ref('base.CHF'))]) moves.action_post() - moves.checked = False + moves.review_state = 'todo' dashboard_data = journal._get_journal_dashboard_data_batched()[journal.id] self.assertEqual(dashboard_data['to_check_balance'], journal.currency_id.format(150)) diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index be62bde355dfd..ec80309e88fa0 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -312,11 +312,27 @@ def test_misc_draft_reconciled_entries_1(self): def test_modify_posted_move_readonly_fields(self): self.test_move.action_post() - readonly_fields = ('invoice_line_ids', 'line_ids', 'invoice_date', 'date', 'partner_id', - 'invoice_payment_term_id', 'currency_id', 'fiscal_position_id', 'invoice_cash_rounding_id') - for field in readonly_fields: + readonly_fields = { + 'invoice_line_ids': False, + 'line_ids': False, + 'invoice_date': '4321-11-11', + 'date': '4321-11-11', + 'partner_id': 42424242, + 'invoice_payment_term_id': 42424242, + 'currency_id': 42424242, + 'fiscal_position_id': 42424242, + 'invoice_cash_rounding_id': 42424242, + } + for fname, value in readonly_fields.items(): with self.assertRaisesRegex(UserError, "You cannot modify the following readonly fields on a posted move"): - self.test_move.write({field: False}) + self.test_move.write({fname: value}) + for fname in readonly_fields: + if fname.endswith('_ids'): + continue # x2m are always raising + elif fname.endswith('_id'): + self.test_move.write({fname: self.test_move[fname].id}) + else: + self.test_move.write({fname: self.test_move[fname]}) def test_misc_move_onchange(self): ''' Test the behavior on onchanges for account.move having 'entry' as type. ''' diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index c440a70a18942..606e5500edd43 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -943,6 +943,8 @@ def test_in_invoice_line_onchange_cash_rounding_1(self): }) def test_in_invoice_line_onchange_currency_1(self): + self.other_currency.rounding = 0.001 + move_form = Form(self.invoice) move_form.currency_id = self.other_currency move_form.save() diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py index 585f4195aaa43..8cb8009845afc 100644 --- a/addons/account/tests/test_account_move_in_refund.py +++ b/addons/account/tests/test_account_move_in_refund.py @@ -605,6 +605,8 @@ def test_in_refund_line_onchange_cash_rounding_1(self): }) def test_in_refund_line_onchange_currency_1(self): + self.other_currency.rounding = 0.001 + move_form = Form(self.invoice) move_form.currency_id = self.other_currency move_form.save() diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 5783784df9607..e709722b69edf 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -1528,6 +1528,8 @@ def test_out_invoice_line_onchange_cash_rounding_1(self): }) def test_out_invoice_line_onchange_currency_1(self): + self.other_currency.rounding = 0.001 + move_form = Form(self.invoice) move_form.currency_id = self.other_currency move_form.save() @@ -4356,8 +4358,8 @@ def invoice(date): ) currency = self.setup_other_currency('EUR', rates=[ - ('2016-01-01', 3.0), - ('2017-01-01', 2.0), + ('2015-12-31', 3.0), + ('2016-12-31', 2.0), ]) self.assertRecordValues(invoice('2015-01-01'), [{ 'amount_total': 1000.0, @@ -4681,8 +4683,8 @@ def test_out_invoice_tax_tags(self): def test_lines_recomputation_after_currency_rate_change(self): currency = self.setup_other_currency('EUR', rates=[ - ('2025-01-01', 0.5), - ('2025-02-01', 0.4), + ('2024-12-31', 0.5), + ('2025-01-31', 0.4), ]) with Form(self.env['account.move'].with_context(default_move_type='out_invoice')) as move_form: @@ -4733,6 +4735,32 @@ def test_narration_preserved_when_use_invoice_terms_disabled(self): "Narration should be preserved after partner change when invoice terms are disabled" ) + def test_narration_translation_on_partner_language_change(self): + """Ensure narration translates when partner.lang changes (HTML terms link).""" + self.env['ir.config_parameter'].sudo().set_bool('account.use_invoice_terms', True) + self.env['res.lang']._activate_lang('fr_FR') + + self.env.company.terms_type = 'html' + + # Baseline: en_US user/partner + self.partner_a.lang = 'en_US' + + # Create invoice + invoice = self.init_invoice(move_type='out_invoice', partner=self.partner_a) + + baseurl = self.env.company.get_base_url() + '/terms' + + # Check the initial narration is in English + expected_en = f"

Terms & Conditions: {baseurl}

" + self.assertEqual(invoice.narration, expected_en) + + # Switch to fr_FR + self.partner_a.lang = 'fr_FR' + + # Check the narration is in French + expected_fr = f"

Conditions générales : {baseurl}

" + self.assertEqual(invoice.narration, expected_fr) + def test_multiple_currency_change(self): """ Test amount currency and balance are correctly recomputed when switching currency multiple times diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py index 7680b1e6e4316..8e777e0c74e0e 100644 --- a/addons/account/tests/test_account_move_out_refund.py +++ b/addons/account/tests/test_account_move_out_refund.py @@ -590,6 +590,8 @@ def test_out_refund_line_onchange_cash_rounding_1(self): }) def test_out_refund_line_onchange_currency_1(self): + self.other_currency.rounding = 0.001 + move_form = Form(self.invoice) move_form.currency_id = self.other_currency move_form.save() diff --git a/addons/account/tests/test_account_move_payments_widget.py b/addons/account/tests/test_account_move_payments_widget.py index c1b932cda173c..e434ef0b169a6 100644 --- a/addons/account/tests/test_account_move_payments_widget.py +++ b/addons/account/tests/test_account_move_payments_widget.py @@ -16,7 +16,7 @@ def setUpClass(cls): cls.curr_1 = cls.company_data['currency'] cls.curr_2 = cls.setup_other_currency('EUR') - cls.curr_3 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)]) + cls.curr_3 = cls.setup_other_currency('CAD', rates=[('2015-12-31', 6.0), ('2016-12-31', 4.0)]) cls.payment_2016_curr_1 = cls.env['account.move'].create({ 'date': '2016-01-01', diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index cb415fb07505b..68a663542c46f 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -35,8 +35,8 @@ def setUpClass(cls): # ==== Multi-currency setup ==== cls.other_currency = cls.setup_other_currency('EUR', rounding=0.001) - cls.other_currency_2 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)]) - cls.other_currency_3 = cls.setup_other_currency('XAF', rates=[('2016-01-01', 0.0001), ('2017-01-01', 0.00001)]) + cls.other_currency_2 = cls.setup_other_currency('CAD', rates=[('2015-12-31', 6.0), ('2016-12-31', 4.0)]) + cls.other_currency_3 = cls.setup_other_currency('XAF', rates=[('2015-12-31', 0.0001), ('2016-12-31', 0.00001)]) # ==== Cash Basis Taxes setup ==== @@ -1122,6 +1122,7 @@ def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_cr def test_reconcile_one_foreign_currency_fallback_company_currency(self): comp_curr = self.company_data['currency'] foreign_curr = self.other_currency_3 + foreign_curr.rounding = 0.001 # to be able to check the amounts up to the 3rd digit line_1 = self.create_line_for_reconciliation(-10.0, -10.0, comp_curr, '2017-01-01') line_2 = self.create_line_for_reconciliation(1000000.0, 100.0, foreign_curr, '2017-01-01') @@ -1852,7 +1853,7 @@ def test_reconcile_foreign_currency_rounding_issue(self): 'symbol': '🍞', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-06-01', 'rate': 0.052972554919}), + Command.create({'name': '2019-05-31', 'rate': 0.052972554919}), ], }) @@ -1949,7 +1950,7 @@ def test_reconcile_partial_exchange_rounding_issue(self): 'symbol': '🍞', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-06-01', 'rate': 0.052972554919}), + Command.create({'name': '2019-05-31', 'rate': 0.052972554919}), ], }) @@ -2023,7 +2024,7 @@ def test_full_reconcile_foreign_currency_rounding_difference_credit_larger(self) 'symbol': '🍞', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-06-01', 'rate': 0.648587}), + Command.create({'name': '2019-05-31', 'rate': 0.648587}), ], }) @@ -2084,7 +2085,7 @@ def test_full_reconcile_foreign_currency_rounding_difference_debit_larger(self): 'symbol': '🍞', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-06-01', 'rate': 0.648587}), + Command.create({'name': '2019-05-31', 'rate': 0.648587}), ], }) @@ -2142,11 +2143,11 @@ def test_reconcile_special_mexican_workflow_1(self): 'symbol': '🍣', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-09-24', 'rate': 0.050800000000}), - Command.create({'name': '2019-06-28', 'rate': 0.052235000000}), - Command.create({'name': '2019-06-24', 'rate': 0.052686000000}), - Command.create({'name': '2019-06-20', 'rate': 0.052353000000}), - Command.create({'name': '2019-06-12', 'rate': 0.052072000000}), + Command.create({'name': '2019-09-23', 'rate': 0.050800000000}), + Command.create({'name': '2019-06-27', 'rate': 0.052235000000}), + Command.create({'name': '2019-06-23', 'rate': 0.052686000000}), + Command.create({'name': '2019-06-19', 'rate': 0.052353000000}), + Command.create({'name': '2019-06-11', 'rate': 0.052072000000}), ], }) @@ -2443,11 +2444,11 @@ def test_reconcile_special_mexican_workflow_2(self): 'symbol': '🍣', 'rounding': 0.01, 'rate_ids': [ - Command.create({'name': '2019-09-24', 'rate': 0.050800000000}), - Command.create({'name': '2019-06-28', 'rate': 0.052235000000}), - Command.create({'name': '2019-06-24', 'rate': 0.052686000000}), - Command.create({'name': '2019-06-20', 'rate': 0.052353000000}), - Command.create({'name': '2019-06-12', 'rate': 0.052072000000}), + Command.create({'name': '2019-09-23', 'rate': 0.050800000000}), + Command.create({'name': '2019-06-27', 'rate': 0.052235000000}), + Command.create({'name': '2019-06-23', 'rate': 0.052686000000}), + Command.create({'name': '2019-06-19', 'rate': 0.052353000000}), + Command.create({'name': '2019-06-11', 'rate': 0.052072000000}), ], }) @@ -2815,7 +2816,7 @@ def test_migration_to_new_reconciliation_multiple_currencies_fix_residual_with_w ]) def test_reconcile_rounding_issue(self): - currency = self.setup_other_currency('CHF', rates=[('2016-01-01', 1 / 1.5289), ('2017-01-01', 1 / 1.5289)]) + currency = self.setup_other_currency('CHF', rates=[('2015-12-31', 1 / 1.5289), ('2016-12-31', 1 / 1.5289)]) # Create an invoice 26.45 XXX = 40.43 USD invoice = self.env['account.move'].create({ @@ -3787,7 +3788,7 @@ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries account is not a reconcile one. ''' self.env.company.tax_exigibility = True - currency_id = self.setup_other_currency('CHF', rates=[('2016-01-01', 0.5), ('2017-01-01', 0.66666666666666)]).id + currency_id = self.setup_other_currency('CHF', rates=[('2015-12-31', 0.5), ('2016-12-31', 0.66666666666666)]).id # Rate 2/1 in 2016. caba_inv = self.env['account.move'].with_context(skip_invoice_sync=True).create({ @@ -3862,7 +3863,7 @@ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries account is not a reconcile one. ''' self.env.company.tax_exigibility = True - currency_id = self.setup_other_currency('CHF', rates=[('2016-01-01', 0.5), ('2017-01-01', 0.66666666666666)]).id + currency_id = self.setup_other_currency('CHF', rates=[('2015-12-31', 0.5), ('2016-12-31', 0.66666666666666)]).id # Rate 2/1 in 2016. caba_inv = self.env['account.move'].with_context(skip_invoice_sync=True).create({ @@ -4043,7 +4044,7 @@ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries def test_reconcile_cash_basis_refund_multicurrency(self): self.env.company.tax_exigibility = True - currency = self.setup_other_currency('CHF', rates=[('2016-01-01', 0.5), ('2017-01-01', 0.33333333333333333)]) + currency = self.setup_other_currency('CHF', rates=[('2015-12-31', 0.5), ('2016-12-31', 0.33333333333333333)]) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py index 47ecc7b700541..0916e7336e44a 100644 --- a/addons/account/tests/test_account_payment_register.py +++ b/addons/account/tests/test_account_payment_register.py @@ -26,7 +26,7 @@ def setUpClass(cls): cls.current_year = fields.Date.today().year cls.other_currency = cls.setup_other_currency('EUR') - cls.other_currency_2 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 3.0), ('2017-01-01', 0.01)]) + cls.other_currency_2 = cls.setup_other_currency('CAD', rates=[('2015-12-31', 3.0), ('2016-12-31', 0.01)]) cls.payment_debit_account_id = cls.copy_account(cls.inbound_payment_method_line.payment_account_id) cls.payment_credit_account_id = cls.copy_account(cls.outbound_payment_method_line.payment_account_id) diff --git a/addons/account/tests/test_account_to_check.py b/addons/account/tests/test_account_to_check.py index e1f83fe0ac21d..a391bbc8b4f28 100644 --- a/addons/account/tests/test_account_to_check.py +++ b/addons/account/tests/test_account_to_check.py @@ -18,47 +18,117 @@ def setUpClass(cls): cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.company.id)], limit=1) def test_try_check_move_with_invoicing_user(self): - if 'accountant' not in self.env["ir.module.module"]._installed(): - self.skipTest('accountant is not installed') - invoice = self._create_invoice(checked=True) + invoice = self._create_invoice(review_state='todo') invoice.action_post() - with self.assertRaisesRegex(ValidationError, 'Validated entries can only be changed by your accountant.'): + with self.assertRaisesRegex(ValidationError, 'This entry has already been reviewed.'): invoice.with_user(self.simple_accountman).button_draft() invoice.button_draft() self.assertEqual(invoice.state, 'draft') invoice.action_post() - invoice.checked = False + self.assertEqual(invoice.review_state, 'reviewed') + invoice.review_state = 'todo' invoice.with_user(self.simple_accountman).button_draft() self.assertEqual(invoice.state, 'draft') + def test_sales_change_invoice_from_accountant(self): + invoice = self._create_invoice() + invoice.action_post() + with self.assertRaisesRegex(ValidationError, 'This entry has already been reviewed.'): + invoice.with_user(self.simple_accountman).button_draft() + + def test_sales_modify_draft_reviewed(self): + invoice = self._create_invoice(review_state='reviewed') + invoice.with_user(self.simple_accountman).invoice_date = '2017-01-01' + self.assertEqual(invoice.review_state, 'todo') + def test_post_move_auto_check(self): - if 'accountant' not in self.env["ir.module.module"]._installed(): - self.skipTest('accountant is not installed') invoice_admin = self._create_invoice() invoice_admin.action_post() - # As the user has admin right, the move should be auto checked - self.assertTrue(invoice_admin.checked) + # As the user has admin right, the move doesn't need to be checked + self.assertFalse(invoice_admin.review_state) invoice_invoicing = self._create_invoice(user_id=self.simple_accountman.id) invoice_invoicing.with_user(self.simple_accountman).action_post() # As the user has only invoicing right, the move shouldn't be checked - self.assertFalse(invoice_invoicing.checked) + self.assertEqual(invoice_invoicing.review_state, 'todo') + + def test_post_move_auto_check_with_auto_post_at_date_accountant(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.auto_post = 'at_date' + self.assertFalse(invoice.review_state) + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertFalse(invoice.review_state) + + def test_post_move_auto_check_with_auto_post_at_date_sales(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.with_user(self.simple_accountman).auto_post = 'at_date' + self.assertEqual(invoice.review_state, 'todo') + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertEqual(invoice.review_state, 'todo') + + def test_post_move_auto_check_with_auto_post_at_date_sales_prereviewed(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.with_user(self.simple_accountman).auto_post = 'at_date' + self.assertEqual(invoice.review_state, 'todo') + invoice.review_state = 'reviewed' + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertEqual(invoice.review_state, 'reviewed') + + def test_post_move_auto_check_with_auto_post_monthly_accountant(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.auto_post = 'monthly' + self.assertFalse(invoice.review_state) + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertFalse(invoice.review_state) + last_recurring = self.env['account.move'].search([('auto_post_origin_id', '=', invoice.id)], limit=1, order='date desc') + self.assertFalse(last_recurring.review_state) + + def test_post_move_auto_check_with_auto_post_monthly_sales(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.with_user(self.simple_accountman).auto_post = 'monthly' + self.assertEqual(invoice.review_state, 'todo') + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertEqual(invoice.review_state, 'todo') + last_recurring = self.env['account.move'].search([('auto_post_origin_id', '=', invoice.id)], limit=1, order='date desc') + self.assertEqual(last_recurring.review_state, 'todo') + + def test_post_move_auto_check_with_auto_post_monthly_sales_prereviewed(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.with_user(self.simple_accountman).auto_post = 'monthly' + self.assertEqual(invoice.review_state, 'todo') + invoice.review_state = 'reviewed' + with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + self.assertEqual(invoice.review_state, 'reviewed') + last_recurring = self.env['account.move'].search([('auto_post_origin_id', '=', invoice.id)], limit=1, order='date desc') + self.assertEqual(last_recurring.review_state, 'reviewed') - def test_post_move_auto_check_with_auto_post(self): - if 'accountant' not in self.env["ir.module.module"]._installed(): - self.skipTest('accountant is not installed') - invoice = self._create_invoice(auto_post='at_date', date=fields.Date.today()) - self.assertFalse(invoice.checked) + def test_post_move_auto_check_with_auto_post_monthly_sales_postreviewed(self): + invoice = self._create_invoice(date=fields.Date.today()) + invoice.with_user(self.simple_accountman).auto_post = 'monthly' + self.assertEqual(invoice.review_state, 'todo') with freeze_time(invoice.date + relativedelta(days=1)), self.enter_registry_test_mode(): self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() - self.assertTrue(invoice.checked) + invoice.review_state = 'reviewed' + last_recurring = self.env['account.move'].search([('auto_post_origin_id', '=', invoice.id)], limit=1, order='date desc') + self.assertEqual(last_recurring.review_state, 'todo') + last_recurring.review_state = 'reviewed' + with freeze_time(invoice.date + relativedelta(days=1, months=1)), self.enter_registry_test_mode(): + self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() + last_recurring = self.env['account.move'].search([('auto_post_origin_id', '=', invoice.id)], limit=1, order='date desc') + self.assertEqual(last_recurring.review_state, 'reviewed') def test_create_statement_line_auto_check(self): + if 'account_accountant' not in self.env["ir.module.module"]._installed(): + self.skipTest('account_accountant is not installed') # required for `_try_auto_reconcile_statement_lines` """Test if a user changes the reconciliation on a st_line, it marks the bank move as 'To Review'""" - if 'accountant' not in self.env["ir.module.module"]._installed(): - self.skipTest('accountant is not installed') payment = self.env['account.payment'].create({ 'payment_type': 'inbound', 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, @@ -76,6 +146,6 @@ def test_create_statement_line_auto_check(self): 'amount': -100, }]) bank_line_1._try_auto_reconcile_statement_lines() - self.assertTrue(bank_line_1.move_id.checked) + self.assertFalse(bank_line_1.move_id.review_state) with self.assertRaisesRegex(ValidationError, 'Validated entries can only be changed by your accountant.'): bank_line_1.with_user(self.simple_accountman).delete_reconciled_line(payment.move_id.line_ids[0].id) diff --git a/addons/account/tests/test_audit_trail.py b/addons/account/tests/test_audit_trail.py index f1e163d610b6e..9b56cf10eb475 100644 --- a/addons/account/tests/test_audit_trail.py +++ b/addons/account/tests/test_audit_trail.py @@ -45,6 +45,28 @@ def assertTrail(self, trail, expected): for message, expected_needle in zip(trail, expected[::-1]): self.assertIn(expected_needle, message.account_audit_log_preview) + def test_can_reset_deferred_invoice(self): + customer = self.env['res.partner'].create({'name': 'Rob Odoo'}) + invoice = self.env['account.move'].create({ + 'invoice_date': '2025-09-01', + 'invoice_date_due': '2025-09-01', + 'invoice_line_ids': [ + (0, 0, {'name': 'Line1', + 'price_unit': 100.0, + 'deferred_start_date': '2025-09-01', + 'deferred_end_date': '2025-12-31', + } + ), + ], + 'move_type': 'out_invoice', + 'name': 'INVOICE_00001', + 'partner_id': customer.id, + }) + + for i in range(4): + invoice.action_post() + invoice.button_draft() + def test_can_unlink_draft(self): self.env.company.restrictive_audit_trail = True self.move.unlink() @@ -86,11 +108,11 @@ def test_content(self): self.assertTrail(self.get_trail(self.move), messages) self.move.action_post() - messages.append("Updated\nFalse ⇨ True (Reviewed)\nFalse ⇨ MISC/2021/04/0001 (Number)\nDraft ⇨ Posted (Status)") + messages.append("Updated\nFalse ⇨ MISC/2021/04/0001 (Number)\nDraft ⇨ Posted (Status)") self.assertTrail(self.get_trail(self.move), messages) self.move.button_draft() - messages.append("Updated\nTrue ⇨ False (Reviewed)\nPosted ⇨ Draft (Status)") + messages.append("Updated\nPosted ⇨ Draft (Status)") self.assertTrail(self.get_trail(self.move), messages) self.move.name = "nawak" diff --git a/addons/account/tests/test_duplicate_res_partner_bank.py b/addons/account/tests/test_duplicate_res_partner_bank.py index 08cbfe0420df6..598b459f112d5 100644 --- a/addons/account/tests/test_duplicate_res_partner_bank.py +++ b/addons/account/tests/test_duplicate_res_partner_bank.py @@ -1,11 +1,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import tagged, Form +from odoo.tests import Form from odoo.addons.base.tests.common import SavepointCaseWithUserDemo -@tagged('at_install', '-post_install') # LEGACY at_install class TestDuplicatePartnerBank(SavepointCaseWithUserDemo): @classmethod @@ -52,3 +51,7 @@ def test_remove_bank_account_from_partner(self): partner_form.bank_ids.remove(0) self.assertEqual(len(partner.bank_ids), 0) + + def test_duplicate_acc_number_inactive_bank_account(self): + self.partner_bank_b.active = False + self.assertFalse(self.partner_bank_a.duplicate_bank_partner_ids) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index e6716ad62350c..52721f10963c3 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -626,7 +626,7 @@ def test_tax_calculation_foreign_currency_large_quantity(self): 2.82 | 20000 | 21% not incl ''' self.env['res.currency.rate'].create({ - 'name': '2018-01-01', + 'name': '2017-12-31', 'rate': 1.1726, 'currency_id': self.other_currency.id, 'company_id': self.env.company.id, @@ -655,7 +655,7 @@ def test_tax_calculation_foreign_currency_large_quantity(self): def test_ensure_no_unbalanced_entry(self): ''' Ensure to not create an unbalanced journal entry when saving. ''' self.env['res.currency.rate'].create({ - 'name': '2018-01-01', + 'name': '2017-12-31', 'rate': 0.654065014, 'currency_id': self.other_currency.id, 'company_id': self.env.company.id, @@ -676,7 +676,7 @@ def test_ensure_no_unbalanced_entry(self): @freeze_time('2018-01-01') def test_tax_calculation_multi_currency(self): self.env['res.currency.rate'].create({ - 'name': '2018-01-01', + 'name': '2017-12-31', 'rate': 0.273748, 'currency_id': self.other_currency.id, 'company_id': self.env.company.id, diff --git a/addons/account/tests/test_kpi_provider.py b/addons/account/tests/test_kpi_provider.py index 924367e6d8284..fd229d7120431 100644 --- a/addons/account/tests/test_kpi_provider.py +++ b/addons/account/tests/test_kpi_provider.py @@ -70,12 +70,12 @@ def test_kpi_summary_reports_posted_but_to_check_moves(self): 'move_type': 'out_invoice', }) move.action_post() - move.checked = False + move.review_state = 'todo' self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), [ {'id': 'account_journal_type.sale', 'name': 'Sales', 'type': 'integer', 'value': 1}, ]) - move.button_set_checked() + move.set_moves_checked() self.assertCountEqual(self.env['kpi.provider'].get_account_kpi_summary(), []) def test_kpi_summary_reports_unreconciled_bank_statements(self): diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index fd1b8b9adbe8f..772c31ad587e5 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -257,7 +257,7 @@
- +
Last Statement @@ -290,6 +290,16 @@
+ +
+
+ + Invalid Statement(s) + +
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index ccde0c99aa2a0..16810abe02a51 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -179,8 +179,8 @@ - - + + @@ -201,7 +201,12 @@ - diff --git a/addons/base_import/static/src/import_model.js b/addons/base_import/static/src/import_model.js index 49d91bc706276..c879d9eb70de1 100644 --- a/addons/base_import/static/src/import_model.js +++ b/addons/base_import/static/src/import_model.js @@ -290,7 +290,7 @@ export class BaseImportModel { } if (!importRes.hasError) { - if (importRes.nextrow) { + if (!isTest && importRes.nextrow) { this._addMessage("warning", [ _t( "Click 'Resume' to proceed with the import, resuming at line %s.", @@ -301,6 +301,7 @@ export class BaseImportModel { } if (isTest) { this._addMessage("info", [_t("Everything seems valid.")]); + this.setOption("skip", 0); } } else { importRes.nextrow = startRow; diff --git a/addons/base_import/static/tests/import_action.test.js b/addons/base_import/static/tests/import_action.test.js index d2c43323a50ef..95a1d5dd48f94 100644 --- a/addons/base_import/static/tests/import_action.test.js +++ b/addons/base_import/static/tests/import_action.test.js @@ -65,6 +65,8 @@ class Partner extends models.Model { }; } +onRpc("has_group", () => true); + defineModels([Partner]); defineActions([ @@ -108,7 +110,7 @@ async function executeImport(data, shouldWait = false) { }, ]; } - if (data[3].skip + 1 < totalRows) { + if (data[3].limit + data[3].skip < totalRows) { res.nextrow = data[3].skip + data[3].limit; } else { res.nextrow = 0; @@ -707,6 +709,7 @@ describe("Import view", () => { }, }); + redirect("/odoo/action-2") await mountWebClient(); onRpc("base_import.import", "parse_preview", ({ route }) => { expect.step(route); @@ -1182,6 +1185,38 @@ describe("Import view", () => { }); }); + test("test in batches then reset starting row", async () => { + patchWithCleanup(ImportAction.prototype, { + get isBatched() { + // make sure the UI displays the batched import options + return true; + }, + }); + + await mountWebClient(); + onRpc("base_import.import", "execute_import", ({ args }) => executeImport(args, true)); + await getService("action").doAction(1); + + // Set and trigger the change of a file for the input + const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" }); + await contains(".o_control_panel_main_buttons .o_import_file").click(); + await setInputFiles([file]); + await animationFrame(); + await contains("input#o_import_batch_limit").edit(1); + await contains(".o_control_panel_main_buttons button:first").click(); + + await animationFrame(); + expect("input#o_import_row_start").toHaveValue("2"); + await animationFrame(); + expect("input#o_import_row_start").toHaveValue("3"); + + await animationFrame(); + expect(".o_import_data_content .alert-info").toHaveText("Everything seems valid."); + expect("input#o_import_row_start").toHaveValue("1", { + message: "the actual import will resume at line 1", + }); + }); + test("relational fields correctly mapped on preview", async () => { await mountWebClient(); onRpc("base_import.import", "parse_preview", ({ args }) => @@ -1413,6 +1448,7 @@ describe("Import view", () => { test("date format should be converted to strftime", async () => { let parseCount = 0; + redirect("/odoo/action-2") await mountWebClient(); onRpc("base_import.import", "parse_preview", async ({ args }) => { parseCount++; @@ -1450,9 +1486,7 @@ describe("Import view", () => { await contains(".o_control_panel_main_buttons button:contains(Import):eq(0)").click(); } expect.verifySteps(["parse_preview", "parse_preview", "execute_import"]); - expect(".o_import_date_format").toHaveValue("YYYYMMDD", { - message: "UI displays the human formatted date", - }); + await waitFor(".o_list_view"); }); }); diff --git a/addons/base_import_module/__manifest__.py b/addons/base_import_module/__manifest__.py index 91680b193f5e9..a1b57db884f51 100644 --- a/addons/base_import_module/__manifest__.py +++ b/addons/base_import_module/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Base import module', @@ -11,7 +10,6 @@ """, 'category': 'Hidden/Tools', 'depends': ['web'], - 'installable': True, 'auto_install': True, 'data': [ 'security/ir.model.access.csv', diff --git a/addons/base_install_request/__manifest__.py b/addons/base_install_request/__manifest__.py index 3486c969fd931..422a28591a996 100644 --- a/addons/base_install_request/__manifest__.py +++ b/addons/base_install_request/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/base_setup/__manifest__.py b/addons/base_setup/__manifest__.py index 92ae94dd346f4..d34741f110a80 100644 --- a/addons/base_setup/__manifest__.py +++ b/addons/base_setup/__manifest__.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Initial Setup Tools', - 'version': '1.0', 'category': 'Hidden', 'description': """ This module helps to configure the system at the installation of a new database. @@ -16,15 +14,13 @@ 'data/base_setup_data.xml', 'views/res_config_settings_views.xml', 'views/res_partner_views.xml', - ], + ], 'assets': { 'web.assets_backend': [ 'base_setup/static/src/views/**/*', ], }, 'auto_install': True, - 'installable': True, - 'author': 'Odoo S.A.', 'license': 'LGPL-3', } diff --git a/addons/base_sparse_field/__manifest__.py b/addons/base_sparse_field/__manifest__.py index c487ecfef4f50..375f8f5989db4 100644 --- a/addons/base_sparse_field/__manifest__.py +++ b/addons/base_sparse_field/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Sparse Fields", 'summary': """Implementation of sparse fields.""", @@ -9,7 +8,6 @@ fields are stored in a "serialized" field in the form of a JSON mapping. """, 'category': 'Hidden', - 'version': '1.0', 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/base_vat/__manifest__.py b/addons/base_vat/__manifest__.py index 1231920ddad64..c48a627c754b7 100644 --- a/addons/base_vat/__manifest__.py +++ b/addons/base_vat/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/board/__manifest__.py b/addons/board/__manifest__.py index b74f13bd9bc51..628fb3950a90c 100644 --- a/addons/board/__manifest__.py +++ b/addons/board/__manifest__.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Dashboards', - 'version': '1.0', 'category': 'Productivity', 'sequence': 225, 'summary': 'Build your own dashboards', diff --git a/addons/bus/__manifest__.py b/addons/bus/__manifest__.py index 2fdfb24697ca6..8eb4fb9920edf 100644 --- a/addons/bus/__manifest__.py +++ b/addons/bus/__manifest__.py @@ -1,13 +1,11 @@ { 'name' : 'IM Bus', - 'version': '1.0', 'category': 'Hidden', 'description': "Instant Messaging Bus allow you to send messages to users, in live.", 'depends': ['base', 'web'], 'data': [ 'security/ir.model.access.csv', ], - 'installable': True, 'auto_install': True, 'assets': { 'web.assets_backend': [ diff --git a/addons/bus/static/src/workers/bus_worker_utils.js b/addons/bus/static/src/workers/bus_worker_utils.js index 2af7afe2ced64..4e2d72145a11f 100644 --- a/addons/bus/static/src/workers/bus_worker_utils.js +++ b/addons/bus/static/src/workers/bus_worker_utils.js @@ -27,6 +27,8 @@ export function debounce(func, wait, immediate) { } /** + * @deprecated Use Promise.withResolvers() instead. + * * Deferred is basically a resolvable/rejectable extension of Promise. */ export class Deferred extends Promise { diff --git a/addons/bus/static/src/workers/websocket_worker.js b/addons/bus/static/src/workers/websocket_worker.js index 1f928c1919132..7dcbfd38ebcd8 100644 --- a/addons/bus/static/src/workers/websocket_worker.js +++ b/addons/bus/static/src/workers/websocket_worker.js @@ -29,6 +29,7 @@ export const WEBSOCKET_CLOSE_CODES = Object.freeze({ SESSION_EXPIRED: 4001, KEEP_ALIVE_TIMEOUT: 4002, RECONNECTING: 4003, + CLOSING_HANDSHAKE_ABORTED: 4004, }); export const WORKER_STATE = Object.freeze({ CONNECTED: "CONNECTED", @@ -50,6 +51,7 @@ const logger = new Logger("bus_websocket_worker"); export class WebsocketWorker { INITIAL_RECONNECT_DELAY = 1000; RECONNECT_JITTER = 1000; + CONNECTION_CHECK_DELAY = 60_000; constructor(name) { this.name = name; @@ -348,6 +350,7 @@ export class WebsocketWorker { * closed. */ _onWebsocketClose({ code, reason }) { + clearInterval(this._connectionCheckInterval); this._logDebug("_onWebsocketClose", code, reason); this._updateState(WORKER_STATE.DISCONNECTED); this.lastChannelSubscription = null; @@ -371,8 +374,15 @@ export class WebsocketWorker { // WebSocket was not closed cleanly, let's try to reconnect. this.broadcast("BUS:RECONNECTING", { closeCode: code }); this.isReconnecting = true; - if (code === WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT) { - // Don't wait to reconnect on keep alive timeout. + if ( + [ + WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT, + WEBSOCKET_CLOSE_CODES.CLOSING_HANDSHAKE_ABORTED, + ].includes(code) + ) { + // Don't wait to reconnect: keep-alive shouldn't be noticed, and the + // closing handshake was aborted because the client explicitly tried + // to connect while the socket was stuck in the closing state. this.connectRetryDelay = 0; } if (code === WEBSOCKET_CLOSE_CODES.SESSION_EXPIRED) { @@ -395,6 +405,7 @@ export class WebsocketWorker { * @param {MessageEvent} messageEv */ _onWebsocketMessage(messageEv) { + this._restartConnectionCheckInterval(); const notifications = JSON.parse(messageEv.data); this._logDebug("_onWebsocketMessage", notifications); this.lastNotificationId = notifications[notifications.length - 1].id; @@ -435,6 +446,27 @@ export class WebsocketWorker { this.messageWaitQueue.forEach((msg) => this.websocket.send(msg)); this.messageWaitQueue = []; }); + this._restartConnectionCheckInterval(); + } + + /** + * Sends a custom application-level message to perform a connection check + * on the WebSocket. + * + * Browsers rely on the OS's TCP mechanism, which can take minutes or + * hours to detect a dead connection. Sending data triggers an immediate + * I/O operation, quickly revealing any network-level failure. This must be + * implemented at the application level because the browser WebSocket API + * does not expose the built-in ping/pong mechanism. + */ + _restartConnectionCheckInterval() { + clearInterval(this._connectionCheckInterval); + this._connectionCheckInterval = setInterval(() => { + if (this._isWebsocketConnected()) { + this.websocket.send(new Uint8Array([0x00])); + this._logDebug("connection_checked"); + } + }, this.CONNECTION_CHECK_DELAY); } /** @@ -474,6 +506,7 @@ export class WebsocketWorker { } else { this.firstSubscribeDeferred.then(() => this.websocket.send(payload)); } + this._restartConnectionCheckInterval(); } } @@ -494,10 +527,13 @@ export class WebsocketWorker { } this._removeWebsocketListeners(); if (this._isWebsocketClosing()) { - // close event was not triggered and will never be, broadcast the - // disconnect event for consistency sake. - this.lastChannelSubscription = null; - this.broadcast("BUS:DISCONNECT", { code: WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE }); + // The close event didn’t trigger. Trigger manually to maintain + // correct state and lifecycle handling. + this._onWebsocketClose( + new CloseEvent("close", { code: WEBSOCKET_CLOSE_CODES.CLOSING_HANDSHAKE_ABORTED }) + ); + this.websocket = null; + return; } this._updateState(WORKER_STATE.CONNECTING); this.websocket = new WebSocket(this.websocketURL); diff --git a/addons/bus/static/tests/assets_watchdog.test.js b/addons/bus/static/tests/assets_watchdog.test.js index 14fe9fb424db5..1604b1263dc8d 100644 --- a/addons/bus/static/tests/assets_watchdog.test.js +++ b/addons/bus/static/tests/assets_watchdog.test.js @@ -17,7 +17,7 @@ test("can listen on bus and display notifications in DOM", async () => { }); await expect.waitForSteps(["bundle_changed"]); await runAllTimers(); - await waitFor(".o_notification", { text: "The page appears to be out of date." }); + await waitFor(".o_notification", { contains: "The page appears to be out of date." }); await contains(".o_notification button:contains(Refresh)").click(); await expect.waitForSteps(["reload-page"]); }); diff --git a/addons/bus/static/tests/bus_service.test.js b/addons/bus/static/tests/bus_service.test.js index 228ae85859dc5..532e92216db3f 100644 --- a/addons/bus/static/tests/bus_service.test.js +++ b/addons/bus/static/tests/bus_service.test.js @@ -471,7 +471,7 @@ test("show notification when version is outdated", async () => { await expect.waitForSteps(["Worker deactivated due to an outdated version.", "BUS:DISCONNECT"]); await runAllTimers(); await waitFor(".o_notification", { - text: "Save your work and refresh to get the latest updates and avoid potential issues.", + contains: "Save your work and refresh to get the latest updates and avoid potential issues.", }); await contains(".o_notification button:contains(Refresh)").click(); await expect.waitForSteps(["reload"]); diff --git a/addons/bus/static/tests/mock_websocket.js b/addons/bus/static/tests/mock_websocket.js index cddfec3515400..7aa2903caaac6 100644 --- a/addons/bus/static/tests/mock_websocket.js +++ b/addons/bus/static/tests/mock_websocket.js @@ -92,6 +92,17 @@ patch(MockServer.prototype, { patch(WebsocketWorker.prototype, { INITIAL_RECONNECT_DELAY: 0, RECONNECT_JITTER: 5, + // `runAllTimers` advances time based on the longest registered timeout. + // Some tests rely on the fragile assumption that time won’t advance too much. + // Disable the interval until those tests are rewritten to be more robust. + enableCheckInterval: false, + + _restartConnectionCheckInterval() { + if (this.enableCheckInterval) { + super._restartConnectionCheckInterval(...arguments); + } + }, + _sendToServer(message) { const { env } = MockServer; if (!env) { diff --git a/addons/bus/static/tests/websocket_worker.test.js b/addons/bus/static/tests/websocket_worker.test.js index 9693e0edbc56d..6daf23c5cf4d4 100644 --- a/addons/bus/static/tests/websocket_worker.test.js +++ b/addons/bus/static/tests/websocket_worker.test.js @@ -1,9 +1,9 @@ import { getWebSocketWorker } from "@bus/../tests/mock_websocket"; -import { describe, expect, test } from "@odoo/hoot"; +import { advanceTime, describe, expect, test } from "@odoo/hoot"; import { runAllTimers } from "@odoo/hoot-dom"; import { makeMockServer, MockServer, patchWithCleanup } from "@web/../tests/web_test_helpers"; -import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker"; +import { WEBSOCKET_CLOSE_CODES, WebsocketWorker } from "@bus/workers/websocket_worker"; describe.current.tags("headless"); @@ -99,3 +99,48 @@ test("disconnect event is sent when stopping the worker", async () => { await runAllTimers(); await expect.waitForSteps(["broadcast BUS:DISCONNECT"]); }); + +test("check connection health during inactivity", async () => { + const ogSocket = window.WebSocket; + let waitingForCheck = true; + patchWithCleanup(window, { + WebSocket: function () { + const ws = new ogSocket(...arguments); + ws.send = (message) => { + if (waitingForCheck && message instanceof Uint8Array) { + expect.step("check_connection_health_sent"); + waitingForCheck = false; + } + }; + return ws; + }, + }); + patchWithCleanup(WebsocketWorker.prototype, { + enableCheckInterval: true, + _restartConnectionCheckInterval() { + expect.step("_restartConnectionCheckInterval"); + super._restartConnectionCheckInterval(); + }, + _sendToServer(payload) { + if (payload.event_name === "foo") { + super._sendToServer(payload); + } + }, + }); + const worker = await startWebSocketWorker((type) => { + if (type === "BUS:CONNECT") { + expect.step(`broadcast ${type}`); + } + }); + await expect.waitForSteps(["broadcast BUS:CONNECT", "_restartConnectionCheckInterval"]); + worker.websocket.dispatchEvent( + new MessageEvent("message", { + data: JSON.stringify([{ id: 70, message: { type: "foo" } }]), + }) + ); + await expect.waitForSteps(["_restartConnectionCheckInterval"]); + worker._sendToServer({ event_name: "foo" }); + await expect.waitForSteps(["_restartConnectionCheckInterval"]); + await advanceTime(worker.CONNECTION_CHECK_DELAY + 1000); + await expect.waitForSteps(["check_connection_health_sent"]); +}); diff --git a/addons/bus/tests/test_websocket_caryall.py b/addons/bus/tests/test_websocket_caryall.py index 622c4284233d1..858f3092e55f0 100644 --- a/addons/bus/tests/test_websocket_caryall.py +++ b/addons/bus/tests/test_websocket_caryall.py @@ -125,7 +125,13 @@ def test_user_logout_incoming_message(self): new_test_user(self.env, login='test_user', password='Password!1') user_session = self.authenticate('test_user', 'Password!1') websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};') - self.url_open('/web/session/logout') + self.url_open( + '/web/session/logout', + method='POST', + data={ + "csrf_token": http.Request.csrf_token(self), + }, + ) # The session with whom the websocket connected has been # deleted. WebSocket should disconnect in order for the # session to be updated. @@ -137,7 +143,13 @@ def test_user_logout_outgoing_message(self): user_session = self.authenticate('test_user', 'Password!1') websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};') self.subscribe(websocket, ['channel1'], self.env['bus.bus']._bus_last_id()) - self.url_open('/web/session/logout') + self.url_open( + '/web/session/logout', + method='POST', + data={ + "csrf_token": http.Request.csrf_token(self), + }, + ) # Simulate postgres notify. The session with whom the websocket # connected has been deleted. WebSocket should be closed without # receiving the message. diff --git a/addons/bus/tests/test_websocket_controller.py b/addons/bus/tests/test_websocket_controller.py index f1cec42ee052e..8869e84bca765 100644 --- a/addons/bus/tests/test_websocket_controller.py +++ b/addons/bus/tests/test_websocket_controller.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import http from odoo.tests import tagged, JsonRpcException from odoo.addons.base.tests.common import HttpCaseWithUserDemo @@ -57,7 +58,12 @@ def test_websocket_peek_session_expired_logout(self): 'last': 0, 'is_first_poll': True, }) - self.url_open('/web/session/logout') + self.url_open('/web/session/logout', + method='POST', + data={ + "csrf_token": http.Request.csrf_token(self), + }, + ) # rpc with outdated session should lead to error. with self.assertRaises(JsonRpcException, msg='odoo.http.SessionExpiredException'): self.make_jsonrpc_request('/websocket/peek_notifications', { diff --git a/addons/bus/websocket.py b/addons/bus/websocket.py index 2661c5a6774b5..2a4f4ae3ab8b9 100644 --- a/addons/bus/websocket.py +++ b/addons/bus/websocket.py @@ -962,7 +962,7 @@ class WebsocketConnectionHandler: # Latest version of the websocket worker. This version should be incremented # every time `websocket_worker.js` is modified to force the browser to fetch # the new worker bundle. - _VERSION = "19.0-1" + _VERSION = "19.0-2" @classmethod def websocket_allowed(cls, request): @@ -1105,6 +1105,9 @@ def _serve_forever(cls, websocket, db, httprequest, version): # worker version. websocket.close(CloseCode.CLEAN, "OUTDATED_VERSION") for message in websocket.get_messages(): + if message == b'\x00': + # Ignore internal sentinel message used to detect dead/idle connections. + continue with WebsocketRequest(db, httprequest, websocket) as req: try: req.serve_websocket_message(message) diff --git a/addons/calendar/__manifest__.py b/addons/calendar/__manifest__.py index 3d14d9e3d6f90..b360571b21952 100644 --- a/addons/calendar/__manifest__.py +++ b/addons/calendar/__manifest__.py @@ -39,7 +39,6 @@ 'wizard/calendar_popover_delete_wizard.xml', 'wizard/mail_activity_schedule_views.xml', ], - 'installable': True, 'application': True, 'assets': { 'web.assets_backend': [ diff --git a/addons/calendar/data/mail_template_data.xml b/addons/calendar/data/mail_template_data.xml index 0413e5d19887c..a69318809bd98 100644 --- a/addons/calendar/data/mail_template_data.xml +++ b/addons/calendar/data/mail_template_data.xml @@ -37,13 +37,13 @@

@@ -167,13 +167,13 @@

- Accept - Decline - View
@@ -274,13 +274,13 @@ This is a reminder for the event below.

- Accept - Decline - View
diff --git a/addons/calendar/models/calendar_attendee.py b/addons/calendar/models/calendar_attendee.py index 7faef8ea26b45..15c16c43d3cd5 100644 --- a/addons/calendar/models/calendar_attendee.py +++ b/addons/calendar/models/calendar_attendee.py @@ -38,7 +38,7 @@ def _default_access_token(self): email = fields.Char('Email', related='partner_id.email') phone = fields.Char('Phone', related='partner_id.phone') common_name = fields.Char('Common name', compute='_compute_common_name', store=True) - access_token = fields.Char('Invitation Token', default=_default_access_token) + access_token = fields.Char('Invitation Token', default=_default_access_token, groups="base.group_system") mail_tz = fields.Selection(_tz_get, compute='_compute_mail_tz', help='Timezone used for displaying time in the mail template') # state state = fields.Selection(STATE_SELECTION, string='Status', default='needsAction') diff --git a/addons/calendar/models/mail_activity.py b/addons/calendar/models/mail_activity.py index 08fe093e0cf6f..2bd33d474daab 100644 --- a/addons/calendar/models/mail_activity.py +++ b/addons/calendar/models/mail_activity.py @@ -20,7 +20,7 @@ def write(self, vals): date_deadline = self[0].date_deadline # updated, hence all same value # also protect against loops in case of ill-managed timezones events = self.calendar_event_id.with_context(mail_activity_meeting_update=True) - user_tz = self.env.context.get('tz', 'UTC') + user_tz = self.env.context.get('tz') or 'UTC' for event in events: # allday: just apply diff between dates if event.allday and event.start_date != date_deadline: diff --git a/addons/calendar/models/mail_activity_mixin.py b/addons/calendar/models/mail_activity_mixin.py index 495fb629aa320..9eac3e2dc1e30 100644 --- a/addons/calendar/models/mail_activity_mixin.py +++ b/addons/calendar/models/mail_activity_mixin.py @@ -16,5 +16,4 @@ def _compute_activity_calendar_event_id(self): It evaluates to false if there is no such event.""" for record in self: activities = record.activity_ids - activity = next(iter(activities), activities) - record.activity_calendar_event_id = activity.calendar_event_id + record.activity_calendar_event_id = activities[:1].calendar_event_id diff --git a/addons/calendar/static/src/views/attendee_calendar/attendee_calendar_controller.xml b/addons/calendar/static/src/views/attendee_calendar/attendee_calendar_controller.xml index d58eb0db9615a..928da9afca217 100644 --- a/addons/calendar/static/src/views/attendee_calendar/attendee_calendar_controller.xml +++ b/addons/calendar/static/src/views/attendee_calendar/attendee_calendar_controller.xml @@ -2,7 +2,7 @@ - + diff --git a/addons/calendar/static/src/views/calendar_form/calendar_quick_create.js b/addons/calendar/static/src/views/calendar_form/calendar_quick_create.js index 60c9336707252..14ac3097da720 100644 --- a/addons/calendar/static/src/views/calendar_form/calendar_quick_create.js +++ b/addons/calendar/static/src/views/calendar_form/calendar_quick_create.js @@ -82,7 +82,7 @@ export class CalendarQuickCreate extends FormViewDialog { super.setup(); Object.assign(this.viewProps, { ...this.viewProps, - buttonTemplate: "calendar.CalendarQuickCreateButtons", + buttonDialogTemplate: "calendar.CalendarQuickCreateButtons", }); } } diff --git a/addons/calendar/static/src/views/fields/attendee_tags_list.js b/addons/calendar/static/src/views/fields/attendee_tags_list.js deleted file mode 100644 index b298031d2e565..0000000000000 --- a/addons/calendar/static/src/views/fields/attendee_tags_list.js +++ /dev/null @@ -1,5 +0,0 @@ -import { TagsList } from "@web/core/tags_list/tags_list"; - -export class AttendeeTagsList extends TagsList { - static template = "calendar.AttendeeTagsList"; -} diff --git a/addons/calendar/static/src/views/fields/attendee_tags_list.xml b/addons/calendar/static/src/views/fields/attendee_tags_list.xml deleted file mode 100644 index 5fe3053876982..0000000000000 --- a/addons/calendar/static/src/views/fields/attendee_tags_list.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - -
- -
-
- - - -
- -
-
- -
-
- - - - - - diff --git a/addons/calendar/static/src/views/fields/many2many_attendee.js b/addons/calendar/static/src/views/fields/many2many_attendee.js index 3a55606cc8082..cba6e623b344a 100644 --- a/addons/calendar/static/src/views/fields/many2many_attendee.js +++ b/addons/calendar/static/src/views/fields/many2many_attendee.js @@ -1,10 +1,10 @@ +import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { Many2ManyTagsAvatarField, many2ManyTagsAvatarField, } from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field"; import { useSpecialData } from "@web/views/fields/relational_utils"; -import { AttendeeTagsList } from "@calendar/views/fields/attendee_tags_list"; import { ConnectionLostError } from "@web/core/network/rpc"; const ICON_BY_STATUS = { @@ -12,11 +12,21 @@ const ICON_BY_STATUS = { declined: "fa-times", tentative: "fa-question", }; + +export class AttendeeTag extends Component { + static template = "calendar.AttendeeTag"; + static props = ["imageUrl", "isUnavailable?", "noEmail?", "onDelete?", "status?", "text", "tooltip"]; + + get statusIcon() { + return ICON_BY_STATUS[this.props.status]; + } +} + export class Many2ManyAttendee extends Many2ManyTagsAvatarField { static template = "calendar.Many2ManyAttendee"; static components = { - ...Many2ManyAttendee.components, - TagsList: AttendeeTagsList, + ...super.components, + Tag: AttendeeTag, }; setup() { super.setup(); @@ -40,37 +50,46 @@ export class Many2ManyAttendee extends Many2ManyTagsAvatarField { }); } - get tags() { - const partnerIds = this.specialData.data; + getTagProps(record) { + const tag = super.getTagProps(record); + const result = { + text: tag.text, + tooltip: tag.tooltip, + imageUrl: tag.imageUrl, + onDelete: tag.onDelete, + }; + + const partner = this.specialData.data.find((partner) => record.resId === partner.id); + if (partner) { + result.status = partner.status; + } + const noEmailPartnerIds = this.props.record.data.invalid_email_partner_ids ? this.props.record.data.invalid_email_partner_ids.records : []; + const noEmail = noEmailPartnerIds.find( + (noEmailPartner) => record.resId == noEmailPartner.resId + ); + if (noEmail) { + result.noEmail = true; + } + const unavailablePartnerIds = this.props.record.data.unavailable_partner_ids ? this.props.record.data.unavailable_partner_ids.records : []; - const tags = super.tags.map((tag) => { - const partner = partnerIds.find((partner) => tag.resId === partner.id); - const noEmail = noEmailPartnerIds.find( - (noEmailPartner) => tag.resId == noEmailPartner.resId - ); - if (partner) { - tag.status = partner.status; - tag.statusIcon = ICON_BY_STATUS[partner.status]; - } - if (noEmail) { - tag.noEmail = true; - } - if ( - unavailablePartnerIds.find( - (unavailablePartner) => tag.resId == unavailablePartner.resId - ) - ) { - tag.isUnavailable = true; - } - return tag; - }); + if ( + unavailablePartnerIds.find( + (unavailablePartner) => record.resId == unavailablePartner.resId + ) + ) { + result.isUnavailable = true; + } + return result; + } - const organizer = partnerIds.find((partner) => partner.is_organizer); + get tags() { + const tags = super.tags; + const organizer = this.specialData.data.find((partner) => partner.is_organizer); if (organizer) { const orgId = organizer.id; // sort elements according to the partner id diff --git a/addons/calendar/static/src/views/fields/attendee_tags_list.scss b/addons/calendar/static/src/views/fields/many2many_attendee.scss similarity index 100% rename from addons/calendar/static/src/views/fields/attendee_tags_list.scss rename to addons/calendar/static/src/views/fields/many2many_attendee.scss diff --git a/addons/calendar/static/src/views/fields/many2many_attendee.xml b/addons/calendar/static/src/views/fields/many2many_attendee.xml index b8ccac2948580..01a113cfd0898 100644 --- a/addons/calendar/static/src/views/fields/many2many_attendee.xml +++ b/addons/calendar/static/src/views/fields/many2many_attendee.xml @@ -8,4 +8,25 @@ props.placeholder + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + + diff --git a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js index 656f247442247..11c35d3c1935d 100644 --- a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js +++ b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js @@ -30,6 +30,10 @@ export class Many2ManyAttendeeExpandable extends Many2ManyAttendee { } } + get visibleItemsLimit() { + return this.state.expanded ? Number.POSITIVE_INFINITY : 5; + } + onExpanderClick() { this.state.expanded = !this.state.expanded; } diff --git a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.xml b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.xml index 3a75f149770f9..523de1b82cf43 100644 --- a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.xml +++ b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.xml @@ -3,13 +3,10 @@ - - - - state.expanded ? tags.length : 5 + - +
attendees @@ -22,7 +19,7 @@ - + $0 diff --git a/addons/calendar/static/tests/attendee_calendar_views.test.js b/addons/calendar/static/tests/attendee_calendar_views.test.js index d53b105b0a5bd..c2a761b648900 100644 --- a/addons/calendar/static/tests/attendee_calendar_views.test.js +++ b/addons/calendar/static/tests/attendee_calendar_views.test.js @@ -117,7 +117,7 @@ test("Linked record rendering", async () => { res_model_id: modelId, }); await mountView({ type: "calendar", resModel: "calendar.event", arch }); - expect(".o_calendar_renderer .fc-view").toHaveCount(1); + expect(".o_calendar_renderer .o_calendar_current .fc-view").toHaveCount(1); await changeScale("week"); await clickEvent(eventId); diff --git a/addons/calendar_sms/__manifest__.py b/addons/calendar_sms/__manifest__.py index d6b0501eaaff4..956a8dc2dfd59 100644 --- a/addons/calendar_sms/__manifest__.py +++ b/addons/calendar_sms/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/certificate/__manifest__.py b/addons/certificate/__manifest__.py index 95e5c585e1693..088b9162430c6 100644 --- a/addons/certificate/__manifest__.py +++ b/addons/certificate/__manifest__.py @@ -3,7 +3,6 @@ 'version': '0.1', 'category': 'Hidden/Tools', 'summary': 'Manage certificate', - 'installable': True, 'data': [ 'security/ir.model.access.csv', 'security/certificate_security.xml', @@ -14,5 +13,5 @@ ], 'depends': ['base_setup'], 'author': 'Odoo S.A.', - 'license': 'OEEL-1', + 'license': 'LGPL-3', } diff --git a/addons/cloud_storage/__manifest__.py b/addons/cloud_storage/__manifest__.py index 309c20594cf15..04fb1c67461e6 100644 --- a/addons/cloud_storage/__manifest__.py +++ b/addons/cloud_storage/__manifest__.py @@ -4,7 +4,6 @@ "name": "Cloud Storage", "summary": """Store chatter attachments in the cloud""", "category": "Technical Settings", - "version": "1.0", "depends": ["base_setup", "mail"], "data": [ "views/settings.xml", diff --git a/addons/cloud_storage/static/src/core/common/attachment_upload_service_patch.js b/addons/cloud_storage/static/src/core/common/attachment_upload_service_patch.js index 0ffc07601fb88..2a28c7f1bc876 100644 --- a/addons/cloud_storage/static/src/core/common/attachment_upload_service_patch.js +++ b/addons/cloud_storage/static/src/core/common/attachment_upload_service_patch.js @@ -85,6 +85,7 @@ patch(AttachmentUploadService.prototype, { async _upload(thread, composer, file, options, tmpId, tmpURL) { if ( + !thread.channel?.ai_agent_id && // ai_agent (enterprise) does not support url files session.cloud_storage_min_file_size !== undefined && file.size > session.cloud_storage_min_file_size && !session.cloud_storage_unsupported_models.includes(thread.model) diff --git a/addons/cloud_storage/static/src/core/common/composer_patch.js b/addons/cloud_storage/static/src/core/common/composer_patch.js index 5256a68fe1cfc..5906ba4e93558 100644 --- a/addons/cloud_storage/static/src/core/common/composer_patch.js +++ b/addons/cloud_storage/static/src/core/common/composer_patch.js @@ -4,8 +4,10 @@ import { patch } from "@web/core/utils/patch"; import { session } from "@web/session"; patch(Composer.prototype, { - setup() { - super.setup(); - this.cloudStorageUsable = session.cloud_storage_min_file_size !== undefined; + get cloudStorageUsable() { + return ( + !this.thread?.channel?.ai_agent_id && // ai_agent (enterprise) does not support url files + session.cloud_storage_min_file_size !== undefined + ); }, }); diff --git a/addons/cloud_storage_azure/__manifest__.py b/addons/cloud_storage_azure/__manifest__.py index 6be24af978dad..995ac54f1d815 100644 --- a/addons/cloud_storage_azure/__manifest__.py +++ b/addons/cloud_storage_azure/__manifest__.py @@ -4,7 +4,6 @@ "name": "Cloud Storage Azure", "summary": """Store chatter attachments in the Azure cloud""", "category": "Technical Settings", - "version": "1.0", "depends": ["cloud_storage"], "data": [ "views/settings.xml", diff --git a/addons/cloud_storage_google/__manifest__.py b/addons/cloud_storage_google/__manifest__.py index 1336dcff1c5bc..e95ec4a8d6b3b 100644 --- a/addons/cloud_storage_google/__manifest__.py +++ b/addons/cloud_storage_google/__manifest__.py @@ -4,7 +4,6 @@ "name": "Cloud Storage Google", "summary": """Store chatter attachments in the Google cloud""", "category": "Technical Settings", - "version": "1.0", "depends": ["cloud_storage"], "data": [ "views/settings.xml", diff --git a/addons/contacts/__manifest__.py b/addons/contacts/__manifest__.py index d82c4fd7d41c1..fab05e43ec4e2 100644 --- a/addons/contacts/__manifest__.py +++ b/addons/contacts/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 499c3986b0666..fa3fd8637462e 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. @@ -67,7 +66,6 @@ 'data/crm_team_member_demo.xml', 'data/crm_lead_demo.xml', ], - 'installable': True, 'application': True, 'assets': { 'web.assets_backend': [ diff --git a/addons/crm/static/src/core/common/crm_lead_model.js b/addons/crm/static/src/core/common/crm_lead_model.js index 1660c36aedf35..b5728de39baad 100644 --- a/addons/crm/static/src/core/common/crm_lead_model.js +++ b/addons/crm/static/src/core/common/crm_lead_model.js @@ -1,4 +1,4 @@ -import { fields, Record } from "@mail/core/common/record"; +import { fields, Record } from "@mail/model/export"; import { router } from "@web/core/browser/router"; export class CrmLead extends Record { diff --git a/addons/crm/static/src/core/common/res_partner_model_patch.js b/addons/crm/static/src/core/common/res_partner_model_patch.js index b190a901d54c0..d835aa7314ec1 100644 --- a/addons/crm/static/src/core/common/res_partner_model_patch.js +++ b/addons/crm/static/src/core/common/res_partner_model_patch.js @@ -1,5 +1,5 @@ import { ResPartner } from "@mail/core/common/res_partner_model"; -import { fields } from "@mail/core/common/record"; +import { fields } from "@mail/model/export"; import { patch } from "@web/core/utils/patch"; diff --git a/addons/crm/tests/test_crm_lead_convert.py b/addons/crm/tests/test_crm_lead_convert.py index 7528825bd6799..aea1539682a5d 100644 --- a/addons/crm/tests/test_crm_lead_convert.py +++ b/addons/crm/tests/test_crm_lead_convert.py @@ -383,6 +383,31 @@ def test_lead_convert_same_partner(self): self.assertEqual(lead.city, 'my city', 'City should be preserved during conversion') self.assertEqual(partner.lang, 'en_US') + def test_lead_convert_same_team(self): + """Check that the team_id field of the 'crm.lead2opportunity.partner' form is pre-filled with the team of the + lead that must be converted and not with the default one of its user.""" + lead = self.env['crm.lead'].create({ + 'name': 'Convert Same Team LEAD', + 'type': 'lead', + 'user_id': self.user_sales_manager.id, + 'team_id': self.env['crm.team'].create({ + 'name': 'Convert Sales Team 2', + 'user_id': self.user_sales_manager.id, + }).id, + }) + wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({ + 'active_model': 'crm.lead', + 'active_id': lead.id, + 'active_ids': lead.ids, + })) + # Check that the team_id field of the wizard is pre-filled with the lead's team and ensure that it is not + # because it is the default team of the user. + self.assertEqual(lead.team_id, wizard.team_id) + self.assertNotEqual( + self.env['crm.team']._get_default_team_id(user_id=self.user_sales_manager.id), + lead.team_id + ) + @users('user_sales_manager') def test_lead_convert_properties_preserve(self): """Verify that the properties are preserved when converting.""" diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index b6d7a725c5303..ac73d46717732 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -367,11 +367,8 @@ def test_new_lead_from_email_multicompany(self): --000000000000a47519057e029630-- """ - crm_lead0_id = self.env['mail.thread'].message_process('crm.lead', new_message0) - crm_lead1_id = self.env['mail.thread'].message_process('crm.lead', new_message1) - - crm_lead0 = self.env['crm.lead'].browse(crm_lead0_id) - crm_lead1 = self.env['crm.lead'].browse(crm_lead1_id) + crm_lead0 = self.env['mail.thread'].message_process('crm.lead', new_message0) + crm_lead1 = self.env['mail.thread'].message_process('crm.lead', new_message1) self.assertEqual(crm_lead0.team_id, crm_team0) self.assertEqual(crm_lead1.team_id, crm_team1) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index fcde3cf1856ef..859de2db2240f 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -1201,6 +1201,7 @@ Pipeline + crm crm.lead kanban,list,graph,pivot,form,calendar,activity [('type','=','opportunity')] diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml index 149aa1163c66c..b3bbdbf10962f 100644 --- a/addons/crm/views/res_config_settings_views.xml +++ b/addons/crm/views/res_config_settings_views.xml @@ -31,7 +31,7 @@ Install Extension - + diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 45d73e08a4d36..c2de927259112 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -104,7 +104,7 @@ def _compute_user_id(self): for convert in self: convert.user_id = convert.lead_id.user_id if convert.lead_id.user_id else False - @api.depends('user_id') + @api.depends('lead_id', 'user_id') def _compute_team_id(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ @@ -115,6 +115,9 @@ def _compute_team_id(self): user = convert.user_id if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id: continue + elif user in convert.lead_id.team_id.member_ids | convert.lead_id.team_id.user_id: + convert.team_id = convert.lead_id.team_id + continue team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=None) convert.team_id = team.id diff --git a/addons/crm/wizard/crm_lead_to_opportunity_mass.py b/addons/crm/wizard/crm_lead_to_opportunity_mass.py index 4c9a715b62218..d312fa08cf667 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_mass.py +++ b/addons/crm/wizard/crm_lead_to_opportunity_mass.py @@ -43,7 +43,7 @@ def _compute_commercial_partner_id(self): """Setting a company for each lead in mass mode is not supported.""" self.commercial_partner_id = False - @api.depends('user_ids') + @api.depends('lead_id', 'user_ids') def _compute_team_id(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ @@ -54,6 +54,9 @@ def _compute_team_id(self): user = convert.user_id or convert.user_ids and convert.user_ids[0] or self.env.user if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id: continue + elif user in convert.lead_id.team_id.member_ids | convert.lead_id.team_id.user_id: + convert.team_id = convert.lead_id.team_id + continue team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=None) convert.team_id = team.id diff --git a/addons/crm_iap_enrich/__manifest__.py b/addons/crm_iap_enrich/__manifest__.py index 2a7c7fce04c94..55f5a71a713a3 100644 --- a/addons/crm_iap_enrich/__manifest__.py +++ b/addons/crm_iap_enrich/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/crm_iap_mine/__manifest__.py b/addons/crm_iap_mine/__manifest__.py index 576ec4e178a64..159f255491f68 100644 --- a/addons/crm_iap_mine/__manifest__.py +++ b/addons/crm_iap_mine/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/crm_livechat/__manifest__.py b/addons/crm_livechat/__manifest__.py index d851d57e7637a..fa35aa140993f 100644 --- a/addons/crm_livechat/__manifest__.py +++ b/addons/crm_livechat/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { diff --git a/addons/crm_livechat/static/src/core/thread_actions.js b/addons/crm_livechat/static/src/core/thread_actions.js index 4dc7ade67f9e5..66b8841ac7cea 100644 --- a/addons/crm_livechat/static/src/core/thread_actions.js +++ b/addons/crm_livechat/static/src/core/thread_actions.js @@ -9,18 +9,19 @@ import { usePopover } from "@web/core/popover/popover_hook"; registerThreadAction("create-lead", { actionPanelClose: ({ action }) => action.popover?.close(), actionPanelComponent: LivechatCommandDialog, - actionPanelComponentProps: ({ action }) => ({ + actionPanelComponentProps: ({ action, thread }) => ({ close: () => action.actionPanelClose(), commandName: "lead", placeholderText: _t("e.g. Product pricing"), + thread, title: _t("Create Lead"), icon: "fa fa-handshake-o", }), actionPanelOpen({ owner, thread }) { - this.popover?.open(owner.root.el.querySelector(`[name="${this.id}"]`), { - thread, - ...this.actionPanelComponentProps, - }); + this.popover?.open( + owner.root.el.querySelector(`[name="${this.id}"]`), + this.actionPanelComponentProps + ); }, actionPanelOuterClass: "bg-100", condition: false, // managed by ThreadAction patch diff --git a/addons/crm_mail_plugin/__manifest__.py b/addons/crm_mail_plugin/__manifest__.py index bc496eeb948fa..b54ee41de44e1 100644 --- a/addons/crm_mail_plugin/__manifest__.py +++ b/addons/crm_mail_plugin/__manifest__.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'CRM Mail Plugin', - 'version': '1.0', 'category': 'Sales/CRM', 'sequence': 5, 'summary': 'Turn emails received in your mailbox into leads and log their content as internal notes.', @@ -17,7 +15,6 @@ 'views/crm_mail_plugin_lead.xml', 'views/crm_lead_views.xml' ], - 'installable': True, 'auto_install': True, 'author': 'Odoo S.A.', 'license': 'LGPL-3', diff --git a/addons/crm_sms/__manifest__.py b/addons/crm_sms/__manifest__.py index 7d4549717359b..b8e7184fff271 100644 --- a/addons/crm_sms/__manifest__.py +++ b/addons/crm_sms/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { @@ -12,7 +11,6 @@ 'security/ir.model.access.csv', 'security/sms_security.xml', ], - 'installable': True, 'auto_install': True, 'author': 'Odoo S.A.', 'license': 'LGPL-3', diff --git a/addons/data_recycle/__manifest__.py b/addons/data_recycle/__manifest__.py index 4b1543579c1f0..16ee1be702f69 100644 --- a/addons/data_recycle/__manifest__.py +++ b/addons/data_recycle/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { @@ -16,7 +15,6 @@ 'views/data_recycle_templates.xml', 'security/ir.model.access.csv', ], - 'installable': True, 'application': True, 'assets': { 'web.assets_backend': [ diff --git a/addons/delivery/__manifest__.py b/addons/delivery/__manifest__.py index a81fba96f9c33..c54c12cdfa795 100644 --- a/addons/delivery/__manifest__.py +++ b/addons/delivery/__manifest__.py @@ -2,7 +2,6 @@ { 'name': 'Delivery Costs', - 'version': '1.0', 'category': 'Sales/Delivery', 'description': """ Allows you to add delivery methods in sale orders. @@ -41,7 +40,6 @@ }, 'post_init_hook': 'post_init_hook', 'uninstall_hook': 'uninstall_hook', - 'installable': True, 'author': 'Odoo S.A.', 'license': 'LGPL-3', } diff --git a/addons/delivery/data/neutralize.sql b/addons/delivery/data/neutralize.sql index d8fedac97ddaf..313a4c47e3627 100644 --- a/addons/delivery/data/neutralize.sql +++ b/addons/delivery/data/neutralize.sql @@ -1,7 +1,7 @@ --- disable prod environment in all delivery carriers +-- disable prod environment in all delivery methods UPDATE delivery_carrier SET prod_environment = false; --- disable delivery carriers from external providers +-- disable delivery methods from external providers UPDATE delivery_carrier SET active = false WHERE delivery_type NOT IN ('fixed', 'base_on_rule'); diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index 2f9de9182a389..c102a7bdf7704 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -12,7 +12,7 @@ class DeliveryCarrier(models.Model): _name = 'delivery.carrier' - _description = "Shipping Method" + _description = "Delivery Method" _order = 'sequence, id' ''' A Shipping Provider @@ -69,7 +69,7 @@ class DeliveryCarrier(models.Model): state_ids = fields.Many2many('res.country.state', 'delivery_carrier_state_rel', 'carrier_id', 'state_id', 'States') zip_prefix_ids = fields.Many2many( 'delivery.zip.prefix', 'delivery_zip_prefix_rel', 'carrier_id', 'zip_prefix_id', 'Zip Prefixes', - help="Prefixes of zip codes that this carrier applies to. Note that regular expressions can be used to support countries with varying zip code lengths, i.e. '$' can be added to end of prefix to match the exact zip (e.g. '100$' will only match '100' and not '1000')") + help="Prefixes of zip codes that this delivery method applies to. Note that regular expressions can be used to support countries with varying zip code lengths, i.e. '$' can be added to end of prefix to match the exact zip (e.g. '100$' will only match '100' and not '1000')") max_weight = fields.Float('Max Weight', help="If the total weight of the order is over this weight, the method won't be available.") weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name') @@ -81,7 +81,7 @@ class DeliveryCarrier(models.Model): help="The method is NOT available if at least one product of the order has one of these tags.") carrier_description = fields.Text( - 'Carrier Description', translate=True, + 'Description', translate=True, help="A description of the delivery method that you want to communicate to your customers on the Sales Order and sales confirmation email." "E.g. instructions for customers to follow.") @@ -122,7 +122,7 @@ class DeliveryCarrier(models.Model): def _check_tags(self): for carrier in self: if carrier.must_have_tag_ids & carrier.excluded_tag_ids: - raise UserError(_("Carrier %s cannot have the same tag in both Must Have Tags and Excluded Tags.") % carrier.name) + raise UserError(_("Delivery method %(name)s cannot have the same tag in both Must Have Tags and Excluded Tags."), name=carrier.name) def _compute_weight_uom_name(self): self.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() @@ -286,7 +286,7 @@ def copy_data(self, default=None): def _get_delivery_type(self): """Return the delivery type. - This method needs to be overridden by a delivery carrier module if the delivery type is not + This method needs to be overridden by a delivery method module if the delivery type is not stored on the field `delivery_type`. """ self.ensure_one() diff --git a/addons/delivery/models/product_category.py b/addons/delivery/models/product_category.py index af24b70978072..cff4c7ac3ff3f 100644 --- a/addons/delivery/models/product_category.py +++ b/addons/delivery/models/product_category.py @@ -11,4 +11,4 @@ class ProductCategory(models.Model): def _unlink_except_delivery_category(self): delivery_category = self.env.ref('delivery.product_category_deliveries', raise_if_not_found=False) if delivery_category and delivery_category in self: - raise UserError(_("You cannot delete the deliveries product category as it is used on the delivery carriers products.")) + raise UserError(_("You cannot delete this product category as it is used on the products linked to delivery methods.")) diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index e82a7bfb369c0..370b4b9b0e9e9 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -129,7 +129,7 @@ def action_open_delivery_wizard(self): if self.env.context.get('carrier_recompute'): name = _('Update shipping cost') else: - name = _('Add a shipping method') + name = _('Add a delivery method') return { 'name': name, 'type': 'ir.actions.act_window', diff --git a/addons/delivery/report/ir_actions_report_templates.xml b/addons/delivery/report/ir_actions_report_templates.xml index 9357b8dcffd12..1ac2e9b31b2f8 100644 --- a/addons/delivery/report/ir_actions_report_templates.xml +++ b/addons/delivery/report/ir_actions_report_templates.xml @@ -4,7 +4,7 @@