diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-26 16:39:30 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-26 16:39:30 +0100 |
| commit | ff9c9341d6ed4e9ab18a0aa02e79441999d658f1 (patch) | |
| tree | 47ffd01e071d3783cff6013cfc4b6c3d8e47a841 /addons | |
| parent | bc84fdf71b15ad419f8693861aea682ee751f85c (diff) | |
| parent | fcb972db9dc778eeaf7d6b6ba3638c78d93998ad (diff) | |
[MERGE] forward port branch saas-12.2 up to fcb972db9dc
Diffstat (limited to 'addons')
276 files changed, 6512 insertions, 3774 deletions
diff --git a/addons/account/i18n/ca.po b/addons/account/i18n/ca.po index 68da51122b5..71951921291 100644 --- a/addons/account/i18n/ca.po +++ b/addons/account/i18n/ca.po @@ -11,6 +11,7 @@ # Marc Tormo i Bochaca <mtbochaca@gmail.com>, 2019 # RGB Consulting <odoo@rgbconsulting.com>, 2019 # Quim - eccit <quim@eccit.com>, 2019 +# Sabina Batlle <sbb@odoo.com>, 2019 # msgid "" msgstr "" @@ -18,7 +19,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Quim - eccit <quim@eccit.com>, 2019\n" +"Last-Translator: Sabina Batlle <sbb@odoo.com>, 2019\n" "Language-Team: Catalan (https://www.transifex.com/odoo/teams/41243/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -216,7 +217,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_my_home_menu_invoice msgid "<em>Draft Invoice</em>" -msgstr "" +msgstr "Factura esborrany" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_move_line_view_kanban @@ -12071,13 +12072,6 @@ msgid "have been reconciled automatically." msgstr "" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "" diff --git a/addons/account/i18n/es.po b/addons/account/i18n/es.po index be0a06a0e79..91bec89e08e 100644 --- a/addons/account/i18n/es.po +++ b/addons/account/i18n/es.po @@ -70,9 +70,9 @@ # Miquel Torner <miq@odoo.com>, 2019 # Nicolás Broggi <rnbroggi@gmail.com>, 2019 # Luis González <lgonzalez@vauxoo.com>, 2019 -# Gabriel Umaña <gabriel.umana@delfixcr.com>, 2019 # Luis Marin <marin.guadarrama@gmail.com>, 2019 # Edilianny Sánchez <esanchez@vauxoo.com>, 2019 +# Gabriel Umana <gabriel.umana@delfixcr.com>, 2019 # msgid "" msgstr "" @@ -80,7 +80,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Edilianny Sánchez <esanchez@vauxoo.com>, 2019\n" +"Last-Translator: Gabriel Umana <gabriel.umana@delfixcr.com>, 2019\n" "Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5107,7 +5107,7 @@ msgstr "Agrupar líneas de factura" #. module: account #: model:ir.model.fields,field_description:account.field_account_register_payments__group_invoices msgid "Group Invoices" -msgstr "" +msgstr "Grupo de facturas" #. module: account #: selection:account.tax,amount_type:0 @@ -12359,13 +12359,6 @@ msgid "have been reconciled automatically." msgstr "" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "o_manual_statement" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "De este mes" diff --git a/addons/account/i18n/fr.po b/addons/account/i18n/fr.po index 1288fa09eb5..6ea1c304c29 100644 --- a/addons/account/i18n/fr.po +++ b/addons/account/i18n/fr.po @@ -41,7 +41,6 @@ # Fabri Yohann <psn@fabri.pw>, 2017 # FR R4 <e2ffr1111@hotmail.com>, 2017 # Matillon Samantha <smatillon@gmail.com>, 2017 -# Moka Tourisme <hello@mokatourisme.fr>, 2018 # Olivier Dony <odo@odoo.com>, 2018 # Hubert TETARD <htetard@apik-conseils.com>, 2018 # Nancy Bolognesi <nb@microcom.ca>, 2018 @@ -79,6 +78,8 @@ # Alain van de Werve <avw@openerp.com>, 2019 # Khalyomede Khalyomede <khalyomede@gmail.com>, 2019 # Fabien Bourgeois <fabien@yaltik.com>, 2019 +# Phil Anth <thep3r3x@gmail.com>, 2019 +# Moka Tourisme <hello@mokatourisme.fr>, 2019 # msgid "" msgstr "" @@ -86,7 +87,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Fabien Bourgeois <fabien@yaltik.com>, 2019\n" +"Last-Translator: Moka Tourisme <hello@mokatourisme.fr>, 2019\n" "Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -681,7 +682,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_invoice_page msgid "<strong class=\"text-muted\">Your Contact</strong>" -msgstr "" +msgstr "<strong class=\"text-muted\">Votre contact</strong>" #. module: account #: model_terms:ir.ui.view,arch_db:account.report_invoice_document_with_payments @@ -4909,6 +4910,8 @@ msgstr "Pour être en pourcentage, saisir une valeur entre 0 et 100." msgid "" "Forbidden unit price, account and quantity on non-accountable invoice line" msgstr "" +"Prix unitaire, compte et quantité interdits sur la ligne de facture non " +"comptable" #. module: account #: model:ir.model.fields,help:account.field_account_reconcile_model__force_second_tax_included @@ -12423,13 +12426,6 @@ msgid "have been reconciled automatically." msgstr "" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "o_manual_statement" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "du mois en cours" diff --git a/addons/account/i18n/hu.po b/addons/account/i18n/hu.po index f22a0acac83..71970867f2a 100644 --- a/addons/account/i18n/hu.po +++ b/addons/account/i18n/hu.po @@ -6,9 +6,9 @@ # picibucor <picibucor@gmail.com>, 2017 # Tibor Kőnig <konig.tibor@evitalit.hu>, 2019 # krnkris, 2019 -# gezza <geza.nagy@oregional.hu>, 2019 # Kovács Tibor <kovika@gmail.com>, 2019 # Martin Trigaux, 2019 +# gezza <geza.nagy@oregional.hu>, 2019 # Ákos Nagy <akos.nagy@oregional.hu>, 2019 # msgid "" @@ -4044,7 +4044,7 @@ msgstr "" #. module: account #: model:ir.model,name:account.model_digest_digest msgid "Digest" -msgstr "" +msgstr "Kivonat" #. module: account #: model:ir.model.fields,help:account.field_res_config_settings__group_products_in_bills @@ -4152,7 +4152,7 @@ msgstr "Feltüntetve a számlákon" #: code:addons/account/models/digest.py:16 #, python-format msgid "Do not have access, skip this data for user's digest email" -msgstr "" +msgstr "Nincs hozzáférés, adat kihagyása a felhasználó kivonat e-mailjéből" #. module: account #: model:ir.model.fields,help:account.field_account_invoice_send__auto_delete_message @@ -6332,7 +6332,7 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:account.view_account_invoice_filter #: model_terms:ir.ui.view,arch_db:account.view_account_payment_search msgid "Late Activities" -msgstr "" +msgstr "Késő tevékenységek" #. module: account #: model:ir.model.fields,field_description:account.field_account_account__last_time_entries_checked @@ -6344,7 +6344,7 @@ msgstr "Legutóbbi Számlák & Fizetések könyvelői párosításának dátuma" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__layout msgid "Layout" -msgstr "" +msgstr "Elrendezés" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_position_form @@ -9344,6 +9344,10 @@ msgid "" "Today: Activity date is today\n" "Planned: Future activities." msgstr "" +"Tevékenységeken alapuló állapot\n" +"Lejárt: Az esedékesség dátuma már elmúlt\n" +"Ma: A tevékenység dátuma ma van\n" +"Tervezett: Jövőbeli tevékenységek." #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_bank_account_step diff --git a/addons/account/i18n/mn.po b/addons/account/i18n/mn.po index b6ced1cabae..617e63fbdca 100644 --- a/addons/account/i18n/mn.po +++ b/addons/account/i18n/mn.po @@ -12,12 +12,12 @@ # Batzul B <batzul.active@gmail.com>, 2018 # nasaaskii <nasaaskii@gmail.com>, 2019 # Chinzorita <chinzorig.o@asterisk-tech.mn>, 2019 -# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019 # Otgonbayar.A <gobi.mn@gmail.com>, 2019 # Ganbaatar Buriad <Ganbaatar@asterisk-tech.mn>, 2019 # Onii Onii <onii0223@yahoo.com>, 2019 # Martin Trigaux, 2019 # Khishigbat Ganbold <khishigbat@asterisk-tech.mn>, 2019 +# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019 # msgid "" msgstr "" @@ -25,7 +25,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Khishigbat Ganbold <khishigbat@asterisk-tech.mn>, 2019\n" +"Last-Translator: Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019\n" "Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1116,7 +1116,7 @@ msgstr "Санхүү" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form msgid "Accounting App Options" -msgstr "" +msgstr "Санхүү бүртгэлийн сонголтууд" #. module: account #: model:ir.model.fields,field_description:account.field_account_bank_statement__accounting_date @@ -1139,7 +1139,7 @@ msgstr "Санхүүгийн бичилт" #. module: account #: model:ir.actions.act_window,name:account.open_account_journal_dashboard_kanban msgid "Accounting Overview" -msgstr "" +msgstr "Санхүүгийн тойм" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_partner_property_form @@ -2318,7 +2318,7 @@ msgstr "Төлөх тооцоо" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view msgid "Bills to Validate" -msgstr "" +msgstr "Батлах нэхэмжлэлүүд" #. module: account #: code:addons/account/models/account_journal_dashboard.py:178 @@ -2344,7 +2344,7 @@ msgstr "Байгаа улсуудыг харах" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_budget msgid "Budget Management" -msgstr "" +msgstr "Төсвийн удирдлага" #. module: account #: model:ir.model.fields,field_description:account.field_account_reconcile_model_template__name @@ -2359,7 +2359,7 @@ msgstr "Улсаар" #. module: account #: model:ir.filters,name:account.filter_invoice_refund msgid "By Credit Note" -msgstr "" +msgstr "Буцаалтын нэхэмжлэлээр" #. module: account #: model:ir.filters,name:account.filter_invoice_product @@ -2900,6 +2900,8 @@ msgstr "Мөнгө/Тооцооны Утга" #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Collect customer payments in one-click using Euro SEPA Service" msgstr "" +"Euro SEPA үйлчилгээ ашиглан ганцхан товч даралтаар харилцагчдын төлбөр " +"төлөлтүүдийг цуглуулна" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -3466,7 +3468,7 @@ msgstr "Зээлийн карт" #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__credit_cash_basis msgid "Credit Cash Basis" -msgstr "" +msgstr "Мөнгөн суурьт кредит" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_refund__filter_refund @@ -3476,7 +3478,7 @@ msgstr "" #. module: account #: model:ir.model.fields,field_description:account.field_account_partial_reconcile__credit_move_id msgid "Credit Move" -msgstr "" +msgstr "Кредит гүйлгээ" #. module: account #: code:addons/account/models/account_invoice.py:498 @@ -3494,22 +3496,22 @@ msgstr "Буцаалт" #: code:addons/account/models/account_invoice.py:499 #, python-format msgid "Credit Note - %s" -msgstr "" +msgstr "Буцаалт - %s" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view msgid "Credit Note Bill" -msgstr "" +msgstr "Өглөгийн буцаалт" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_refund__date_invoice msgid "Credit Note Date" -msgstr "" +msgstr "Буцаалтын огноо" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__refund_sequence_id msgid "Credit Note Entry Sequence" -msgstr "" +msgstr "Буцаалтын гүйлгээний дугаарлалт" #. module: account #: model:ir.actions.act_window,name:account.action_invoice_out_refund @@ -3517,12 +3519,12 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view #: model_terms:ir.ui.view,arch_db:account.view_account_invoice_report_search msgid "Credit Notes" -msgstr "" +msgstr "Буцаалтууд" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__refund_sequence_number_next msgid "Credit Notes: Next Number" -msgstr "" +msgstr "Буцаалт: Дараагийн дугаар" #. module: account #: model:ir.model.fields,field_description:account.field_tax_adjustments_wizard__credit_account_id @@ -3645,7 +3647,7 @@ msgstr "Захиалагчийн Төлбөрийн нөхцлүүд" #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form #: model_terms:ir.ui.view,arch_db:account.view_account_payment_search msgid "Customer Payments" -msgstr "" +msgstr "Харилцагчийн төлөлт" #. module: account #: model:ir.model.fields,help:account.field_account_invoice__access_url @@ -4460,7 +4462,7 @@ msgstr "Гадаад Код" #: code:addons/account/static/src/xml/account_reconciliation.xml:335 #, python-format msgid "External link" -msgstr "" +msgstr "Гадаад холбоос" #. module: account #: model:account.incoterms,name:account.incoterm_FAS @@ -4501,7 +4503,7 @@ msgstr "Холбооны Мужууд" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_import_wizard__attachment_ids msgid "Files" -msgstr "" +msgstr "Файл" #. module: account #: model_terms:ir.ui.view,arch_db:account.cash_box_in_form @@ -4529,7 +4531,7 @@ msgstr "Санхүүгийн жилийн мэдээлэл" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Fiscal Localization" -msgstr "" +msgstr "Байршилаас хамаарах санхүү бүртгэл " #. module: account #: model:ir.model.fields,field_description:account.field_account_fiscal_position_account_template__position_id @@ -4539,7 +4541,7 @@ msgstr "Санхүүгийн зурагжуулалт" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Fiscal Periods" -msgstr "" +msgstr "Тайлант мөчлөг" #. module: account #: model:ir.actions.act_window,name:account.action_account_fiscal_position_template_form @@ -4583,35 +4585,35 @@ msgstr "Санхүүгийн жил" #. module: account #: model_terms:ir.ui.view,arch_db:account.action_account_fiscal_year_form msgid "Fiscal Year 2018" -msgstr "" +msgstr "Санхүүгийн жил 2018" #. module: account #: model:ir.actions.act_window,name:account.actions_account_fiscal_year #: model:ir.model.fields,field_description:account.field_res_config_settings__group_fiscal_year msgid "Fiscal Years" -msgstr "" +msgstr "Санхүүгийн жил" #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_day msgid "Fiscal year last day." -msgstr "" +msgstr "Санхүүгийн жилийн сүүлийн өдөр." #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_month msgid "Fiscal year last month." -msgstr "" +msgstr "Санхүүгийн жилийн сүүлийн сар." #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__fiscalyear_last_day #: model:ir.model.fields,field_description:account.field_res_company__fiscalyear_last_day msgid "Fiscalyear Last Day" -msgstr "" +msgstr "Санхүүгийн жилийн сүүлийн өдөр" #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__fiscalyear_last_month #: model:ir.model.fields,field_description:account.field_res_company__fiscalyear_last_month msgid "Fiscalyear Last Month" -msgstr "" +msgstr "Санхүүгийн жилийн сүүлийн сар" #. module: account #: selection:account.reconcile.model,amount_type:0 @@ -4672,12 +4674,12 @@ msgstr "Дагагчид (Харилцагчид)" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__inbound_payment_method_ids msgid "For Incoming Payments" -msgstr "" +msgstr "Ирж буй төлбөрийн хувьд" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__outbound_payment_method_ids msgid "For Outgoing Payments" -msgstr "" +msgstr "Гарч буй төлбөрийн хувьд" #. module: account #: model:ir.model.fields,help:account.field_account_payment_term_line__value_amount @@ -4689,18 +4691,20 @@ msgstr "Хувь оруулахдаа 0-100 хооронд харьцааг ор msgid "" "Forbidden unit price, account and quantity on non-accountable invoice line" msgstr "" +"Санхүүгийн гүйлгээнд хамаарахгүй мөр дээр ямар нэг нэгж үнэ, данс, тоо " +"хэмжээний утга оруулахыг хориглоно" #. module: account #: model:ir.model.fields,help:account.field_account_reconcile_model__force_second_tax_included #: model:ir.model.fields,help:account.field_account_reconcile_model_template__force_second_tax_included msgid "Force the second tax to be managed as a price included tax." -msgstr "" +msgstr "Хоёрдогч нэмэлт татвар нь үнийн дүнд шингэсэн байдлаар хэрэгжинэ." #. module: account #: model:ir.model.fields,help:account.field_account_reconcile_model__force_tax_included #: model:ir.model.fields,help:account.field_account_reconcile_model_template__force_tax_included msgid "Force the tax to be managed as a price included tax." -msgstr "" +msgstr "Хоёрдогч нэмэлт татвар нь үнийн дүнд шингэсэн байдлаар хэрэгжинэ." #. module: account #: model:ir.model.fields,help:account.field_account_account__currency_id @@ -4760,7 +4764,7 @@ msgstr "" #: code:addons/account/models/account_invoice.py:387 #, python-format msgid "From: " -msgstr "" +msgstr "Хаанаас:" #. module: account #: model:ir.model,name:account.model_account_full_reconcile @@ -4794,12 +4798,12 @@ msgstr "Бичилтүүдийг үүсгэх" #. module: account #: model:ir.ui.menu,name:account.account_reports_legal_statements_menu msgid "Generic Statements" -msgstr "" +msgstr "Нийтлэг тайлангууд" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Get warnings when invoicing specific customers" -msgstr "" +msgstr "Зарим онцгой харилцагчид нэхэмжлэл бүртгэх үед анхааруулга өгөх" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -4809,6 +4813,11 @@ msgid "" "“Bank Synchronization” in bank account settings. Then, click “Configure” on " "the online account to enter your bank credentials." msgstr "" +"Yodlee ба Plaid үйлчилгээг ашигласнаар Таны банкны гүйлгээ 4 цаг тутамд " +"автоматаар бүртгэгдэх эсвэл товч дарж тэр даруй бүртгэгдэх боломжтой. Энэ " +"нэмэлтийг суулгасны дараа банкны дансны тохиргооны хэсэг дэх \"Банк " +"ижилсүүлэлт\" талбарт \"Банкнаас татах\" утгыг сонгоно. Дараа нь " +"\"Тохируулах\" товч дээр дарж онлайн дансны бүртгэлээ оруулна. " #. module: account #: model:ir.model.fields,help:account.field_account_invoice_line__sequence @@ -4830,14 +4839,14 @@ msgstr "Нэхэмжлэлийн татварын харагдах эрэмбэ #: model:ir.model.fields,help:account.field_account_payment_term_line__sequence msgid "" "Gives the sequence order when displaying a list of payment terms lines." -msgstr "" +msgstr "Төлбөрийн нөхцөлийн мөрүүдийн хэрэгжих эрэмбэ, дарааллыг тодорхойлно." #. module: account #. openerp-web #: code:addons/account/static/src/xml/account_reconciliation.xml:63 #, python-format msgid "Go to bank statement(s)" -msgstr "" +msgstr "Банкны бүртгэл рүү очно уу" #. module: account #: code:addons/account/models/account_invoice.py:773 @@ -4850,7 +4859,7 @@ msgstr "Тохиргооны самбар руу очно уу" #: code:addons/account/models/company.py:482 #, python-format msgid "Go to the journal configuration" -msgstr "" +msgstr "Журнал тохируулга руу очих" #. module: account #: selection:res.partner,trust:0 @@ -4891,7 +4900,7 @@ msgstr "Нэхэмжлэлийн мөрүүдийг бүлэглэх" #. module: account #: model:ir.model.fields,field_description:account.field_account_register_payments__group_invoices msgid "Group Invoices" -msgstr "" +msgstr "Нэхэмжлэл бүлэглэлт" #. module: account #: selection:account.tax,amount_type:0 @@ -4902,22 +4911,22 @@ msgstr "Татварын бүлэг" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Group payments into a single batch to ease the reconciliation process" -msgstr "" +msgstr "Хялбар тулгалт хийх зорилгоор төлөлтүүдийг бүлэглэж багц болгох" #. module: account #: selection:account.cash.rounding,rounding_method:0 msgid "HALF-UP" -msgstr "" +msgstr "HALF-UP" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__has_accounting_entries msgid "Has Accounting Entries" -msgstr "" +msgstr "Ажил гүйлгээтэй" #. module: account #: model:ir.model.fields,field_description:account.field_account_payment__has_invoices msgid "Has Invoices" -msgstr "" +msgstr "Нэхэмжлэлтэй" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__has_outstanding @@ -4928,30 +4937,30 @@ msgstr "" #: model:ir.model.fields,field_description:account.field_res_partner__has_unreconciled_entries #: model:ir.model.fields,field_description:account.field_res_users__has_unreconciled_entries msgid "Has Unreconciled Entries" -msgstr "" +msgstr "Тулгагдаагүй гүйлгээтэй" #. module: account #: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__qr_code_valid msgid "Has all required arguments" -msgstr "" +msgstr "QR код шаардлагатай" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__has_error #: model:ir.model.fields,help:account.field_account_invoice_send__has_error msgid "Has error" -msgstr "" +msgstr "Алдаатай" #. module: account #: model:ir.model.fields,field_description:account.field_account_abstract_payment__hide_payment_method #: model:ir.model.fields,field_description:account.field_account_payment__hide_payment_method #: model:ir.model.fields,field_description:account.field_account_register_payments__hide_payment_method msgid "Hide Payment Method" -msgstr "" +msgstr "Төлбөрийн аргыг нуух" #. module: account #: model:ir.model.fields,field_description:account.field_account_tax__hide_tax_exigibility msgid "Hide Use Cash Basis Option" -msgstr "" +msgstr "Мөнгөн суурьт сонголтын нуух" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_invoice_chatter @@ -4962,6 +4971,7 @@ msgstr "Түүх" #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "How total tax amount is computed in orders and invoices" msgstr "" +"Захиалга болон нэхэмжлэл дээрх татварын нийлбэр дүн хэрхэн тооцоглогдох" #. module: account #: model:ir.model.fields,field_description:account.field_account_abstract_payment__id @@ -5059,7 +5069,7 @@ msgstr "Хэрэв сонгосон бол, шинэ зурвасуудад ан #: model:ir.model.fields,help:account.field_account_invoice__message_has_error #: model:ir.model.fields,help:account.field_account_payment__message_has_error msgid "If checked, some messages have a delivery error." -msgstr "" +msgstr "Үүнийг сонговол алдаа үүсэх үед зурвасууд ирнэ." #. module: account #: model:ir.model.fields,help:account.field_account_account_template__nocreate @@ -5081,6 +5091,9 @@ msgid "" " type and recipient bank account in the generated payments. If disabled,\n" " a distinct payment will be generated for each invoice." msgstr "" +"Үүнийг сонговол, нэхэмжлэлүүдээс үүсэх төлбөрийн бүртгэлүүд харилцагч, нэхэмжлэлийн данс,\n" +" нэхэмжлэлийн төрөл болон хүлээн авагчийн банкны данс зэрэг мэдээллээр ижилсэж бүлэглэгдэнэ.\n" +" Хэрэв сонгохгүй орхивол, нэхэмжлэл бүрээс харгалзан төлбөрийн бүртгэл үүснэ." #. module: account #: model:ir.model.fields,help:account.field_account_tax__include_base_amount @@ -5098,6 +5111,8 @@ msgid "" "If set, the accounting entries created during the bank statement reconciliation process will be created at this date.\n" "This is useful if the accounting period in which the entries should normally be booked is already closed." msgstr "" +"Үүнийг сонговол, банкны хуулга тулгалтаас үүсч буй ажил гүйлгээнүүд энэ огноогоор бүртгэгдэх болно.\n" +" Хэрэв хаагдсан тайлант хугацааны гүйлгээнд тулгалт хийж байгаа бол энэ огноог зөв сонгох хэрэгтэй." #. module: account #: model:ir.model.fields,help:account.field_account_tax__analytic @@ -5115,6 +5130,8 @@ msgid "" "If the active field is set to False, it will allow you to hide the payment " "terms without removing it." msgstr "" +"Хэрэв Идэвхитэй талбарыг Үгүй утгатай болговол энэ нь танд төлбөрийн " +"нөхцөлийг устгалгүйгээр нуух боломжыг олгоно." #. module: account #: model:ir.model.fields,help:account.field_account_journal__group_invoice_lines @@ -5131,6 +5148,8 @@ msgid "" "If this checkbox is ticked, this entry will be automatically reversed at the" " reversal date you defined." msgstr "" +"Үүнийг сонгосон бол, энэ ажил гүйлгээнээс таны тохируулсан өдөр автоматаар " +"эсрэг бичилт үүсэх болно." #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -5421,7 +5440,7 @@ msgstr "Нөөцийн үнэлгээний оролтын данс" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Install More Packages" -msgstr "" +msgstr "Нэмэлт сангууд суулгах" #. module: account #: model:ir.model.fields,field_description:account.field_res_company__transfer_account_id @@ -5440,7 +5459,7 @@ msgstr "" #: model:ir.model.fields,field_description:account.field_account_account__internal_group #: model:ir.model.fields,field_description:account.field_account_account_type__internal_group msgid "Internal Group" -msgstr "" +msgstr "Дотоод бүлэг" #. module: account #: model:ir.model.fields,field_description:account.field_account_move__narration @@ -5529,7 +5548,7 @@ msgstr "Нэхэмжлэл #" #: code:addons/account/models/account_invoice.py:497 #, python-format msgid "Invoice - %s" -msgstr "" +msgstr "Нэхэмжлэл - %s" #. module: account #: model:mail.message.subtype,description:account.mt_invoice_created @@ -5550,12 +5569,12 @@ msgstr "Нэхэмжлэх огноо" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__invoice_icon msgid "Invoice Icon" -msgstr "" +msgstr "Нэхэмжлэлийн дүрс" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_invoice_layout_step msgid "Invoice Layout" -msgstr "" +msgstr "Нэхэмжлэлийн загвар" #. module: account #: model:ir.model,name:account.model_account_invoice_line @@ -5577,12 +5596,12 @@ msgstr "Компанийн хэмжээнд нэхэмжлэлийн дугааÑ #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_form msgid "Invoice Number:" -msgstr "" +msgstr "Нэхэмжлэлийн дугаар:" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_payment msgid "Invoice Online Payment" -msgstr "" +msgstr "Нэхэмжлэл онлайнаар төлөх" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_line__invoice_id @@ -5636,7 +5655,7 @@ msgstr "Нэхэмжлэл төлөгдсөн" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_invoice_send_wizard_form msgid "Invoice send & Print" -msgstr "" +msgstr "Нэхэмжлэлийг илгээсэн & Хэвлэсэн" #. module: account #: model:mail.message.subtype,description:account.mt_invoice_validated @@ -5708,17 +5727,17 @@ msgstr "Танд өртэй нэхэмжлэлүүд" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view msgid "Invoices to Validate" -msgstr "" +msgstr "Батлах нэхэмжлэлүүд" #. module: account #: model:ir.model.fields,help:account.field_account_payment__reconciled_invoice_ids msgid "Invoices whose journal items have been reconciled with this payment's." -msgstr "" +msgstr "Энэ төлбөрөөр тулгагдсан нэхэмжлэлийн журналын бичилтүүд." #. module: account #: model:ir.actions.report,name:account.account_invoices_without_payment msgid "Invoices without Payment" -msgstr "" +msgstr "Төлөлтгүй нэхэмжлэл" #. module: account #: model:ir.ui.menu,name:account.menu_finance @@ -5732,7 +5751,7 @@ msgstr "Нэхэмжлэх" #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Between" -msgstr "" +msgstr "Хооронд" #. module: account #: model:ir.model.fields,field_description:account.field_account_bank_statement__message_is_follower @@ -5745,28 +5764,28 @@ msgstr "Дагагч эсэх" #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Greater Than" -msgstr "" +msgstr "Их эсэх" #. module: account #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Lower Than" -msgstr "" +msgstr "Бага эсэх" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__printed msgid "Is Printed" -msgstr "" +msgstr "Хэвлэгдсэн эсэх" #. module: account #: model:ir.model.fields,field_description:account.field_account_reconcile_model__is_second_tax_price_included msgid "Is Second Tax Included in Price" -msgstr "" +msgstr "Үнийн дүнд хоёрдогч татвар шингэсэн эсэх" #. module: account #: model:ir.model.fields,field_description:account.field_account_reconcile_model__is_tax_price_included msgid "Is Tax Included in Price" -msgstr "" +msgstr "Татвар үнийн дүнд шингэсэн эсэх" #. module: account #: model:ir.model.fields,help:account.field_account_invoice_line__is_rounding_line @@ -5883,7 +5902,7 @@ msgstr "Журналийн бичилт" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_move_filter msgid "Journal Entries by Date" -msgstr "" +msgstr "Ажил гүйлгээ огноогоор" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__move_id @@ -5953,7 +5972,7 @@ msgstr "Журналын мөрүүд" #. module: account #: model:ir.actions.act_window,name:account.action_move_line_select_tax_audit msgid "Journal Items for Tax Audit" -msgstr "" +msgstr "Ажил гүйлгээ татвар аудитаар" #. module: account #. openerp-web @@ -5988,7 +6007,7 @@ msgstr "" #. module: account #: model:ir.model.fields,field_description:account.field_account_bank_statement_line__journal_currency_id msgid "Journal's Currency" -msgstr "" +msgstr "Журналын валют" #. module: account #: model:ir.actions.act_window,name:account.action_account_journal_form @@ -6026,7 +6045,7 @@ msgstr "6-р сар" #: selection:res.company,account_setup_coa_state:0 #: selection:res.company,account_setup_fy_data_state:0 msgid "Just done" -msgstr "" +msgstr "Дөнгөж дууссан" #. module: account #: model:ir.model.fields,field_description:account.field_tax_adjustments_wizard__reason @@ -6036,12 +6055,12 @@ msgstr "Баталгаажуулалт" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__kanban_dashboard msgid "Kanban Dashboard" -msgstr "" +msgstr "Канбан самбар" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__kanban_dashboard_graph msgid "Kanban Dashboard Graph" -msgstr "" +msgstr "Канбан самбар граффик" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form @@ -6336,7 +6355,7 @@ msgstr "Хамгийн сүүлийн Нэхэмжлэл, Төлбөрийг тÑ #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__layout msgid "Layout" -msgstr "" +msgstr "Хэв загвар" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_position_form @@ -6400,7 +6419,7 @@ msgstr "Хөрвөх чадвар" #: code:addons/account/models/chart_template.py:154 #, python-format msgid "Liquidity Transfer" -msgstr "" +msgstr "Мөнгөн хөрөнгө шилжүүлгэ" #. module: account #: model:ir.model.fields,help:account.field_account_chart_template__tax_template_ids @@ -6412,7 +6431,7 @@ msgstr "Шинээр үүсгэх татваруудын жагсаалт" #: code:addons/account/static/src/xml/account_reconciliation.xml:33 #, python-format msgid "Load more" -msgstr "" +msgstr "Илүү унших" #. module: account #. openerp-web @@ -6439,7 +6458,7 @@ msgstr "Дотоод тэмдэглэл хөтлөх" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_invoice_layout_step msgid "Looks great!" -msgstr "" +msgstr "Дажгүй харагдаж байна!" #. module: account #: code:addons/account/models/account_bank_statement.py:175 @@ -6479,7 +6498,7 @@ msgstr "Мэйлийн жагсаалт" #: model:ir.model.fields,field_description:account.field_account_invoice__message_main_attachment_id #: model:ir.model.fields,field_description:account.field_account_payment__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Үндсэн хавсралт" #. module: account #: model:ir.model.fields,help:account.field_res_config_settings__currency_id @@ -6489,7 +6508,7 @@ msgstr "Компанийн үндсэн валют." #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Main currency of your company" -msgstr "" +msgstr "Компаны үндсэн валют" #. module: account #: model:ir.ui.menu,name:account.account_management_menu @@ -6556,7 +6575,7 @@ msgstr "3-р сар" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Margin Analysis" -msgstr "" +msgstr "Ашгийн шинжилгээ" #. module: account #: selection:account.abstract.payment,payment_difference_handling:0 @@ -6578,7 +6597,7 @@ msgstr "Масс мэйлийн шинжилгээ" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__mass_mailing_name msgid "Mass Mailing Name" -msgstr "" +msgstr "Масс мэйлийн нэр" #. module: account #: selection:account.reconcile.model,match_label:0 @@ -6593,17 +6612,17 @@ msgstr "" #: code:addons/account/models/chart_template.py:983 #, python-format msgid "Match existing invoices/bills." -msgstr "" +msgstr "Байгаа нэхэмжлэлтэй харгалзуулах" #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__matched_credit_ids msgid "Matched Credit" -msgstr "" +msgstr "Харгалзсан кредит" #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__matched_debit_ids msgid "Matched Debit" -msgstr "" +msgstr "Харгалзсан дебет" #. module: account #: model:ir.model.fields,field_description:account.field_account_full_reconcile__reconciled_line_ids @@ -6625,7 +6644,7 @@ msgstr "Тохирох дугаар" #. module: account #: model:ir.model.fields,field_description:account.field_account_partial_reconcile__max_date msgid "Max Date of Matched Lines" -msgstr "" +msgstr "Харгалзсан мөрүүдийн хамгийн өндөр огноо" #. module: account #: selection:res.company,fiscalyear_last_month:0 @@ -6642,7 +6661,7 @@ msgstr "Санамж" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_from_invoices msgid "Memo will be computed from invoices" -msgstr "" +msgstr "Санамж нэхэмжлэлээс тооцоологдоно" #. module: account #. openerp-web @@ -6858,7 +6877,7 @@ msgstr "Шинэ" #. module: account #: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__new_journal_name msgid "New Journal Name" -msgstr "" +msgstr "Шинэ журналын нэр" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view @@ -6875,7 +6894,7 @@ msgstr "Төлбөрийн шинэ таамагласан огноо: " #: model:ir.model.fields,field_description:account.field_account_invoice__activity_date_deadline #: model:ir.model.fields,field_description:account.field_account_payment__activity_date_deadline msgid "Next Activity Deadline" -msgstr "" +msgstr "Дараагийн ажилбарын эцсийн огноо" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__activity_summary @@ -6887,7 +6906,7 @@ msgstr "Дараагийн Ажилбарын Хураангуй" #: model:ir.model.fields,field_description:account.field_account_invoice__activity_type_id #: model:ir.model.fields,field_description:account.field_account_payment__activity_type_id msgid "Next Activity Type" -msgstr "" +msgstr "Дараагийн ажилбарын төрөл" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__sequence_number_next @@ -6898,7 +6917,7 @@ msgstr "Дараагийн дугаар" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__sequence_number_next_prefix msgid "Next Number Prefix" -msgstr "" +msgstr "Дараагийн дугаарын Угтвар" #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__blocked @@ -6916,7 +6935,7 @@ msgstr "Мессеж байхгүй" #: code:addons/account/static/src/js/reconciliation/reconciliation_renderer.js:143 #, python-format msgid "No Title" -msgstr "" +msgstr "Гарчиг байхгүй" #. module: account #: code:addons/account/models/account_invoice.py:912 @@ -6970,7 +6989,7 @@ msgstr "Энгийн Зээлдэгч" #: selection:account.reconcile.model,match_label:0 #: selection:account.reconcile.model.template,match_label:0 msgid "Not Contains" -msgstr "" +msgstr "Агуулаагүй" #. module: account #: selection:res.company,account_dashboard_onboarding_state:0 @@ -6982,7 +7001,7 @@ msgstr "" #: selection:res.company,account_setup_coa_state:0 #: selection:res.company,account_setup_fy_data_state:0 msgid "Not done" -msgstr "" +msgstr "Дуусаагүй" #. module: account #. openerp-web @@ -7021,7 +7040,7 @@ msgstr "Тэмдэглэл" #: code:addons/account/static/src/xml/account_reconciliation.xml:37 #, python-format msgid "Nothing to do!" -msgstr "" +msgstr "Хийх зүйл алга!" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__notification_ids @@ -7077,7 +7096,7 @@ msgstr "Хоногийн Тоо" #: model:ir.model.fields,field_description:account.field_account_invoice__message_has_error_counter #: model:ir.model.fields,field_description:account.field_account_payment__message_has_error_counter msgid "Number of error" -msgstr "" +msgstr "Алдааны тоо" #. module: account #: model:ir.model.fields,help:account.field_account_bank_statement__message_needaction_counter @@ -7091,7 +7110,7 @@ msgstr "Үйлдэл шаардлагатай зурвасын тоо" #: model:ir.model.fields,help:account.field_account_invoice__message_has_error_counter #: model:ir.model.fields,help:account.field_account_payment__message_has_error_counter msgid "Number of messages with delivery error" -msgstr "" +msgstr "Алдааны мэдэгдэл бүхий зурвасын тоо" #. module: account #: model:ir.model.fields,help:account.field_account_bank_statement__message_unread_counter @@ -7131,7 +7150,7 @@ msgstr "" #. module: account #: model:ir.model.fields,field_description:account.field_account_move_line__tax_line_grouping_key msgid "Old Taxes" -msgstr "" +msgstr "Хуучин татвар" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_payment_term_line_form @@ -7191,7 +7210,7 @@ msgstr "Нээх" #: code:addons/account/models/account_reconcile_model.py:276 #, python-format msgid "Open Balance" -msgstr "" +msgstr "Нээлтийн баланс" #. module: account #. openerp-web @@ -7204,7 +7223,7 @@ msgstr "Нээлттэй баланс" #. module: account #: model:ir.model,name:account.model_account_financial_year_op msgid "Opening Balance of Financial Year" -msgstr "" +msgstr "Санхүүгийн жилийн нээлтийн баланс" #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__opening_date @@ -7215,19 +7234,19 @@ msgstr "Нээх өдөр" #. module: account #: model:ir.model.fields,field_description:account.field_res_company__account_opening_journal_id msgid "Opening Journal" -msgstr "" +msgstr "Нээлтийн журналт" #. module: account #: code:addons/account/models/company.py:365 #: model:ir.model.fields,field_description:account.field_res_company__account_opening_move_id #, python-format msgid "Opening Journal Entry" -msgstr "" +msgstr "Нээлтийн журналын бичилт" #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__opening_move_posted msgid "Opening Move Posted" -msgstr "" +msgstr "Нээлтийн батлагдсан гүйлгээ" #. module: account #: model:ir.model.fields,help:account.field_account_cashbox_line__number @@ -7238,27 +7257,27 @@ msgstr "Нээлтийн нэгжийн тоонууд" #: code:addons/account/models/account.py:169 #, python-format msgid "Opening balance" -msgstr "" +msgstr "Нээлтийн баланс" #. module: account #: model:ir.model.fields,field_description:account.field_account_account__opening_credit msgid "Opening credit" -msgstr "" +msgstr "Нээлтийн кредит" #. module: account #: model:ir.model.fields,help:account.field_account_account__opening_credit msgid "Opening credit value for this account." -msgstr "" +msgstr "Энэ дансны нээлтийн кредит утга." #. module: account #: model:ir.model.fields,field_description:account.field_account_account__opening_debit msgid "Opening debit" -msgstr "" +msgstr "Нээлтийн дебет" #. module: account #: model:ir.model.fields,help:account.field_account_account__opening_debit msgid "Opening debit value for this account." -msgstr "" +msgstr "Энэ дансны нээлтийн дебет утга." #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_reconcile_model_form @@ -7399,7 +7418,7 @@ msgstr "Хугацаа хэтэрсэн нэхэмжлэлүүд, төлөх оÐ #. module: account #: model:ir.ui.menu,name:account.menu_board_journal_1 msgid "Overview" -msgstr "" +msgstr "Тойм" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -11143,7 +11162,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Use follow-up levels and schedule actions" -msgstr "" +msgstr "Мөшгөлтийн төвшин болон төлөвлөгөөт арга хэмжээ ашиглах" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__group_products_in_bills @@ -11302,7 +11321,7 @@ msgstr "Нийлүүлэгчийн тооцоо" #: code:addons/account/models/account_invoice.py:501 #, python-format msgid "Vendor Bill - %s" -msgstr "" +msgstr "Нийлүүлэгчийн нэхэмжлэл - %s" #. module: account #: code:addons/account/models/chart_template.py:419 @@ -11318,24 +11337,24 @@ msgstr "Нийлүүлэгчийн Нэхэмжлэлүүд" #: model_terms:ir.ui.view,arch_db:account.report_invoice_document #, python-format msgid "Vendor Credit Note" -msgstr "" +msgstr "Нийлүүлэгчийн буцаалт" #. module: account #: code:addons/account/models/account_invoice.py:503 #, python-format msgid "Vendor Credit Note - %s" -msgstr "" +msgstr "Нийлүүлэгчийн буцаалт - %s" #. module: account #: code:addons/account/models/account_invoice.py:1359 #, python-format msgid "Vendor Credit note" -msgstr "" +msgstr "Нийлүүлэгчийн буцаалт" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__vendor_display_name msgid "Vendor Display Name" -msgstr "" +msgstr "Нийлүүлэгчийн дэлгэцийн нэр" #. module: account #: code:addons/account/models/account_payment.py:774 @@ -11354,7 +11373,7 @@ msgstr "Нийлүүлэгчийн Төлбөрийн Нөхцлүүд" #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view #: model_terms:ir.ui.view,arch_db:account.view_account_payment_search msgid "Vendor Payments" -msgstr "" +msgstr "Нийлүүлэгчийн төлбөр" #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_supplier_form @@ -11412,7 +11431,7 @@ msgstr "Анхааруулга" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__group_warning_account msgid "Warnings in Invoices" -msgstr "" +msgstr "Нэхэмжлэлийн анхааруулга" #. module: account #: code:addons/account/models/company.py:541 @@ -11496,7 +11515,7 @@ msgstr "Хасалт" #: model:ir.model.fields,field_description:account.field_account_reconcile_model__amount #: model:ir.model.fields,field_description:account.field_account_reconcile_model_template__amount msgid "Write-off Amount" -msgstr "" +msgstr "Зөрүү дүн" #. module: account #: sql_constraint:account.move.line:0 @@ -11830,7 +11849,7 @@ msgstr "" #: code:addons/account/static/src/xml/account_reconciliation.xml:55 #, python-format msgid "You reconciled" -msgstr "" +msgstr "Та тулгалт хийсэн" #. module: account #: code:addons/account/models/account_move.py:1617 @@ -11963,13 +11982,6 @@ msgid "have been reconciled automatically." msgstr "" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "" diff --git a/addons/account/i18n/nl.po b/addons/account/i18n/nl.po index caf7adf1672..617fc9372fb 100644 --- a/addons/account/i18n/nl.po +++ b/addons/account/i18n/nl.po @@ -10005,8 +10005,8 @@ msgid "" " as it can have been generated first, and reconciled later" msgstr "" "Technisch veld bevat de facturen voor welke reeds betalingen werden gegenereerd.\n" -"Dit komt niet noodzakelijk overeen met de facturen reconsilieerd met de betalingen,\n" -"als deze eerst werden gegenereerd en pas later gereconsilieerd." +"Dit komt niet noodzakelijk overeen met de facturen afgeletterd met de betalingen,\n" +"als deze eerst werden gegenereerd en pas later afgeletterd." #. module: account #: model:ir.model.fields,help:account.field_account_invoice_line__display_type @@ -10222,9 +10222,9 @@ msgid "" " the moment the ORM reflects that change in its one2many" msgstr "" "Technische veld gebruikt om de oude veld waarde te bewaren is gebruikt om de" -" belasting regels te berekenen (in account.move from view) op het ogenblik " -"dat de gebruiker het wijzigde en het ogenblik dat de ORM deze wijziging " -"aangeeft in zijn one2many " +" belasting regels te berekenen (in account.move formulier weergave) op het " +"ogenblik dat de gebruiker het wijzigde en het ogenblik dat de ORM deze " +"wijziging aangeeft in zijn one2many " #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__chart_template_id @@ -12027,8 +12027,8 @@ msgid "" "You can not have an overlap between two fiscal years, please correct the " "start and/or end dates of your fiscal years." msgstr "" -"U kunt geen overlapping hebben tussen twee belastbare jaren, wijzig aub de " -"start en/of eind datum van uw belastbare jaren." +"U kunt geen overlapping hebben tussen twee fiscale jaren, wijzig aub de " +"start en/of einddatum van uw belastbare jaren." #. module: account #: code:addons/account/models/account_invoice.py:1827 @@ -12100,8 +12100,8 @@ msgid "" "You cannot create journal items with a secondary currency without filling " "both 'currency' and 'amount currency' fields." msgstr "" -"U kunt geen joernaalposten aanmaken met een tweede valuta zonder invulling " -"van beide 'valuta' en 'bedrag valuta' velden." +"U kunt geen dagboeken aanmaken met een tweede valuta zonder invulling van " +"beide 'valuta' en 'bedrag valuta' velden." #. module: account #: code:addons/account/models/company.py:187 @@ -12290,8 +12290,8 @@ msgid "" "You cannot switch an account to prevent the reconciliation if some partial " "reconciliations are still pending." msgstr "" -"You cannot switch an account to prevent the reconciliation if some partial " -"reconciliations are still pending." +"U kan niet wisselen van account om het afletteren te voorkomen als " +"gedeeltelijke afletteringen nog in aanvraag zijn." #. module: account #: code:addons/account/models/account_move.py:1102 diff --git a/addons/account/i18n/ro.po b/addons/account/i18n/ro.po index 7001c5548eb..a8b2c9271da 100644 --- a/addons/account/i18n/ro.po +++ b/addons/account/i18n/ro.po @@ -2827,6 +2827,8 @@ msgid "" "Click to <b>validate your invoice.</b> A reference will be assigned to this " "invoice and you will not be able to modify it anymore." msgstr "" +"Faceți clic pentru a <b>valida factura</b>. O referință va fi atribuită " +"acestei facturi și nu o veți mai putea modifica." #. module: account #. openerp-web @@ -11893,13 +11895,6 @@ msgid "have been reconciled automatically." msgstr "" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "" diff --git a/addons/account/i18n/sv.po b/addons/account/i18n/sv.po index 3b1c0416f00..a75fcdb2e45 100644 --- a/addons/account/i18n/sv.po +++ b/addons/account/i18n/sv.po @@ -2993,7 +2993,7 @@ msgstr "" #. module: account #: model:ir.model,name:account.model_res_config_settings msgid "Config Settings" -msgstr "" +msgstr "Konfigurations Inställningar" #. module: account #: model:ir.ui.menu,name:account.menu_finance_configuration diff --git a/addons/account/i18n/uk.po b/addons/account/i18n/uk.po index 08fe21338d1..485989d2cd2 100644 --- a/addons/account/i18n/uk.po +++ b/addons/account/i18n/uk.po @@ -2911,7 +2911,7 @@ msgstr "" #: code:addons/account/static/src/xml/account_reconciliation.xml:17 #, python-format msgid "Click to Rename" -msgstr "" +msgstr "Натисніть, щоб змінити назву" #. module: account #. openerp-web @@ -3077,7 +3077,7 @@ msgstr "Повний набір податків" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__composer_id msgid "Composer" -msgstr "" +msgstr "Автор" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__composition_mode @@ -3106,7 +3106,7 @@ msgstr "Розрахований баланс" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_reconcile_model_form msgid "Conditions on Bank Statement Line" -msgstr "" +msgstr "Умови на рядку банківського рахунку" #. module: account #: model:ir.model,name:account.model_res_config_settings @@ -3129,12 +3129,12 @@ msgstr "Налаштування" #: code:addons/account/static/src/js/section_and_note_fields_backend.js:102 #, python-format msgid "Configure a product" -msgstr "" +msgstr "Налаштувати товар" #. module: account #: model:ir.actions.act_window,name:account.action_open_account_onboarding_invoice_layout msgid "Configure your document layout" -msgstr "" +msgstr "Налаштуйте макет документа" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_bnk_stmt_cashbox @@ -3188,7 +3188,7 @@ msgstr "Вітаємо, ви все завершили!" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_invoice_onboarding_panel msgid "Congratulations! You are all set." -msgstr "" +msgstr "Вітаємо! Ви все налаштували." #. module: account #: model:ir.model,name:account.model_res_partner @@ -3218,7 +3218,7 @@ msgstr "Містить" #: model:ir.model.fields,field_description:account.field_res_partner__contracts_count #: model:ir.model.fields,field_description:account.field_res_users__contracts_count msgid "Contracts Count" -msgstr "" +msgstr "Підрахунок контрактів" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_journal_form @@ -5765,6 +5765,8 @@ msgid "" "Invoice_${(object.number or '').replace('/','_')}${object.state == 'draft' " "and '_draft' or ''}" msgstr "" +"Invoice_${(object.number or '').replace('/','_')}${object.state == 'draft' " +"and '_draft' or ''}" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_invoice_report_search diff --git a/addons/account/i18n/zh_TW.po b/addons/account/i18n/zh_TW.po index 0cb0eeb472d..7a0ac8e0273 100644 --- a/addons/account/i18n/zh_TW.po +++ b/addons/account/i18n/zh_TW.po @@ -12,6 +12,7 @@ # Andy Cheng <andy.cheng@richsoda.com>, 2019 # sejun huang <sejun.huang@gmail.com>, 2019 # 敬雲 林 <chingyun@yuanchih-consult.com>, 2019 +# Ying Chang <acc.tou@gmail.com>, 2019 # msgid "" msgstr "" @@ -19,7 +20,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: 敬雲 林 <chingyun@yuanchih-consult.com>, 2019\n" +"Last-Translator: Ying Chang <acc.tou@gmail.com>, 2019\n" "Language-Team: Chinese (Taiwan) (https://www.transifex.com/odoo/teams/41243/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -248,7 +249,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_kanban msgid "<i class=\"fa fa-clock-o\" role=\"img\" aria-label=\"Date\" title=\"Date\"/>" -msgstr "" +msgstr "<i class=\"fa fa-clock-o\" role=\"img\" aria-label=\"Date\" title=\"Date\"/>" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_invoice_page @@ -341,6 +342,9 @@ msgid "" "specific.\" aria-label=\"Values set here are company-specific.\" " "groups=\"base.group_multi_company\" role=\"img\"/>" msgstr "" +"<span class=\"fa fa-lg fa-building-o\" title=\"Values set here are company-" +"specific.\" aria-label=\"Values set here are company-specific.\" " +"groups=\"base.group_multi_company\" role=\"img\"/>" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -812,7 +816,7 @@ msgstr "選擇的對帳單明細已被會計憑證調節。" #. module: account #: sql_constraint:account.fiscal.position.tax:0 msgid "A tax fiscal position could be defined only one time on same taxes." -msgstr "" +msgstr "稅務財政情況在相同的稅上面只能定義一次。" #. module: account #: model:res.groups,name:account.group_warning_account @@ -1689,7 +1693,7 @@ msgstr "金額:" #: sql_constraint:account.fiscal.position.account:0 msgid "" "An account fiscal position could be defined only one time on same accounts." -msgstr "" +msgstr "財政狀況只能在一個科目上定義一次。" #. module: account #: model_terms:ir.actions.act_window,help:account.action_account_form @@ -4608,12 +4612,12 @@ msgstr "財務年度" #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_day msgid "Fiscal year last day." -msgstr "" +msgstr "會計年度的最後一天" #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_month msgid "Fiscal year last month." -msgstr "" +msgstr "會計年度的最後一個月" #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__fiscalyear_last_day @@ -5489,7 +5493,7 @@ msgstr "無效的\"郵編範圍\",請正確的配置。" #: code:addons/account/models/company.py:106 #, python-format msgid "Invalid fiscal year last day" -msgstr "" +msgstr "無效的會計年度的最後一天" #. module: account #: code:addons/account/models/account_invoice.py:1358 @@ -11871,13 +11875,6 @@ msgid "have been reconciled automatically." msgstr "已自動調帳" #. module: account -#. openerp-web -#: code:addons/account/static/src/xml/account_reconciliation.xml:44 -#, python-format -msgid "o_manual_statement" -msgstr "o_manual_statement" - -#. module: account #: selection:account.payment.term.line,option:0 msgid "of the current month" msgstr "當月" diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 0730a364a5c..e8a5b60af4c 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -482,7 +482,7 @@ class AccountJournal(models.Model): post_at_bank_rec = fields.Boolean(string="Post At Bank Reconciliation", help="Whether or not the payments made in this journal should be generated in draft state, so that the related journal entries are only posted when performing bank reconciliation.") # alias configuration for 'purchase' type journals - alias_id = fields.Many2one('mail.alias', string='Alias') + alias_id = fields.Many2one('mail.alias', string='Alias', copy=False) alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")) alias_name = fields.Char('Alias Name for Vendor Bills', related='alias_id.alias_name', help="It creates draft vendor bill by sending an email.", readonly=False) diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 778c62d1d61..2695db40ee6 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -604,8 +604,11 @@ class AccountBankStatementLine(models.Model): # Fully reconciled moves are just linked to the bank statement total = self.amount + currency = self.currency_id or statement_currency for aml_rec in payment_aml_rec: - total -= aml_rec.debit - aml_rec.credit + balance = aml_rec.amount_currency if aml_rec.currency_id else aml_rec.balance + aml_currency = aml_rec.currency_id or aml_rec.company_currency_id + total -= aml_currency._convert(balance, currency, aml_rec.company_id, aml_rec.date) aml_rec.with_context(check_move_validity=False).write({'statement_line_id': self.id}) counterpart_moves = (counterpart_moves | aml_rec.move_id) if aml_rec.journal_id.post_at_bank_rec and aml_rec.payment_id and aml_rec.move_id.state == 'draft': diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 2a9218ad65a..fd08620f992 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -123,7 +123,7 @@ class AccountInvoice(models.Model): self.residual_signed = abs(residual) * sign self.residual = abs(residual) digits_rounding_precision = self.currency_id.rounding - if float_is_zero(self.residual, precision_rounding=digits_rounding_precision): + if self.move_id and float_is_zero(self.residual, precision_rounding=digits_rounding_precision): self.reconciled = True else: self.reconciled = False diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index ec3321184b3..605c09784a3 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -410,18 +410,20 @@ class AccountMove(models.Model): @api.multi def _reverse_move(self, date=None, journal_id=None, auto=False): self.ensure_one() - reversed_move = self.copy(default={ - 'date': date, - 'journal_id': journal_id.id if journal_id else self.journal_id.id, - 'ref': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (self.name), - 'auto_reverse': False}) - for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): - acm_line.write({ - 'debit': acm_line.credit, - 'credit': acm_line.debit, - 'amount_currency': -acm_line.amount_currency - }) - self.reverse_entry_id = reversed_move + with self.env.norecompute(): + reversed_move = self.copy(default={ + 'date': date, + 'journal_id': journal_id.id if journal_id else self.journal_id.id, + 'ref': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (self.name), + 'auto_reverse': False}) + for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): + acm_line.write({ + 'debit': acm_line.credit, + 'credit': acm_line.debit, + 'amount_currency': -acm_line.amount_currency + }) + self.reverse_entry_id = reversed_move + self.recompute() return reversed_move @api.multi @@ -550,7 +552,7 @@ class AccountMoveLine(models.Model): #computing the `reconciled` field. reconciled = False digits_rounding_precision = line.company_id.currency_id.rounding - if float_is_zero(amount, precision_rounding=digits_rounding_precision): + if (line.matched_debit_ids or line.matched_credit_ids) and float_is_zero(amount, precision_rounding=digits_rounding_precision): if line.currency_id and line.amount_currency: if float_is_zero(amount_residual_currency, precision_rounding=line.currency_id.rounding): reconciled = True @@ -932,7 +934,7 @@ class AccountMoveLine(models.Model): for line in self: company_ids.add(line.company_id.id) all_accounts.append(line.account_id) - if line.reconciled: + if (line.matched_debit_ids or line.matched_credit_ids) and line.reconciled: raise UserError(_('You are trying to reconcile some entries that are already reconciled.')) if len(company_ids) > 1: raise UserError(_('To reconcile the entries company should be the same for all entries.')) @@ -1081,6 +1083,56 @@ class AccountMoveLine(models.Model): ) return rec_move_ids.unlink() + def _apply_taxes(self, vals, amount): + tax_lines_vals = [] + # Get ids from triplets : https://www.odoo.com/documentation/10.0/reference/orm.html#odoo.models.Model.write + tax_ids = [tax['id'] for tax in self.resolve_2many_commands('tax_ids', vals['tax_ids']) if tax.get('id')] + # Since create() receives ids instead of recordset, let's just use the old-api bridge + taxes = self.env['account.tax'].browse(tax_ids) + currency = self.env['res.currency'].browse(vals.get('currency_id')) + partner = self.env['res.partner'].browse(vals.get('partner_id')) + ctx = dict(self._context) + ctx['round'] = ctx.get('round', True) + res = taxes.with_context(ctx).compute_all(amount, + currency, 1, vals.get('product_id'), partner) + # Adjust line amount if any tax is price_include + if abs(res['total_excluded']) < abs(amount): + if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] + if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] + if vals.get('amount_currency'): + vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (res['total_excluded']/amount)) + # Create tax lines + for tax_vals in res['taxes']: + if tax_vals['amount']: + tax = self.env['account.tax'].browse([tax_vals['id']]) + account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) + if not account_id: account_id = vals['account_id'] + temp = { + 'account_id': account_id, + 'name': vals['name'] + ' ' + tax_vals['name'], + 'tax_line_id': tax_vals['id'], + 'move_id': vals['move_id'], + 'partner_id': vals.get('partner_id'), + 'statement_id': vals.get('statement_id'), + 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, + 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, + 'analytic_account_id': vals.get('analytic_account_id') if tax.analytic else False, + } + bank = self.env["account.bank.statement.line"].browse(vals.get('statement_line_id')).statement_id + if bank.currency_id != bank.company_id.currency_id: + ctx = {} + if 'date' in vals: + ctx['date'] = vals['date'] + elif 'date_maturity' in vals: + ctx['date'] = vals['date_maturity'] + temp['currency_id'] = bank.currency_id.id + temp['amount_currency'] = bank.company_id.currency_id.with_context(ctx).compute(tax_vals['amount'], bank.currency_id, round=True) + if vals.get('tax_exigible'): + temp['tax_exigible'] = True + temp['account_id'] = tax.cash_basis_account.id or account_id + tax_lines_vals.append(temp) + return tax_lines_vals + #################################################### # CRUD methods #################################################### @@ -1522,90 +1574,91 @@ class AccountPartialReconcile(models.Model): self.ensure_one() move_date = self.debit_move_id.date newly_created_move = self.env['account.move'] - for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): - #move_date is the max of the 2 reconciled items - if move_date < move.date: - move_date = move.date - percentage_before = percentage_before_rec[move.id] - percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] - # update the percentage before as the move can be part of - # multiple partial reconciliations - percentage_before_rec[move.id] = percentage_after - - for line in move.line_ids: - if not line.tax_exigible: - #amount is the current cash_basis amount minus the one before the reconciliation - amount = line.balance * percentage_after - line.balance * percentage_before - rounded_amt = self._get_amount_tax_cash_basis(amount, line) - if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): - continue - if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the tax line - to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.move_id.name, - 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'credit': rounded_amt if rounded_amt > 0 else 0.0, - 'account_id': line.account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - # Group by cash basis account and tax - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt if rounded_amt > 0 else 0.0, - 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'account_id': line.tax_line_id.cash_basis_account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_line_id': line.tax_line_id.id, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - if line.account_id.reconcile: - #setting the account to allow reconciliation will help to fix rounding errors - to_clear_aml |= line - to_clear_aml.reconcile() - - if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the base - for tax in line.tax_ids.filtered(lambda t: t.tax_exigibility == 'on_payment'): - account_id = self._get_tax_cash_basis_base_account(line, tax) - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt > 0 and rounded_amt or 0.0, - 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + with self.env.norecompute(): + for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): + #move_date is the max of the 2 reconciled items + if move_date < move.date: + move_date = move.date + percentage_before = percentage_before_rec[move.id] + percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] + # update the percentage before as the move can be part of + # multiple partial reconciliations + percentage_before_rec[move.id] = percentage_after + + for line in move.line_ids: + if not line.tax_exigible: + #amount is the current cash_basis amount minus the one before the reconciliation + amount = line.balance * percentage_after - line.balance * percentage_before + rounded_amt = self._get_amount_tax_cash_basis(amount, line) + if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): + continue + if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the tax line + to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.move_id.name, + 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'credit': rounded_amt if rounded_amt > 0 else 0.0, + 'account_id': line.account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, 'tax_exigible': True, - 'tax_ids': [(6, 0, [tax.id])], - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) + # Group by cash basis account and tax self.env['account.move.line'].with_context(check_move_validity=False).create({ 'name': line.name, - 'credit': rounded_amt > 0 and rounded_amt or 0.0, - 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + 'debit': rounded_amt if rounded_amt > 0 else 0.0, + 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'account_id': line.tax_line_id.cash_basis_account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, + 'tax_line_id': line.tax_line_id.id, 'tax_exigible': True, - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) - + if line.account_id.reconcile: + #setting the account to allow reconciliation will help to fix rounding errors + to_clear_aml |= line + to_clear_aml.reconcile() + + if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the base + for tax in line.tax_ids.filtered(lambda t: t.tax_exigibility == 'on_payment'): + account_id = self._get_tax_cash_basis_base_account(line, tax) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'debit': rounded_amt > 0 and rounded_amt or 0.0, + 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'tax_ids': [(6, 0, [tax.id])], + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, + 'partner_id': line.partner_id.id, + }) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'credit': rounded_amt > 0 and rounded_amt or 0.0, + 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, + 'partner_id': line.partner_id.id, + }) + self.recompute() if newly_created_move: if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date: # The move date should be the maximum date between payment and invoice (in case diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index dd6ec32f869..7646343f7a4 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -211,6 +211,8 @@ class account_abstract_payment(models.AbstractModel): self.amount = abs(self._compute_payment_amount()) # Set by default the first liquidity journal having this currency if exists. + if self.journal_id: + return journal = self.env['account.journal'].search( [('type', 'in', ('bank', 'cash')), ('currency_id', '=', self.currency_id.id)], limit=1) if journal: diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2cc7dd7e791..108538368b8 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -173,7 +173,7 @@ class AccountChartTemplate(models.Model): # (won't work well for multi-company) company = self.env.user.company_id # If we don't have any chart of account on this company, install this chart of account - if not self.existing_accounting(company): + if not company.chart_template_id and not self.existing_accounting(company): self.load_for_current_company(15.0, 15.0) def load_for_current_company(self, sale_tax_rate, purchase_tax_rate): diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index ef09176438b..a091adbd8b8 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -68,7 +68,7 @@ access_account_account_type_invoice,account.account.type invoice,model_account_a access_account_account_tax,account.account.tag,model_account_account_tag,account.group_account_user,1,1,1,1 access_account_account_tax_user,account.account.tag,model_account_account_tag,account.group_account_invoice,1,0,0,0 access_account_reconcile_model,account.reconcile.model,model_account_reconcile_model,account.group_account_user,1,1,1,1 -access_account_reconcile_model_template,account.reconcile.model.template,model_account_reconcile_model_template,account.group_account_user,1,1,1,1 +access_account_reconcile_model_template,account.reconcile.model.template,model_account_reconcile_model_template,account.group_account_invoice,1,1,1,1 access_account_partial_reconcile_group_invoice,account.partial.reconcile.group.invoice,model_account_partial_reconcile,account.group_account_invoice,1,1,1,0 access_account_partial_reconcile,account.partial.reconcile,model_account_partial_reconcile,account.group_account_user,1,1,1,1 access_account_full_reconcile_group_invoice,account.full.reconcile.group.invoice,model_account_full_reconcile,account.group_account_invoice,1,1,1,0 diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 91b200c2d93..1d5eee2a700 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -925,11 +925,11 @@ var StatementModel = BasicModel.extend({ }); var company_currency = session.get_currency(line.st_line.currency_id); var company_precision = company_currency && company_currency.digits[1] || 2; - total = utils.round_decimals(total*1000, company_precision)/1000 || 0; + total = utils.round_decimals(total, company_precision) || 0; if(isOtherCurrencyId){ var other_currency = session.get_currency(isOtherCurrencyId); var other_precision = other_currency && other_currency.digits[1] || 2; - amount_currency = utils.round_decimals(amount_currency, other_precision) + amount_currency = utils.round_decimals(amount_currency, other_precision); } line.balance = { amount: total, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 0d4a025c28c..1543615a491 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -262,7 +262,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'click .accounting_view thead td': '_onTogglePanel', 'click .accounting_view tfoot td:not(.cell_left,.cell_right)': '_onShowPanel', 'click .cell': '_onEditAmount', - 'input input.filter': '_onFilterChange', + 'change input.filter': '_onFilterChange', 'click .match .load-more a': '_onLoadMore', 'click .match .mv_line td': '_onSelectMoveLine', 'click .accounting_view tbody .mv_line td': '_onSelectProposition', diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index e21f5124c9b..57822f75b09 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -149,7 +149,8 @@ <div t-name="reconciliation.line.match"> <div class="match_controls"> - <input class="filter" placeholder="Filter..." type="text" value=""/> + <input class="filter o_input" placeholder="Filter..." type="text" value=""/> + <button class="btn btn-secondary btn-sm fa fa-search" type="button"></button> </div> <table> <tbody> diff --git a/addons/account/tests/account_test_no_chart.py b/addons/account/tests/account_test_no_chart.py index 05aa54df4c6..ca1c971d0a6 100644 --- a/addons/account/tests/account_test_no_chart.py +++ b/addons/account/tests/account_test_no_chart.py @@ -46,6 +46,12 @@ class TestAccountNoChartCommon(SavepointCase): 'property_account_receivable_id': cls.account_receivable.id, }) + cls.sale_journal0 = cls.env['account.journal'].create({ + 'name': 'Sale Journal', + 'type': 'sale', + 'code': 'SJT0', + }) + @classmethod def setUpAdditionalAccounts(cls): """ Set up some addionnal accounts: expenses, revenue, ... """ diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index b077d6112cf..ca35292af5a 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -530,6 +530,14 @@ class TestReconciliation(AccountingTestCase): self.diff_income_account.id: {'debit': 0.0, 'credit': 3.27, 'amount_currency': -5, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 52.33, 'amount_currency': -80, 'currency_id': self.currency_usd_id}, } + + payments = bank_stmt_aml.mapped('payment_id') + # creation and reconciliation of the over-amount statement + # has created an another payment + self.assertEqual(len(payments), 2) + # Check amount of second, automatically created payment + self.assertEqual((payments - payment).amount, 5) + for aml in bank_stmt_aml: line = lines[aml.account_id.id] if type(line) == list: diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index fb263e4be5a..6b4a5df4983 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -96,7 +96,6 @@ </div> <div class="o_setting_right_pane" name="account_taxcloud_right_pane"> <label for="module_account_taxcloud" string="TaxCloud"/> - <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/> <div class="text-muted"> Compute tax rates based on U.S. ZIP codes </div> diff --git a/addons/account_bank_statement_import/i18n/mn.po b/addons/account_bank_statement_import/i18n/mn.po index 5f5f53699cb..481f137662a 100644 --- a/addons/account_bank_statement_import/i18n/mn.po +++ b/addons/account_bank_statement_import/i18n/mn.po @@ -3,14 +3,21 @@ # * account_bank_statement_import # # Translators: +# Bayarkhuu Bataa, 2018 # Martin Trigaux, 2018 +# Otgonbayar.A <gobi.mn@gmail.com>, 2018 +# nasaaskii <nasaaskii@gmail.com>, 2018 +# Onii Onii <onii0223@yahoo.com>, 2018 +# Khishigbat Ganbold <khishigbat@asterisk-tech.mn>, 2018 +# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019 +# msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" +"PO-Revision-Date: 2016-08-05 12:55+0000\n" +"Last-Translator: Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019\n" "Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -222,12 +229,12 @@ msgstr "Үүсгэсэн" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__refund_sequence_id msgid "Credit Note Entry Sequence" -msgstr "" +msgstr "Буцаалтын гүйлгээний дугаарлалт" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__refund_sequence_number_next msgid "Credit Notes: Next Number" -msgstr "" +msgstr "Буцаалт: Дараагийн дугаар" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__currency_id @@ -281,12 +288,12 @@ msgstr "Файлын нэр" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__inbound_payment_method_ids msgid "For Incoming Payments" -msgstr "" +msgstr "Ирж буй төлбөрийн хувьд" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__outbound_payment_method_ids msgid "For Outgoing Payments" -msgstr "" +msgstr "Гарч буй төлбөрийн хувьд" #. module: account_bank_statement_import #: model:ir.model.fields,help:account_bank_statement_import.field_account_bank_statement_import__data_file @@ -398,12 +405,12 @@ msgstr "" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__kanban_dashboard msgid "Kanban Dashboard" -msgstr "" +msgstr "Канбан самбар" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__kanban_dashboard_graph msgid "Kanban Dashboard Graph" -msgstr "" +msgstr "Канбан самбар граффик" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import____last_update diff --git a/addons/account_check_printing/i18n/sv.po b/addons/account_check_printing/i18n/sv.po index baabb1843bf..77e35cf8619 100644 --- a/addons/account_check_printing/i18n/sv.po +++ b/addons/account_check_printing/i18n/sv.po @@ -8,8 +8,8 @@ # Haojun Zou <apollo_zhj@msn.com>, 2018 # Martin Wilderoth <martin.wilderoth@linserv.se>, 2018 # Patrik Lermon <patrik.lermon@gmail.com>, 2018 -# Kim Asplund <kim.asplund@gmail.com>, 2018 # Kristoffer Grundström <hamnisdude@gmail.com>, 2019 +# Kim Asplund <kim.asplund@gmail.com>, 2019 # msgid "" msgstr "" @@ -17,7 +17,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Kristoffer Grundström <hamnisdude@gmail.com>, 2019\n" +"Last-Translator: Kim Asplund <kim.asplund@gmail.com>, 2019\n" "Language-Team: Swedish (https://www.transifex.com/odoo/teams/41243/sv/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -161,7 +161,7 @@ msgstr "" #. module: account_check_printing #: model:ir.model,name:account_check_printing.model_res_config_settings msgid "Config Settings" -msgstr "" +msgstr "Konfigurations Inställningar" #. module: account_check_printing #: model:ir.model.fields,field_description:account_check_printing.field_print_prenumbered_checks__create_uid diff --git a/addons/account_facturx/models/account_invoice.py b/addons/account_facturx/models/account_invoice.py index 619bd64d354..3b5e080162e 100644 --- a/addons/account_facturx/models/account_invoice.py +++ b/addons/account_facturx/models/account_invoice.py @@ -64,12 +64,11 @@ class AccountInvoice(models.Model): with Form(self_ctx, view='account.invoice_supplier_form') as invoice_form: # Partner (first step to avoid warning 'Warning! You must first select a partner.'). - elements = tree.xpath('//ram:SellerTradeParty/ram:Name', namespaces=tree.nsmap) - partner_name = elements and elements[0].text - elements = tree.xpath('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1) if not partner: + elements = tree.xpath('//ram:SellerTradeParty/ram:Name', namespaces=tree.nsmap) + partner_name = elements and elements[0].text partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1) if not partner: elements = tree.xpath('//ram:SellerTradeParty//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap) @@ -161,19 +160,19 @@ class AccountInvoice(models.Model): if product: invoice_line_form.product_id = product + # Quantity. + line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap) + if line_elements: + invoice_line_form.quantity = float(line_elements[0].text) * refund_sign + # Price Unit. line_elements = element.xpath('.//ram:GrossPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap) if line_elements: - invoice_line_form.price_unit = float(line_elements[0].text) + invoice_line_form.price_unit = float(line_elements[0].text) / invoice_line_form.quantity else: line_elements = element.xpath('.//ram:NetPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap) if line_elements: - invoice_line_form.price_unit = float(line_elements[0].text) - - # Quantity. - line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap) - if line_elements: - invoice_line_form.quantity = float(line_elements[0].text) * refund_sign + invoice_line_form.price_unit = float(line_elements[0].text) / invoice_line_form.quantity # Discount. line_elements = element.xpath('.//ram:AppliedTradeAllowanceCharge/ram:CalculationPercent', namespaces=tree.nsmap) diff --git a/addons/account_voucher/i18n/mn.po b/addons/account_voucher/i18n/mn.po index 869c9fdd8ea..3424f08f698 100644 --- a/addons/account_voucher/i18n/mn.po +++ b/addons/account_voucher/i18n/mn.po @@ -3,15 +3,21 @@ # * account_voucher # # Translators: +# Bayarkhuu Bataa, 2018 # Martin Trigaux, 2018 -# nasaaskii <nasaaskii@gmail.com>, 2018 +# Otgonbayar.A <gobi.mn@gmail.com>, 2018 +# Khishigbat Ganbold <khishigbat@asterisk-tech.mn>, 2018 +# Onii Onii <onii0223@yahoo.com>, 2018 +# nasaaskii <nasaaskii@gmail.com>, 2019 +# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019 +# msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: nasaaskii <nasaaskii@gmail.com>, 2018\n" +"PO-Revision-Date: 2016-08-05 12:55+0000\n" +"Last-Translator: Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019\n" "Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -258,7 +264,7 @@ msgstr "Хэрэв сонгосон бол, шинэ зурвасуудад ан #. module: account_voucher #: model:ir.model.fields,help:account_voucher.field_account_voucher__message_has_error msgid "If checked, some messages have a delivery error." -msgstr "" +msgstr "Үүнийг сонговол алдаа үүсэх үед зурвасууд ирнэ." #. module: account_voucher #: model:ir.model.fields,help:account_voucher.field_account_voucher__tax_correction @@ -317,7 +323,7 @@ msgstr "Сүүлийн засвар хийсэн огноо" #. module: account_voucher #: model:ir.model.fields,field_description:account_voucher.field_account_voucher__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Үндсэн хавсралт" #. module: account_voucher #: model:ir.model.fields,field_description:account_voucher.field_account_voucher__message_has_error @@ -359,7 +365,7 @@ msgstr "Үйлдлийн тоо" #. module: account_voucher #: model:ir.model.fields,field_description:account_voucher.field_account_voucher__message_has_error_counter msgid "Number of error" -msgstr "" +msgstr "Алдааны тоо" #. module: account_voucher #: model:ir.model.fields,help:account_voucher.field_account_voucher__message_needaction_counter @@ -369,7 +375,7 @@ msgstr "Үйлдэл шаардлагатай зурвасын тоо" #. module: account_voucher #: model:ir.model.fields,help:account_voucher.field_account_voucher__message_has_error_counter msgid "Number of messages with delivery error" -msgstr "" +msgstr "Алдааны мэдэгдэл бүхий зурвасын тоо" #. module: account_voucher #: model:ir.model.fields,help:account_voucher.field_account_voucher__message_unread_counter diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 05eee5c54ea..d868a1de62f 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -274,6 +274,8 @@ class AccountVoucher(models.Model): :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method) :rtype: tuple(float, list of int) ''' + tax_calculation_rounding_method = self.env.user.company_id.tax_calculation_rounding_method + tax_lines_vals = [] for line in self.line_ids: #create one move line per voucher line where amount is not 0.0 if not line.price_subtotal: @@ -331,6 +333,33 @@ class AccountVoucher(models.Model): self.env['account.move.line'].create(temp) self.env['account.move.line'].create(move_line) + # When global rounding is activated, we must wait until all tax lines are computed to + # merge them. + if tax_calculation_rounding_method == 'round_globally': + tax_lines_vals += self.env['account.move.line'].with_context(round=False)._apply_taxes( + move_line, + move_line.get('debit', 0.0) - move_line.get('credit', 0.0) + ) + + # When round globally is set, we merge the tax lines + if tax_calculation_rounding_method == 'round_globally': + tax_lines_vals_merged = {} + for tax_line_vals in tax_lines_vals: + key = ( + tax_line_vals['tax_line_id'], + tax_line_vals['account_id'], + tax_line_vals['analytic_account_id'], + ) + if key not in tax_lines_vals_merged: + tax_lines_vals_merged[key] = tax_line_vals + else: + tax_lines_vals_merged[key]['debit'] += tax_line_vals['debit'] + tax_lines_vals_merged[key]['credit'] += tax_line_vals['credit'] + currency = self.env['res.currency'].browse(company_currency) + for vals in tax_lines_vals_merged.values(): + vals['debit'] = currency.round(vals['debit']) + vals['credit'] = currency.round(vals['credit']) + self.env['account.move.line'].create(vals) return line_total @api.multi @@ -363,12 +392,12 @@ class AccountVoucher(models.Model): line_total = voucher.with_context(ctx).voucher_move_line_create(line_total, move.id, company_currency, current_currency) # Create a payment to allow the reconciliation when pay_now = 'pay_now'. - if self.pay_now == 'pay_now': - payment_id = self.env['account.payment'].create(self.voucher_pay_now_payment_create()) + if voucher.pay_now == 'pay_now': + payment_id = self.env['account.payment'].create(voucher.voucher_pay_now_payment_create()) payment_id.post() # Reconcile the receipt with the payment - lines_to_reconcile = (payment_id.move_line_ids + move.line_ids).filtered(lambda l: l.account_id == self.account_id) + lines_to_reconcile = (payment_id.move_line_ids + move.line_ids).filtered(lambda l: l.account_id == voucher.account_id) lines_to_reconcile.reconcile() # Add tax correction to move line if any tax correction specified diff --git a/addons/analytic/i18n/mn.po b/addons/analytic/i18n/mn.po index c38493d9b72..e05c2265e18 100644 --- a/addons/analytic/i18n/mn.po +++ b/addons/analytic/i18n/mn.po @@ -3,14 +3,20 @@ # * analytic # # Translators: -# Martin Trigaux, 2018 +# Otgonbayar.A <gobi.mn@gmail.com>, 2018 +# Khishigbat Ganbold <khishigbat@asterisk-tech.mn>, 2018 +# Tsogjav <Tsogjav2007@yahoo.com>, 2018 +# Onii Onii <onii0223@yahoo.com>, 2018 +# Martin Trigaux, 2019 +# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019 +# msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Martin Trigaux, 2018\n" +"PO-Revision-Date: 2016-08-05 12:56+0000\n" +"Last-Translator: Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019\n" "Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -328,7 +334,7 @@ msgstr "Хэрэв сонгосон бол, шинэ зурвасуудад ан #. module: analytic #: model:ir.model.fields,help:analytic.field_account_analytic_account__message_has_error msgid "If checked, some messages have a delivery error." -msgstr "" +msgstr "Үүнийг сонговол алдаа үүсэх үед зурвасууд ирнэ." #. module: analytic #: model:ir.model.fields,help:analytic.field_account_analytic_account__active @@ -382,7 +388,7 @@ msgstr "Сүүлийн засвар хийсэн огноо" #. module: analytic #: model:ir.model.fields,field_description:analytic.field_account_analytic_account__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Үндсэн хавсралт" #. module: analytic #: model:ir.model.fields,field_description:analytic.field_account_analytic_account__message_has_error @@ -419,7 +425,7 @@ msgstr "Үйлдлийн тоо" #. module: analytic #: model:ir.model.fields,field_description:analytic.field_account_analytic_account__message_has_error_counter msgid "Number of error" -msgstr "" +msgstr "Алдааны тоо" #. module: analytic #: model:ir.model.fields,help:analytic.field_account_analytic_account__message_needaction_counter @@ -429,7 +435,7 @@ msgstr "Үйлдэл шаардлагатай зурвасын тоо" #. module: analytic #: model:ir.model.fields,help:analytic.field_account_analytic_account__message_has_error_counter msgid "Number of messages with delivery error" -msgstr "" +msgstr "Алдааны мэдэгдэл бүхий зурвасын тоо" #. module: analytic #: model:ir.model.fields,help:analytic.field_account_analytic_account__message_unread_counter diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 4a1b0bc92cb..85d6d732055 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -53,8 +53,8 @@ class ResPartner(models.Model): the url state components (menu_id, id, view_type) """ res = dict.fromkeys(self.ids, False) - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for partner in self: + base_url = partner.get_base_url() # when required, make sure the partner has a valid signup token if self.env.context.get('signup_valid') and not partner.user_ids: partner.sudo().signup_prepare() diff --git a/addons/base_automation/views/base_automation_view.xml b/addons/base_automation/views/base_automation_view.xml index ae56d4ea8a2..93e1a621f77 100644 --- a/addons/base_automation/views/base_automation_view.xml +++ b/addons/base_automation/views/base_automation_view.xml @@ -11,9 +11,6 @@ <xpath expr="//button[@name='create_action']" position="replace"> </xpath> <xpath expr="//button[@name='unlink_action']" position="replace"> - <button name="unlink_action_for_menu" string="Remove Action" type="object" - attrs="{'invisible':[('binding_model_id','=',False)]}" - help="Remove the contextual action related to this server action"/> </xpath> <div class="oe_title" position="before"> <div class="oe_button_box" name="button_box"> diff --git a/addons/base_import_module/views/base_import_module_view.xml b/addons/base_import_module/views/base_import_module_view.xml index fb18fdfbf49..13c666ad313 100644 --- a/addons/base_import_module/views/base_import_module_view.xml +++ b/addons/base_import_module/views/base_import_module_view.xml @@ -8,6 +8,7 @@ <form string="Import module"> <field name="state" invisible="1"/> <separator string="Import Module" colspan="4"/> + <p class="alert alert-warning" role="alert">Note: you can only import data modules (.xml files and static assets)</p> <group states="init" col="4"> <label for="module_file" string="Select module package to import (.zip file):" colspan="4"/> <field name="module_file" colspan="4"/> diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index a62f4897391..50058bc8ae4 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -56,6 +56,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, { $(window).off("click." + this._id); $(window).off("keydown." + this._id); $(window).off("keyup." + this._id); + this._super(); }, //-------------------------------------------------------------------------- // Public diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index ce56412e9e8..d2370c986e2 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -75,7 +75,7 @@ class Lead(models.Model): kanban_state = fields.Selection([('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')], string='Kanban State', compute='_compute_kanban_state') email_cc = fields.Text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") - description = fields.Text('Notes', tracking=60) + description = fields.Text('Notes') tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', string='Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service") contact_name = fields.Char('Contact Name', tracking=30) partner_name = fields.Char("Company Name", tracking=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity') diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index af694a94e42..4abe323d06e 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -980,7 +980,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid } </field> <field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/> @@ -1042,7 +1041,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid, 'search_default_assigned_to_me': 1 }</field> <field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/> diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml index f405892e692..e3053b661d1 100644 --- a/addons/digest/data/digest_template_data.xml +++ b/addons/digest/data/digest_template_data.xml @@ -10,7 +10,8 @@ <table style="width: 100%; border-spacing: 0; font-family: Helvetica,Arial,Verdana,sans-serif;"> <tr> <td align="center" valign="top" style="border-collapse: collapse; padding: 0"> - % set company, user = ctx['company'], ctx['user'] + % set user = ctx.get('user', user) + % set company = user.company_id % set data = object.compute_kpis(company, user) % set tips = object.compute_tips(company, user) % set kpi_actions = object.compute_kpis_actions(company, user) @@ -136,7 +137,7 @@ <table style="margin-top: 5px; border: 1px solid #e7e7e7; font-size: 15px; width: 100%; max-width: 600px;"> <tr> <td style="border-collapse: collapse; margin: 0; padding: 10px 20px;"> - % if ctx['user'].has_group('base.group_system'): + % if user.has_group('base.group_system'): <div style="margin-top: 20px;"> Want to customize the email? <a href="/web#view_type=form&model=digest.digest&id=${object.id}" target="_blank" style="color: #875A7B;">Choose the metrics you care about</a> diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 9446f5454a2..7809648e48c 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -101,7 +101,7 @@ class Digest(models.Model): for digest in self: for user in digest.user_ids: subject = '%s: %s' % (user.company_id.name, digest.name) - digest.template_id.with_context(user=user, company=user.company_id).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) + digest.template_id.with_context(user=user).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) digest.next_run_date = digest._get_next_run_date() def compute_kpis(self, company, user): diff --git a/addons/gamification/data/gamification_karma_rank_demo.xml b/addons/gamification/data/gamification_karma_rank_demo.xml index 45b57ae34f3..26c639a8cc9 100644 --- a/addons/gamification/data/gamification_karma_rank_demo.xml +++ b/addons/gamification/data/gamification_karma_rank_demo.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<odoo><data noupdate="1"> +<odoo><data noupdate="0"> <!-- Set demo karma for test tour as demo user--> <record id="base.user_demo" model="res.users"> <field name="karma">20</field> @@ -15,7 +15,7 @@ <field name="description_motivational" type="html"> <div class="media align-items-center"> <div class="media-body">Reach the next rank and gain a very nice mug !</div> - <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_mug.jpg"/> + <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_mug.png"/> </div></field> <field name="karma_min">100</field> <field name="image" type="base64" file="gamification/static/img/rank_student_badge.svg"/> @@ -27,7 +27,7 @@ <field name="description_motivational" type="html"> <div class="media align-items-center"> <div class="media-body">Reach the next rank and gain a very magic wand !</div> - <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_wand.jpg"/> + <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_wand.png"/> </div></field> <field name="karma_min">500</field> <field name="image" type="base64" file="gamification/static/img/rank_bachelor_badge.svg"/> @@ -39,7 +39,7 @@ <field name="description_motivational" type="html"> <div class="media align-items-center"> <div class="media-body">Reach the next rank and gain a very nice hat !</div> - <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_hat.jpg"/> + <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_hat.png"/> </div></field> <field name="karma_min">2000</field> <field name="image" type="base64" file="gamification/static/img/rank_master_badge.svg"/> @@ -51,7 +51,7 @@ <field name="description_motivational" type="html"> <div class="media align-items-center"> <div class="media-body">Reach the next rank and gain a week-end with a unicorn !</div> - <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.jpg"/> + <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.png"/> </div></field> <field name="karma_min">10000</field> <field name="image" type="base64" file="gamification/static/img/rank_doctor_badge.svg"/> diff --git a/addons/gamification/static/img/rank_misc_hat.jpg b/addons/gamification/static/img/rank_misc_hat.jpg Binary files differdeleted file mode 100644 index 68abf33bc14..00000000000 --- a/addons/gamification/static/img/rank_misc_hat.jpg +++ /dev/null diff --git a/addons/gamification/static/img/rank_misc_hat.png b/addons/gamification/static/img/rank_misc_hat.png Binary files differnew file mode 100644 index 00000000000..172219569c0 --- /dev/null +++ b/addons/gamification/static/img/rank_misc_hat.png diff --git a/addons/gamification/static/img/rank_misc_mug.jpg b/addons/gamification/static/img/rank_misc_mug.jpg Binary files differdeleted file mode 100644 index 09cc5567b0e..00000000000 --- a/addons/gamification/static/img/rank_misc_mug.jpg +++ /dev/null diff --git a/addons/gamification/static/img/rank_misc_mug.png b/addons/gamification/static/img/rank_misc_mug.png Binary files differnew file mode 100644 index 00000000000..08e1fae9a1f --- /dev/null +++ b/addons/gamification/static/img/rank_misc_mug.png diff --git a/addons/gamification/static/img/rank_misc_unicorn.jpg b/addons/gamification/static/img/rank_misc_unicorn.jpg Binary files differdeleted file mode 100644 index 5612d5f7855..00000000000 --- a/addons/gamification/static/img/rank_misc_unicorn.jpg +++ /dev/null diff --git a/addons/gamification/static/img/rank_misc_unicorn.png b/addons/gamification/static/img/rank_misc_unicorn.png Binary files differnew file mode 100644 index 00000000000..c22056dcabe --- /dev/null +++ b/addons/gamification/static/img/rank_misc_unicorn.png diff --git a/addons/gamification/static/img/rank_misc_wand.jpg b/addons/gamification/static/img/rank_misc_wand.jpg Binary files differdeleted file mode 100644 index 4e2ec0457f3..00000000000 --- a/addons/gamification/static/img/rank_misc_wand.jpg +++ /dev/null diff --git a/addons/gamification/static/img/rank_misc_wand.png b/addons/gamification/static/img/rank_misc_wand.png Binary files differnew file mode 100644 index 00000000000..0101c36f8d9 --- /dev/null +++ b/addons/gamification/static/img/rank_misc_wand.png diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index ac1793f428f..9155cf9a0fa 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -442,7 +442,9 @@ class GoogleCalendar(models.AbstractModel): if google_attendee.get('found'): continue - attendee = ResPartner.search([('email', '=', google_attendee['email'])], limit=1) + attendee = ResPartner.search([('email', '=ilike', google_attendee['email']), ('user_ids', '!=', False)], limit=1) + if not attendee: + attendee = ResPartner.search([('email', '=ilike', google_attendee['email'])], limit=1) if not attendee: data = { 'email': partner_email, diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml index 09e16244846..aa08e5f0762 100644 --- a/addons/hr/security/hr_security.xml +++ b/addons/hr/security/hr_security.xml @@ -21,6 +21,13 @@ <field name="groups_id" eval="[(4,ref('group_hr_manager'))]"/> </record> + <record id="hr_employee_comp_rule" model="ir.rule"> + <field name="name">Employee multi company rule</field> + <field name="model_id" ref="model_hr_employee"/> + <field name="global" eval="True"/> + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + </record> + <record id="hr_dept_comp_rule" model="ir.rule"> <field name="name">Department multi company rule</field> <field name="model_id" ref="model_hr_department"/> diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 235c4e996b8..9f71be2a9c4 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -773,6 +773,17 @@ </field> </record> + <record id="view_users_form_simple_modif_resource" model="ir.ui.view"> + <field name="name">res.users.preferences.form.resource</field> + <field name="model">res.users</field> + <field name="inherit_id" ref="base.view_users_form_simple_modif" /> + <field name="arch" type="xml"> + <field name="tz" position="attributes"> + <attribute name="required">1</attribute> + </field> + </field> + </record> + <menuitem action="open_module_tree_department" id="menu_hr_department_tree" diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js index 4db5eb90b0a..c7640527297 100644 --- a/addons/hr_attendance/static/src/js/kiosk_confirm.js +++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js @@ -9,9 +9,8 @@ var QWeb = core.qweb; var KioskConfirm = AbstractAction.extend({ events: { "click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, - "click .o_hr_attendance_sign_in_out_icon": function () { + "click .o_hr_attendance_sign_in_out_icon": _.debounce(function () { var self = this; - this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); this._rpc({ model: 'hr.employee', method: 'attendance_manual', @@ -22,10 +21,9 @@ var KioskConfirm = AbstractAction.extend({ self.do_action(result.action); } else if (result.warning) { self.do_warn(result.warning); - self.$('.o_hr_attendance_sign_in_out_icon').removeAttr("disabled"); } }); - }, + }, 200, true), 'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); }, 'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); }, 'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); }, @@ -37,7 +35,7 @@ var KioskConfirm = AbstractAction.extend({ 'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); }, 'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); }, 'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); }, - 'click .o_hr_attendance_pin_pad_button_ok': function() { + 'click .o_hr_attendance_pin_pad_button_ok': _.debounce(function() { var self = this; this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled"); this._rpc({ @@ -54,7 +52,7 @@ var KioskConfirm = AbstractAction.extend({ setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500); } }); - }, + }, 200, true), }, init: function (parent, action) { diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index 4ea44093949..b79469596af 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -8,10 +8,9 @@ var core = require('web.core'); var MyAttendances = AbstractAction.extend({ contentTemplate: 'HrAttendanceMyMainMenu', events: { - "click .o_hr_attendance_sign_in_out_icon": function() { - this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); + "click .o_hr_attendance_sign_in_out_icon": _.debounce(function() { this.update_attendance(); - }, + }, 200, true), }, willStart: function () { diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 672ae7ecb33..2666a9d01ad 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -276,7 +276,7 @@ class HrExpense(models.Model): account_date = expense.sheet_id.accounting_date or expense.date or fields.Date.context_today(expense) company_currency = expense.company_id.currency_id - different_currency = expense.currency_id != company_currency + different_currency = expense.currency_id and expense.currency_id != company_currency move_line_values = [] taxes = expense.tax_ids.with_context(round=True).compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id) @@ -285,13 +285,17 @@ class HrExpense(models.Model): partner_id = expense.employee_id.address_home_id.commercial_partner_id.id # source move line - amount_currency = expense.total_amount if different_currency else False + amount = taxes['total_excluded'] + amount_currency = False + if different_currency: + amount = expense.currency_id._convert(amount, company_currency, expense.company_id, account_date) + amount_currency = taxes['total_excluded'] move_line_src = { 'name': move_line_name, 'quantity': expense.quantity or 1, - 'debit': taxes['total_excluded'] > 0 and taxes['total_excluded'], - 'credit': taxes['total_excluded'] < 0 and -taxes['total_excluded'], - 'amount_currency': taxes['total_excluded'] > 0 and abs(amount_currency) or -abs(amount_currency), + 'debit': amount if amount > 0 else 0, + 'credit': -amount if amount < 0 else 0, + 'amount_currency': amount_currency if different_currency else 0.0, 'account_id': account_src.id, 'product_id': expense.product_id.id, 'product_uom_id': expense.product_uom_id.id, @@ -308,23 +312,25 @@ class HrExpense(models.Model): # taxes move lines for tax in taxes['taxes']: - price = expense.currency_id._convert( - tax['amount'], company_currency, expense.company_id, account_date) - amount_currency = price if different_currency else False + amount = tax['amount'] + amount_currency = False + if different_currency: + amount = expense.currency_id._convert(amount, company_currency, expense.company_id, account_date) + amount_currency = tax['amount'] move_line_tax_values = { 'name': tax['name'], 'quantity': 1, - 'debit': price > 0 and price, - 'credit': price < 0 and -price, - 'amount_currency': price > 0 and abs(amount_currency) or -abs(amount_currency), + 'debit': amount if amount > 0 else 0, + 'credit': -amount if amount < 0 else 0, + 'amount_currency': amount_currency if different_currency else 0.0, 'account_id': tax['account_id'] or move_line_src['account_id'], 'tax_line_id': tax['id'], 'expense_id': expense.id, 'partner_id': partner_id, - 'currency_id': expense.currency_id if different_currency else False, + 'currency_id': expense.currency_id.id if different_currency else False, } - total_amount -= price - total_amount_currency -= move_line_tax_values['amount_currency'] or price + total_amount -= amount + total_amount_currency -= move_line_tax_values['amount_currency'] or amount move_line_values.append(move_line_tax_values) # destination move line @@ -334,7 +340,7 @@ class HrExpense(models.Model): 'credit': total_amount < 0 and -total_amount, 'account_id': account_dst, 'date_maturity': account_date, - 'amount_currency': total_amount > 0 and abs(amount_currency) or -abs(amount_currency), + 'amount_currency': total_amount_currency if different_currency else 0.0, 'currency_id': expense.currency_id.id if different_currency else False, 'expense_id': expense.id, 'partner_id': partner_id, diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py index 62eb6367365..66a56b2f557 100644 --- a/addons/hr_expense/tests/test_expenses.py +++ b/addons/hr_expense/tests/test_expenses.py @@ -78,6 +78,73 @@ class TestAccountEntry(TestExpenseCommon): self.assertAlmostEquals(self.analytic_account.line_ids[0].amount, -636.36, "Amount on the only AAL is wrong") self.assertEquals(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense") + def test_account_entry_multi_currency(self): + """ Checking accounting move entries and analytic entries when submitting expense. With + multi-currency. And taxes. """ + + # Clean-up the rates + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref('base.USD').id, self.env.user.company_id.id]) + self.env['res.currency.rate'].search([]).unlink() + self.env['res.currency.rate'].create({ + 'currency_id': self.env.ref('base.EUR').id, + 'company_id': self.env.user.company_id.id, + 'rate': 2.0, + 'name': '2010-01-01', + }) + + expense = self.env['hr.expense.sheet'].create({ + 'name': 'Expense for Dick Tracy', + 'employee_id': self.employee.id, + }) + expense_line = self.env['hr.expense'].create({ + 'name': 'Choucroute Saucisse', + 'employee_id': self.employee.id, + 'product_id': self.product_expense.id, + 'unit_amount': 700.00, + 'tax_ids': [(6, 0, [self.tax.id])], + 'sheet_id': expense.id, + 'analytic_account_id': self.analytic_account.id, + 'currency_id': self.env.ref('base.EUR').id, + }) + expense_line._onchange_product_id() + # State should default to draft + self.assertEquals(expense.state, 'draft', 'Expense should be created in Draft state') + # Submitted to Manager + expense.action_submit_sheet() + self.assertEquals(expense.state, 'submit', 'Expense is not in Reported state') + # Approve + expense.approve_expense_sheets() + self.assertEquals(expense.state, 'approve', 'Expense is not in Approved state') + # Create Expense Entries + expense.action_sheet_move_create() + self.assertEquals(expense.state, 'post', 'Expense is not in Waiting Payment state') + self.assertTrue(expense.account_move_id.id, 'Expense Journal Entry is not created') + + # Should get this result [(0.0, 350.0, -700.0), (318.18, 0.0, 636.36), (31.82, 0.0, 63.64)] + for line in expense.account_move_id.line_ids: + if line.credit: + self.assertAlmostEquals(line.credit, 350.0) + self.assertAlmostEquals(line.amount_currency, -700.0) + self.assertEquals(len(line.analytic_line_ids), 0, "The credit move line should not have analytic lines") + self.assertFalse(line.product_id, "Product of credit move line should be false") + else: + if not line.tax_line_id == self.tax: + self.assertAlmostEquals(line.debit, 318.18) + self.assertAlmostEquals(line.amount_currency, 636.36) + self.assertEquals(len(line.analytic_line_ids), 1, "The debit move line should have 1 analytic lines") + self.assertEquals(line.product_id, self.product_expense, "Product of debit move line should be the one from the expense") + else: + self.assertAlmostEquals(line.debit, 31.82) + self.assertAlmostEquals(line.amount_currency, 63.64) + self.assertEquals(len(line.analytic_line_ids), 0, "The tax move line should not have analytic lines") + self.assertFalse(line.product_id, "Product of tax move line should be false") + + self.assertEquals(self.analytic_account.line_ids, expense.account_move_id.mapped('line_ids.analytic_line_ids')) + self.assertEquals(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line") + self.assertAlmostEquals(self.analytic_account.line_ids[0].amount, -318.18, "Amount on the only AAL is wrong") + self.assertAlmostEquals(self.analytic_account.line_ids[0].currency_id, self.env.user.company_id.currency_id, "Currency on the only AAL is wrong") + self.assertEquals(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense") + def test_expense_from_email(self): user_demo = self.env.ref('base.user_demo') self.tax.price_include = False diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 38c0f6a4644..65760f4f6d1 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -437,7 +437,7 @@ class HolidaysRequest(models.Model): """ Returns a float equals to the timedelta between two dates given as string.""" if employee_id: employee = self.env['hr.employee'].browse(employee_id) - return employee.get_work_days_data(date_from, date_to)['days'] + return employee._get_work_days_data(date_from, date_to)['days'] today_hours = self.env.user.company_id.resource_calendar_id.get_work_hours_count( datetime.combine(date_from.date(), time.min), @@ -568,12 +568,15 @@ class HolidaysRequest(models.Model): @api.multi def _create_resource_leave(self): - """ This method will create entry in resource calendar time off object at the time of holidays validated """ + """ This method will create entry in resource calendar time off object at the time of holidays validated + :returns: created `resource.calendar.leaves` + """ + vals_list = [] for leave in self: date_from = fields.Datetime.from_string(leave.date_from) date_to = fields.Datetime.from_string(leave.date_to) - self.env['resource.calendar.leaves'].create({ + vals_list.append({ 'name': leave.name, 'date_from': fields.Datetime.to_string(date_from), 'holiday_id': leave.id, @@ -582,7 +585,7 @@ class HolidaysRequest(models.Model): 'calendar_id': leave.employee_id.resource_calendar_id.id, 'time_type': leave.holiday_status_id.time_type, }) - return True + return self.env['resource.calendar.leaves'].create(vals_list) @api.multi def _remove_resource_leave(self): @@ -634,7 +637,7 @@ class HolidaysRequest(models.Model): 'request_date_from': self.date_from, 'request_date_to': self.date_to, 'notes': self.notes, - 'number_of_days': employee.get_work_days_data(self.date_from, self.date_to)['days'], + 'number_of_days': employee._get_work_days_data(self.date_from, self.date_to)['days'], 'parent_id': self.id, 'employee_id': employee.id } diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 87ed680ccf6..206a3933555 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -178,8 +178,8 @@ class HolidaysAllocation(models.Model): if period_start <= creation_date: period_start = creation_date - worked = holiday.employee_id.get_work_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] - left = holiday.employee_id.get_leave_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] + worked = holiday.employee_id._get_work_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] + left = holiday.employee_id._get_leave_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days'] prorata = worked / (left + worked) if worked else 0 days_to_give = holiday.number_per_interval diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index daa49d1e989..8d461c53a78 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -294,7 +294,10 @@ class HolidaysType(models.Model): if record.allocation_type != 'no': name = "%(name)s (%(count)s)" % { 'name': name, - 'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, float_round(record.max_leaves, precision_digits=2) or 0.0) + 'count': _('%g remaining out of %g') % ( + float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, + float_round(record.max_leaves, precision_digits=2) or 0.0, + ) } res.append((record.id, name)) return res diff --git a/addons/hr_payroll/__manifest__.py b/addons/hr_payroll/__manifest__.py index e8e69c66a45..0ea4b76f672 100644 --- a/addons/hr_payroll/__manifest__.py +++ b/addons/hr_payroll/__manifest__.py @@ -30,7 +30,6 @@ 'views/report_contributionregister_templates.xml', 'views/report_payslip_templates.xml', 'views/hr_benefit_views.xml', - 'views/hr_benefit_views.xml', 'views/hr_leave_views.xml', 'views/resource_views.xml', 'views/hr_benefit_template.xml', diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py index bafe3fd7414..6c3367fd3aa 100644 --- a/addons/hr_payroll/models/hr_benefit.py +++ b/addons/hr_payroll/models/hr_benefit.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import pytz import itertools +import pytz + from psycopg2 import IntegrityError from dateutil.relativedelta import relativedelta -from odoo import api, fields, models, exceptions, _ -from odoo.tools import mute_logger -from odoo.exceptions import UserError, ValidationError + +from odoo import api, fields, models, _ from odoo.addons.resource.models.resource import Intervals +from odoo.exceptions import ValidationError +from odoo.tools import mute_logger + class HrBenefit(models.Model): _name = 'hr.benefit' @@ -17,8 +20,7 @@ class HrBenefit(models.Model): name = fields.Char(required=True) active = fields.Boolean(default=True) - employee_id = fields.Many2one('hr.employee', required=True, - domain=lambda self: [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]) + employee_id = fields.Many2one('hr.employee', required=True, domain=[('contract_ids.state', 'in', ('open', 'pending'))]) date_start = fields.Datetime(required=True, string='From') date_stop = fields.Datetime(string='To') duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period") @@ -37,34 +39,25 @@ class HrBenefit(models.Model): default=lambda self: self.env['res.company']._company_default_get()) _sql_constraints = [ - ('_unique', 'unique (employee_id, date_start, date_stop, benefit_type_id)', "Benefit already exists for this attendence"), + ('_unique', 'unique (date_start, date_stop, employee_id, benefit_type_id)', "Benefit already exists for this attendance"), + ('_benefit_has_end', 'check (date_stop IS NOT NULL OR duration <> 0)', 'Benefit must end. Please define an end date or a duration.'), + ('_benefit_start_before_end', 'check (date_stop is null OR (date_stop > date_start))', 'Starting time should be before end time.') ] - @api.constrains('date_stop', 'duration') - def _check_validity_benefit_ends(self): - """ verifies if benefit has an end. """ - for benefit in self: - if not (benefit.date_stop or benefit.duration): - raise exceptions.ValidationError(_('Benefit must end. Please define an end date or a duration.')) - - @api.constrains('date_start', 'date_stop') - def _check_validity_start_before_end(self): - """ verifies if benefit has an end. """ - for benefit in self: - if benefit.date_stop < benefit.date_start: - raise exceptions.ValidationError(_('Starting time should be before end time.')) - - @api.onchange('duration') def _onchange_duration(self): self._inverse_duration() + def _get_duration(self, date_start, date_stop): + if not date_start or not date_stop: + return 0 + dt = date_stop - date_start + return dt.days * 24 + dt.seconds / 3600 # Number of hours + @api.depends('date_stop', 'date_start') def _compute_duration(self): for benefit in self: - if benefit.date_start and benefit.date_stop: - dt = benefit.date_stop - benefit.date_start - benefit.duration = dt.days * 24 + dt.seconds / 3600 # Number of hours + benefit.duration = benefit._get_duration(benefit.date_start, benefit.date_stop) def _inverse_duration(self): for benefit in self: @@ -81,25 +74,22 @@ class HrBenefit(models.Model): return super(HrBenefit, self).write(vals) @api.multi - def check_if_error(self): + def _check_if_error(self): if not self: return False undefined_type = self.filtered(lambda b: not b.benefit_type_id) undefined_type.write({'display_warning': True}) - conflict = self._compute_schedule_conflicts() - conflict_with_leaves = self.compute_conflicts_leaves_to_approve() + conflict = self._mark_conflicting_benefits(min(self.mapped('date_start')), max(self.mapped('date_stop'))) + conflict_with_leaves = self._compute_conflicts_leaves_to_approve() return undefined_type or conflict or conflict_with_leaves - @api.multi - def _compute_schedule_conflicts(self): + @api.model + def _mark_conflicting_benefits(self, start, stop): conflict = False - date_start_benefits = min(self.mapped('date_start')) - date_stop_benefits = max(self.mapped('date_stop')) domain = [ - ('date_start', '<', date_stop_benefits), - ('date_stop', '>', date_start_benefits), + ('date_start', '<', stop), + ('date_stop', '>', start), ] - benefs = self.search(domain) benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id) for employee, benefs in benefits_by_employee: @@ -111,8 +101,7 @@ class HrBenefit(models.Model): return conflict @api.multi - def compute_conflicts_leaves_to_approve(self): - + def _compute_conflicts_leaves_to_approve(self): if not self: return False @@ -137,26 +126,35 @@ class HrBenefit(models.Model): }) return bool(conflicts) - def safe_duplicate_create(self, vals): + def _safe_duplicate_create(self, vals_list, date_start, date_stop): """ - Create benefit but silently abord if it already exists in the database (according to the _unique constraints). - @return: new record if it didn't exist, empty recordset otherwise. + Create benefits between date_start and date_stop according to vals_list. + Skip the values in vals_list if a benefit already exists for the given + date_start, date_stop, employee_id, benefit_type_id + :return: new record id if it didn't exist. """ - try: - with mute_logger('odoo.sql_db'), self.env.cr.savepoint(): - # Any database call will be run inside a postgresql savepoint which is - # rolledback if an exception occurs allowing to make subsequent calls in the transaction. - # (Otherwise InternalError is raised by any subsequent database operations) - return self.create(vals) - except IntegrityError as err: - if 'hr_benefit__unique' not in err.pgerror: - raise err - return self.env['hr.benefit'] + # The search_read should be fast as date_start and date_stop are indexed from the + # unique sql constraint + month_recs = self.search_read([('date_start', '>=', date_start), ('date_stop', '<=', date_stop)], + ['employee_id', 'date_start', 'date_stop', 'benefit_type_id']) + existing_entries = {( + r['date_start'], + r['date_stop'], + r['employee_id'][0], + r['benefit_type_id'][0] if r['benefit_type_id'] else False, + ) for r in month_recs} + new_vals = [v for v in vals_list if (v['date_start'].replace(tzinfo=None), v['date_stop'].replace(tzinfo=None), v['employee_id'], v['benefit_type_id']) not in existing_entries] + # Remove duplicates from vals_list, shouldn't be necessary from saas-12.2 + unique_new_vals = set() + for values in new_vals: + unique_new_vals.add(tuple(values.items())) + new_vals = [dict(values) for values in unique_new_vals] + return self.create(new_vals) def action_leave(self): leave = self.leave_id return { - 'type':'ir.actions.act_window', + 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_id': leave.id, @@ -164,7 +162,7 @@ class HrBenefit(models.Model): 'views': [[False, 'form']], } - def split_by_day(self): + def _split_by_day(self): """ Split the benefit by days and unlink the original benefit. @return recordset @@ -184,6 +182,8 @@ class HrBenefit(models.Model): return [(start, end) for start, end in days if start != end] new_benefits = self.env['hr.benefit'] + benefits_to_unlink = self.env['hr.benefit'] + for benefit in self: if benefit.date_start.date() == benefit.date_stop.date(): new_benefits |= benefit @@ -197,7 +197,7 @@ class HrBenefit(models.Model): 'contract_id': benefit.contract_id.id, } benefit_state = benefit.state - benefit.unlink() + benefits_to_unlink |= benefit for start, stop in _split_range_by_day(benefit_start, benefit_stop): values['date_start'] = start.astimezone(pytz.utc) values['date_stop'] = stop.astimezone(pytz.utc) @@ -206,6 +206,7 @@ class HrBenefit(models.Model): new_benefit.state = benefit_state new_benefits |= new_benefit + benefits_to_unlink.unlink() return new_benefits @api.multi @@ -213,31 +214,36 @@ class HrBenefit(models.Model): """ Duplicate data to keep the complexity in benefit and not mess up payroll, etc. """ + attendance_type = self.env.ref('hr_payroll.benefit_type_attendance') attendance_benefits = self.filtered(lambda b: not b.benefit_type_id.is_leave and # Normal benefit are global to all employees -> avoid duplicating it - not b.benefit_type_id == self.env.ref('hr_payroll.benefit_type_attendance')) + not b.benefit_type_id == attendance_type) leave_benefits = self.filtered(lambda b: b.benefit_type_id.is_leave) + benefits_to_duplicate = self.env['hr.benefit'] for benefit in attendance_benefits: - benefit = benefit.split_by_day() - benefit._duplicate_to_calendar_attendance() + benefit = benefit._split_by_day() + benefits_to_duplicate |= benefit + + benefits_to_duplicate._duplicate_to_calendar_attendance() leave_benefits._duplicate_to_calendar_leave() @api.multi def _duplicate_to_calendar_leave(self): - + vals_list = [] for benefit in self: if not benefit.leave_id: - tz = pytz.timezone(benefit.employee_id.tz) - self.env['resource.calendar.leaves'].create({ + vals_list += [{ 'name': benefit.name, 'date_from': benefit.date_start, 'date_to': benefit.date_stop, 'calendar_id': benefit.employee_id.resource_calendar_id.id, 'resource_id': benefit.employee_id.resource_id.id, 'benefit_type_id': benefit.benefit_type_id.id, - }) + }] + if vals_list: + self.env['resource.calendar.leaves'].create(vals_list) @api.multi def _duplicate_to_calendar_attendance(self): @@ -251,32 +257,35 @@ class HrBenefit(models.Model): if any(data[0].date() != data[1].date() for data in mapped_data.values()): raise ValidationError(_("You can't validate a benefit that covers several days.")) + vals_list = [] for benefit in self: start, end = mapped_data.get(benefit) - self.env['resource.calendar.attendance'].create({ + vals_list += [{ 'name': benefit.name, 'dayofweek': str(start.weekday()), 'date_from': start.date(), 'date_to': end.date(), - 'hour_from':start.hour + start.minute/60, - 'hour_to': end.hour + end.minute/60, + 'hour_from': start.hour + start.minute / 60, + 'hour_to': end.hour + end.minute / 60, 'calendar_id': benefit.contract_id.resource_calendar_id.id, 'day_period': 'morning' if end.hour <= 12 else 'afternoon', 'resource_id': benefit.employee_id.resource_id.id, 'benefit_type_id': benefit.benefit_type_id.id, - }) + }] + self.env['resource.calendar.attendance'].create(vals_list) @api.model def action_validate(self, ids): benefits = self.env['hr.benefit'].search([('id', 'in', ids), ('state', '!=', 'validated')]) benefits.write({'display_warning': False}) - if not benefits.check_if_error(): + if not benefits._check_if_error(): benefits.write({'state': 'validated'}) benefits._duplicate_to_calendar() return True return False + class HrBenefitType(models.Model): _name = 'hr.benefit.type' _description = 'hr.benefit.type' @@ -285,8 +294,9 @@ class HrBenefitType(models.Model): code = fields.Char() color = fields.Integer(default=1) # Will be used with the new calendar/kanban view sequence = fields.Integer(default=25) - active = fields.Boolean('Active', default=True, - help="If the active field is set to false, it will allow you to hide the benefit type without removing it.") + active = fields.Boolean( + 'Active', default=True, + help="If the active field is set to false, it will allow you to hide the benefit type without removing it.") is_leave = fields.Boolean(default=False, string="Leave") class Contacts(models.Model): @@ -301,4 +311,4 @@ class Contacts(models.Model): _sql_constraints = [ ('user_id_employee_id_unique', 'UNIQUE(user_id,employee_id)', 'You cannot have twice the same employee.') - ] + ]
\ No newline at end of file diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py index ec3d6df3198..527f70c41ad 100644 --- a/addons/hr_payroll/models/hr_employee.py +++ b/addons/hr_payroll/models/hr_employee.py @@ -44,12 +44,15 @@ class HrEmployee(models.Model): @api.model def generate_benefit(self, date_start, date_stop): - def _format_datetime(date): fmt = '%Y-%m-%d %H:%M:%S' date = datetime.strptime(date, fmt) if isinstance(date, str) else date return date.replace(tzinfo=pytz.utc) if not date.tzinfo else date + attendance_type = self.env.ref('hr_payroll.benefit_type_attendance') + vals_list = [] + leaves = self.env['hr.leave'] + date_start = _format_datetime(date_start) date_stop = _format_datetime(date_stop) @@ -70,9 +73,10 @@ class HrEmployee(models.Model): r.date_to.replace(tzinfo=pytz.utc) >= date_start ) global_leaves = employee.resource_calendar_id.global_leave_ids - (emp_leaves | global_leaves).mapped('holiday_id').copy_to_benefits() - new_benefits = self.env['hr.benefit'] + employee_leaves = (emp_leaves | global_leaves).mapped('holiday_id') + vals_list.extend(employee_leaves._get_benefits_values()) + for contract in contracts: date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc)) @@ -83,8 +87,8 @@ class HrEmployee(models.Model): attendances = calendar._work_intervals(date_start_benefits, date_stop_benefits, resource=resource) # Attendances for interval in attendances: - benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or self.env.ref('hr_payroll.benefit_type_attendance') - new_benefits |= self.env['hr.benefit'].safe_duplicate_create({ + benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or attendance_type + vals_list += [{ 'name': "%s: %s" % (benefit_type_id.name, employee.name), 'date_start': interval[0].astimezone(pytz.utc), 'date_stop': interval[1].astimezone(pytz.utc), @@ -92,6 +96,7 @@ class HrEmployee(models.Model): 'employee_id': employee.id, 'contract_id': contract.id, 'state': 'confirmed', - }) + }] - new_benefits.compute_conflicts_leaves_to_approve() + new_benefits = self.env['hr.benefit']._safe_duplicate_create(vals_list, date_start, date_stop) + new_benefits._compute_conflicts_leaves_to_approve() diff --git a/addons/hr_payroll/models/hr_leave.py b/addons/hr_payroll/models/hr_leave.py index c4cee2cf791..e69aaa8b85f 100644 --- a/addons/hr_payroll/models/hr_leave.py +++ b/addons/hr_payroll/models/hr_leave.py @@ -18,13 +18,14 @@ class HrLeave(models.Model): _inherit = 'hr.leave' @api.multi - def copy_to_benefits(self): + def _get_benefits_values(self): + vals_list = [] for leave in self: contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close']) start = max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())) end = min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())) benefit_type = leave.holiday_status_id.benefit_type_id - self.env['hr.benefit'].safe_duplicate_create({ + vals_list += [{ 'name': "%s%s" % (benefit_type.name + ": " if benefit_type else "", leave.employee_id.name), 'date_start': start, 'date_stop': end, @@ -34,7 +35,8 @@ class HrLeave(models.Model): 'leave_id': leave.id, 'state': 'confirmed', 'contract_id': contract.id, - }) + }] + return vals_list @api.multi def _create_resource_leave(self): @@ -43,7 +45,10 @@ class HrLeave(models.Model): This is needed in order to compute the correct number of hours/days of the leave according to the contract's calender. """ - super(HrLeave, self)._create_resource_leave() + resource_leaves = super(HrLeave, self)._create_resource_leave() + for resource_leave in resource_leaves: + resource_leave.benefit_type_id = resource_leave.holiday_id.holiday_status_id.benefit_type_id.id + resource_leave_values = [] for leave in self.filtered(lambda l: l.employee_id): @@ -61,8 +66,7 @@ class HrLeave(models.Model): 'calendar_id': contract.resource_calendar_id.id, }] - self.env['resource.calendar.leaves'].create(resource_leave_values) - return True + return resource_leaves | self.env['resource.calendar.leaves'].create(resource_leave_values) @api.constrains('date_from', 'date_to') def _check_contracts(self): @@ -101,7 +105,7 @@ class HrLeave(models.Model): """ benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)]) if benefits: - self.copy_to_benefits() + vals_list = self._get_benefits_values() # create new benefits where the leave does not cover the full benefit benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits]) leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self]) @@ -116,7 +120,7 @@ class HrLeave(models.Model): benefit_start = interval[0] + relativedelta(seconds=1) if leave.date_to == interval[0] else interval[0] benefit_stop = interval[1] - relativedelta(seconds=1) if leave.date_from == interval[1] else interval[1] - self.env['hr.benefit'].safe_duplicate_create({ + vals_list += [{ 'name': "%s: %s" % (benefit_type.name, employee.name), 'date_start': benefit_start, 'date_stop': benefit_stop, @@ -124,7 +128,11 @@ class HrLeave(models.Model): 'contract_id': benefit.contract_id.id, 'employee_id': employee.id, 'state': 'confirmed', - }) + }] + + date_start = min(benefits.mapped('date_start')) + date_stop = max(benefits.mapped('date_stop')) + self.env['hr.benefit']._safe_duplicate_create(vals_list, date_start, date_stop) benefits.unlink() return True return False @@ -132,13 +140,13 @@ class HrLeave(models.Model): @api.multi def action_validate(self): super(HrLeave, self).action_validate() - self._cancel_benefit_conflict() # delete preexisting conflicting benefits + self.sudo()._cancel_benefit_conflict() # delete preexisting conflicting benefits return True @api.multi def action_refuse(self): super(HrLeave, self).action_refuse() - benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)]) + benefits = self.env['hr.benefit'].sudo().search([('leave_id', 'in', self.ids)]) benefits.write({'display_warning': False, 'active': False}) return True @@ -155,7 +163,7 @@ class HrLeave(models.Model): # Use sudo otherwise base users can't compute number of days contracts = employee.sudo()._get_contracts(date_from, date_to, states=['incoming', 'open', 'pending']) calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint - return employee.get_work_days_data(date_from, date_to, calendar=calendar)['days'] + return employee._get_work_days_data(date_from, date_to, calendar=calendar)['days'] return days diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index c3f625e618c..3277d473f47 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -163,7 +163,7 @@ class HrPayslip(models.Model): benefit_types = self.env['hr.benefit.type'].search([('code', '!=', False)]) for benefit_type in benefit_types: - hours = contract.employee_id.get_benefit_days_data(benefit_type, day_from, day_to, calendar=calendar)['hours'] + hours = contract.employee_id._get_benefit_days_data(benefit_type, day_from, day_to, calendar=calendar)['hours'] if hours: line = { 'name': benefit_type.name, diff --git a/addons/hr_payroll/models/resource.py b/addons/hr_payroll/models/resource.py index 4763312c4f5..1dd5e0677a5 100644 --- a/addons/hr_payroll/models/resource.py +++ b/addons/hr_payroll/models/resource.py @@ -1,10 +1,11 @@ # -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields, api from odoo.osv.expression import AND from odoo.tools import float_compare from datetime import datetime +from odoo import models, fields, api +from odoo.addons.resource.models.resource_mixin import timezone_datetime class ResourceCalendar(models.Model): _inherit = 'resource.calendar' @@ -78,7 +79,7 @@ class ResourceCalendarLeave(models.Model): class ResourceMixin(models.AbstractModel): _inherit = "resource.mixin" - def get_benefit_days_data(self, benefit_type, from_datetime, to_datetime, calendar=None): + def _get_benefit_days_data(self, benefit_type, from_datetime, to_datetime, calendar=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. @@ -94,8 +95,8 @@ class ResourceMixin(models.AbstractModel): domain = [('benefit_type_id', 'in', benefit_type_ids)] # naive datetimes are made explicit in UTC - from_datetime = self._timezone_datetime(from_datetime) - to_datetime = self._timezone_datetime(to_datetime) + from_datetime = timezone_datetime(from_datetime) + to_datetime = timezone_datetime(to_datetime) day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) # actual hours per day diff --git a/addons/hr_payroll/security/ir.model.access.csv b/addons/hr_payroll/security/ir.model.access.csv index 751a64b4779..68d8dd8f6d7 100644 --- a/addons/hr_payroll/security/ir.model.access.csv +++ b/addons/hr_payroll/security/ir.model.access.csv @@ -10,5 +10,6 @@ access_hr_payslip_worked_days_officer,hr.payslip.worked_days.officer,model_hr_pa access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_benefit,access_hr_benefit,model_hr_benefit,group_hr_payroll_user,1,1,1,1
-access_hr_benefit_type,access_hr_benefit_type,model_hr_benefit_type,group_hr_payroll_user,1,1,1,1
+access_hr_benefit_type_officer,access_hr_benefit_type_officer,model_hr_benefit_type,group_hr_payroll_user,1,0,0,0
access_hr_benefit_employee,hr.benefit.employee,model_hr_user_benefit_employee,group_hr_payroll_user,1,1,1,1
+access_hr_benefit_type_manager,access_hr_benefit_type_manager,model_hr_benefit_type,group_hr_payroll_manager,1,1,1,1
diff --git a/addons/hr_payroll/static/src/js/benefit.js b/addons/hr_payroll/static/src/js/benefit.js index f403e718a64..f021d29d63f 100644 --- a/addons/hr_payroll/static/src/js/benefit.js +++ b/addons/hr_payroll/static/src/js/benefit.js @@ -3,11 +3,11 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) { var core = require('web.core'); var CalendarController = require("web.CalendarController"); + var time = require('web.time'); var CalendarModel = require('web.CalendarModel'); var CalendarRenderer = require('web.CalendarRenderer'); var CalendarView = require('web.CalendarView'); var viewRegistry = require('web.view_registry'); - var _t = core._t; var BenefitCalendarController = CalendarController.extend({ @@ -35,8 +35,8 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) { return; } - this.firstDay = this.model.data.target_date.clone().startOf('month'); - this.lastDay = this.model.data.target_date.clone().endOf('month'); + this.firstDay = this.model.data.target_date.clone().startOf('month').toDate(); + this.lastDay = this.model.data.target_date.clone().endOf('month').toDate(); this.events = this._checkDataInRange(this.firstDay, this.lastDay, this.model.data.data); var is_validated = this._checkValidation(this.events); this.$buttons.find('.btn-benefit').remove(); @@ -54,11 +54,10 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) { _onGeneratePayslips: function (e) { e.preventDefault(); e.stopImmediatePropagation(); - var date_fmt = 'YYYY-MM-DD'; this.do_action('hr_payroll.action_generate_payslips_from_benefits', { additional_context: { - default_date_start: this.firstDay.format(date_fmt), - default_date_end: this.lastDay.format(date_fmt), + default_date_start: time.datetime_to_str(this.firstDay), + default_date_end: time.datetime_to_str(this.lastDay), }, }); }, @@ -79,12 +78,11 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) { _onGenerateBenefits: function (e) { e.preventDefault(); e.stopImmediatePropagation(); - var date_fmt = 'YYYY-MM-DD HH:mm:ss'; var self = this; this._rpc({ model: 'hr.employee', method: 'generate_benefit', - args: [this.firstDay.format(date_fmt), this.lastDay.format(date_fmt)], + args: [time.datetime_to_str(this.firstDay), time.datetime_to_str(this.lastDay)], }).then(function () { self.reload(); }); diff --git a/addons/hr_payroll/tests/__init__.py b/addons/hr_payroll/tests/__init__.py index 98f0f93e77b..bd5daca4153 100644 --- a/addons/hr_payroll/tests/__init__.py +++ b/addons/hr_payroll/tests/__init__.py @@ -5,3 +5,4 @@ from . import test_payslip_flow from . import test_benefit from . import test_multi_contract from . import test_calendar_sync +from . import test_leave diff --git a/addons/hr_payroll/tests/test_benefit.py b/addons/hr_payroll/tests/test_benefit.py index 92a5d1daa6b..083a9c287ac 100644 --- a/addons/hr_payroll/tests/test_benefit.py +++ b/addons/hr_payroll/tests/test_benefit.py @@ -5,6 +5,7 @@ from datetime import datetime import pytz from dateutil.relativedelta import relativedelta from odoo import exceptions +from odoo.fields import Datetime, Date from odoo.tests.common import tagged from odoo.addons.hr_payroll.tests.common import TestPayslipBase @@ -15,8 +16,8 @@ class TestBenefit(TestPayslipBase): def setUp(self): super(TestBenefit, self).setUp() self.tz = pytz.timezone(self.richard_emp.tz) - self.start = datetime.strptime('2015-11-01 01:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - self.end = datetime.strptime('2015-11-30 23:59:59', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + self.start = self.to_datetime_tz('2015-11-01 01:00:00') + self.end = self.to_datetime_tz('2015-11-30 23:59:59') self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Zboub'}) contract = self.env['hr.contract'].create({ 'date_start': self.start - relativedelta(days=5), @@ -35,6 +36,24 @@ class TestBenefit(TestPayslipBase): 'code': 'WORK200' }) + def to_datetime_tz(self, datetime_str, tz=None): + tz = tz or self.tz + return tz.localize(Datetime.to_datetime(datetime_str)) + + def assertDatetimeTzEqual(self, value, target, tz=None): + """ + Assert equality between two dates. + :param value: timezone naive datetime + :param target: datetime string + :param tz: timezone to interpret the tartget string + :raises AssertionError: raises exception if the two dates are not equal + """ + tz = tz or self.tz + self.assertEqual( + pytz.utc.localize(value).astimezone(tz), + self.to_datetime_tz(target, tz=tz) + ) + def test_no_duplicate(self): self.richard_emp.generate_benefit(self.start, self.end) pou1 = self.env['hr.benefit'].search_count([]) @@ -51,8 +70,8 @@ class TestBenefit(TestPayslipBase): self.assertEqual(attendance_nb, benefit_nb, "One benefit should be generated for each calendar attendance") def test_split_benefit_by_day(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 18:00:00') # Benefit of type attendance should be split in three benefit = self.env['hr.benefit'].create({ @@ -63,20 +82,21 @@ class TestBenefit(TestPayslipBase): 'date_stop': end, }) - benefits = benefit.split_by_day() + benefits = benefit._split_by_day() self.assertEqual(len(benefits), 3, "Benefit should be split in three") - self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2015-11-01 23:59:59', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2015-11-02 00:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2015-11-02 23:59:59', '%Y-%m-%d %H:%M:%S'))) + self.assertDatetimeTzEqual(benefits[0].date_start, '2015-11-01 09:00:00') + self.assertDatetimeTzEqual(benefits[0].date_stop, '2015-11-01 23:59:59') + + self.assertDatetimeTzEqual(benefits[1].date_start, '2015-11-02 00:00:00') + self.assertDatetimeTzEqual(benefits[1].date_stop, '2015-11-02 23:59:59') - self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2015-11-03 00:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertDatetimeTzEqual(benefits[2].date_start, '2015-11-03 00:00:00') + self.assertDatetimeTzEqual(benefits[2].date_stop, '2015-11-03 18:00:00') # Test with end at mid-night -> should not create benefit starting and ending at the same time (at 00:00) - start = datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2013-11-04 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2013-11-01 00:00:00') + end = self.to_datetime_tz('2013-11-04 00:00:00') benefit = self.env['hr.benefit'].create({ 'name': '1', @@ -85,21 +105,21 @@ class TestBenefit(TestPayslipBase): 'date_start': start, 'date_stop': end, }) - benefits = benefit.split_by_day() + benefits = benefit._split_by_day() self.assertEqual(len(benefits), 3, "Benefit should be split in three") - self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2013-11-01 23:59:59', '%Y-%m-%d %H:%M:%S'))) + self.assertDatetimeTzEqual(benefits[0].date_start, '2013-11-01 00:00:00') + self.assertDatetimeTzEqual(benefits[0].date_stop, '2013-11-01 23:59:59') - self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2013-11-02 00:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2013-11-02 23:59:59', '%Y-%m-%d %H:%M:%S'))) + self.assertDatetimeTzEqual(benefits[1].date_start, '2013-11-02 00:00:00') + self.assertDatetimeTzEqual(benefits[1].date_stop, '2013-11-02 23:59:59') - self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2013-11-03 00:00:00', '%Y-%m-%d %H:%M:%S'))) - self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2013-11-03 23:59:59', '%Y-%m-%d %H:%M:%S'))) + self.assertDatetimeTzEqual(benefits[2].date_start, '2013-11-03 00:00:00') + self.assertDatetimeTzEqual(benefits[2].date_stop, '2013-11-03 23:59:59') def test_approve_multiple_day_benefit(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 18:00:00') # Benefit of type attendance should be split in three benefit = self.env['hr.benefit'].create({ @@ -116,8 +136,8 @@ class TestBenefit(TestPayslipBase): self.assertEqual(len(benefits), 3, "Benefit should be split in three") def test_duplicate_global_benefit_to_attendance(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 18:00:00') benef = self.env['hr.benefit'].create({ 'name': '1', @@ -135,8 +155,8 @@ class TestBenefit(TestPayslipBase): self.assertEqual(attendance_nb, 0, "It should not duplicate the 'normal/global' benefit type") def test_duplicate_benefit_to_attendance(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 18:00:00') # Benefit (not leave) should be split in three attendance benef = self.env['hr.benefit'].create({ @@ -154,27 +174,27 @@ class TestBenefit(TestPayslipBase): ]) self.assertEqual(attendance_nb, 3, "It should create one calendar attendance per day") self.assertTrue(self.env['resource.calendar.attendance'].search([ - ('date_from', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()), - ('date_to', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()), + ('date_from', '=', Date.to_date('2015-11-01')), + ('date_to', '=', Date.to_date('2015-11-01')), ('hour_from', '=', 9.0), ('hour_to', '>=', 23.9) ])) self.assertTrue(self.env['resource.calendar.attendance'].search([ - ('date_from', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()), - ('date_to', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()), + ('date_from', '=', Date.to_date('2015-11-02')), + ('date_to', '=', Date.to_date('2015-11-02')), ('hour_from', '=', 0.0), ('hour_to', '>=', 23.9) ])) self.assertTrue(self.env['resource.calendar.attendance'].search([ - ('date_from', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()), - ('date_to', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()), + ('date_from', '=', Date.to_date('2015-11-03')), + ('date_to', '=', Date.to_date('2015-11-03')), ('hour_from', '=', 0.0), ('hour_to', '=', 18.0) ])) def test_create_benefit_leave(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 18:00:00') benef = self.env['hr.benefit'].create({ 'name': 'Richard leave from benef', @@ -190,8 +210,8 @@ class TestBenefit(TestPayslipBase): self.assertEqual(calendar_leave.benefit_type_id, benef.benefit_type_id, "It should have the same benefit type") def test_validate_conflict_benefit(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-01 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-01 13:00:00') benef1 = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, @@ -243,8 +263,8 @@ class TestBenefit(TestPayslipBase): self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits without a type") def test_approve_leave_benefit(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 13:00:00') leave = self.env['hr.leave'].create({ 'name': 'Doctor Appointment', 'employee_id': self.richard_emp.id, @@ -265,13 +285,13 @@ class TestBenefit(TestPayslipBase): leave.action_approve() new_leave_benef = self.env['hr.benefit'].search([ - ('date_start', '=', datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S')), - ('date_stop', '=', datetime.strptime('2015-11-02 09:00:00', '%Y-%m-%d %H:%M:%S')), + ('date_start', '=', Datetime.to_datetime('2015-11-01 09:00:00')), + ('date_stop', '=', Datetime.to_datetime('2015-11-02 09:00:00')), ('benefit_type_id.is_leave', '=', True) ]) new_benef = self.env['hr.benefit'].search([ - ('date_start', '=', datetime.strptime('2015-11-02 09:00:01', '%Y-%m-%d %H:%M:%S')), + ('date_start', '=', Datetime.to_datetime('2015-11-02 09:00:01')), ('date_stop', '=', end), ('benefit_type_id.is_leave', '=', False) ]) @@ -279,11 +299,11 @@ class TestBenefit(TestPayslipBase): self.assertTrue(new_benef, "It should have created a benefit for the last two days") self.assertTrue(new_leave_benef, "It should have created a leave benefit for the first day") - self.assertTrue(benef.action_validate((new_benef|new_leave_benef).ids), "It should be able to validate the benefits") + self.assertTrue(benef.action_validate((new_benef | new_leave_benef).ids), "It should be able to validate the benefits") def test_refuse_leave_benefit(self): - start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 09:00:00') + end = self.to_datetime_tz('2015-11-03 13:00:00') leave = self.env['hr.leave'].create({ 'name': 'Doctor Appointment', 'employee_id': self.richard_emp.id, @@ -308,12 +328,12 @@ class TestBenefit(TestPayslipBase): def test_time_normal_benefit(self): # Normal attendances (global to all employees) - data = self.richard_emp.get_benefit_days_data(self.env.ref('hr_payroll.benefit_type_attendance'), self.start, self.end) + data = self.richard_emp._get_benefit_days_data(self.env.ref('hr_payroll.benefit_type_attendance'), self.start, self.end) self.assertEqual(data['hours'], 168.0) def test_time_extra_benefit(self): - start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) - end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + start = self.to_datetime_tz('2015-11-01 10:00:00') + end = self.to_datetime_tz('2015-11-01 17:00:00') benef = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, @@ -323,13 +343,13 @@ class TestBenefit(TestPayslipBase): 'date_stop': end, }) benef.action_validate(benef.ids) - data = self.richard_emp.get_benefit_days_data(self.benefit_type, self.start, self.end) + data = self.richard_emp._get_benefit_days_data(self.benefit_type, self.start, self.end) self.assertEqual(data['hours'], 7.0) def test_time_week_leave_benefit(self): # /!\ this is a week day => it exists an calendar attendance at this time - start = datetime.strptime('2015-11-02 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC - end = datetime.strptime('2015-11-02 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + start = self.to_datetime_tz('2015-11-02 10:00:00', tz=pytz.utc) + end = self.to_datetime_tz('2015-11-02 17:00:00', tz=pytz.utc) leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, @@ -339,13 +359,13 @@ class TestBenefit(TestPayslipBase): 'date_stop': end, }) leave_benef.action_validate(leave_benef.ids) - data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end) + data = self.richard_emp._get_benefit_days_data(self.benefit_type_leave, self.start, self.end) self.assertEqual(data['hours'], 5.0, "It should equal the number of hours richard should have worked") def test_time_weekend_leave_benefit(self): # /!\ this is in the weekend => no calendar attendance at this time - start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC - end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + start = self.to_datetime_tz('2015-11-01 10:00:00', tz=pytz.utc) + end = self.to_datetime_tz('2015-11-01 17:00:00', tz=pytz.utc) leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, @@ -355,13 +375,13 @@ class TestBenefit(TestPayslipBase): 'date_stop': end, }) leave_benef.action_validate(leave_benef.ids) - data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end) + data = self.richard_emp._get_benefit_days_data(self.benefit_type_leave, self.start, self.end) self.assertEqual(data['hours'], 0.0, "It should equal the number of hours richard should have worked") def test_payslip_generation_with_leave(self): # /!\ this is a week day => it exists an calendar attendance at this time - start = datetime.strptime('2015-11-02 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC - end = datetime.strptime('2015-11-02 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + start = self.to_datetime_tz('2015-11-02 10:00:00', tz=pytz.utc) + end = self.to_datetime_tz('2015-11-02 17:00:00', tz=pytz.utc) leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, @@ -372,9 +392,9 @@ class TestBenefit(TestPayslipBase): }) leave_benef.action_validate(leave_benef.ids) payslip_wizard = self.env['hr.payslip.employees'].create({'employee_ids': [(4, self.richard_emp.id)]}) - payslip_wizard.with_context({'default_date_start': start.strftime('%Y-%m-%d'),'default_date_end': end.strftime('%Y-%m-%d')}).compute_sheet() + payslip_wizard.with_context({'default_date_start': Date.to_string(start), 'default_date_end': Date.to_string(end)}).compute_sheet() payslip = self.env['hr.payslip'].search([('employee_id', '=', self.richard_emp.id)]) - work_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100') # From default calendar.attendance + work_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100') # From default calendar.attendance leave_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE100') self.assertTrue(work_line, "It should have a work line in the payslip") @@ -384,8 +404,8 @@ class TestBenefit(TestPayslipBase): def test_payslip_generation_with_extra_work(self): # /!\ this is in the weekend (Sunday) => no calendar attendance at this time - start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC - end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + start = self.to_datetime_tz('2015-11-01 10:00:00', tz=pytz.utc) + end = self.to_datetime_tz('2015-11-01 17:00:00', tz=pytz.utc) benef = self.env['hr.benefit'].create({ 'name': 'Extra', 'employee_id': self.richard_emp.id, @@ -397,8 +417,8 @@ class TestBenefit(TestPayslipBase): benef.action_validate(benef.ids) payslip_wizard = self.env['hr.payslip.employees'].create({'employee_ids': [(4, self.richard_emp.id)]}) payslip_wizard.with_context({ - 'default_date_start': start.strftime('%Y-%m-%d'), - 'default_date_end': (end + relativedelta(days=1)).strftime('%Y-%m-%d') + 'default_date_start': Date.to_string(start), + 'default_date_end': Date.to_string(end + relativedelta(days=1)) }).compute_sheet() payslip = self.env['hr.payslip'].search([('employee_id', '=', self.richard_emp.id)]) work_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100') # From default calendar.attendance diff --git a/addons/hr_payroll/tests/test_leave.py b/addons/hr_payroll/tests/test_leave.py new file mode 100644 index 00000000000..1168d05fee0 --- /dev/null +++ b/addons/hr_payroll/tests/test_leave.py @@ -0,0 +1,53 @@ +# # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +import pytz +from dateutil.relativedelta import relativedelta +from odoo import exceptions +from odoo.tests.common import tagged +from odoo.addons.hr_payroll.tests.common import TestPayslipBase + + +class TestPayrollLeave(TestPayslipBase): + + def create_leave(self): + return self.env['hr.leave'].create({ + 'name': 'Holiday !!!', + 'employee_id': self.richard_emp.id, + 'holiday_status_id': self.leave_type.id, + 'date_to': datetime.today() + relativedelta(days=1), + 'date_from': datetime.today(), + 'number_of_days': 1, + }) + + def test_resource_leave_has_benefit_type(self): + leave = self.create_leave() + + resource_leave = leave._create_resource_leave() + self.assertEqual(resource_leave.benefit_type_id, self.leave_type.benefit_type_id, "it should have the corresponding benefit type") + + def test_resource_leave_in_contract_calendar(self): + other_calendar = self.env['resource.calendar'].create({'name': 'New calendar'}) + contract = self.richard_emp.contract_ids[0] + contract.resource_calendar_id = other_calendar + contract.state = 'open' # this set richard's calendar to New calendar + leave = self.create_leave() + + resource_leave = leave._create_resource_leave() + self.assertEqual(len(resource_leave), 1, "it should have created only one resource leave") + self.assertEqual(resource_leave.benefit_type_id, self.leave_type.benefit_type_id, "it should have the corresponding benefit type") + + def test_resource_leave_different_calendars(self): + other_calendar = self.env['resource.calendar'].create({'name': 'New calendar'}) + contract = self.richard_emp.contract_ids[0] + contract.resource_calendar_id = other_calendar + contract.state = 'open' # this set richard's calendar to New calendar + + # set another calendar + self.richard_emp.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Other calendar'}) + + leave = self.create_leave() + resource_leave = leave._create_resource_leave() + self.assertEqual(len(resource_leave), 2, "it should have created one resource leave per calendar") + self.assertEqual(resource_leave.mapped('benefit_type_id'), self.leave_type.benefit_type_id, "they should have the corresponding benefit type") diff --git a/addons/hr_payroll/views/hr_benefit_views.xml b/addons/hr_payroll/views/hr_benefit_views.xml index c6fd9023e5b..b5d9b018dea 100644 --- a/addons/hr_payroll/views/hr_benefit_views.xml +++ b/addons/hr_payroll/views/hr_benefit_views.xml @@ -53,8 +53,7 @@ <span class="fa fa-exclamation-triangle mr-2" aria-label="Conflict" title="error"/> </t> <t t-if="record.state == 'validated'"> - <span class="fa fa-check-circle-o" aria-label="Validated" title="validated"/> - <t t-esc="' '"/> + <span class="fa fa-check-circle-o mr-2" aria-label="Validated" title="validated"/> </t> <t t-foreach="widget.displayFields" t-as="name"> <t t-esc="format(record, name)"/> @@ -75,7 +74,7 @@ <header> <field name="display_warning" invisible="1"/> <field name="leave_id" invisible="1"/> - <button string="Go To Conflicting Leave" name="action_leave" type="object" attrs="{'invisible': ['|', '|', '!',('display_warning', '=', True), '!', ('leave_id', '!=', False), ('benefit_type_id', '=', False)]}"/> + <button string="Go To Conflicting Leave" name="action_leave" type="object" attrs="{'invisible': ['|', '|', '!',('display_warning', '=', True), ('leave_id', '=', False), ('benefit_type_id', '=', False)]}"/> <field name="state" widget="statusbar" options="{'clickable': '1'}"/> </header> <sheet> diff --git a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py index a41147c68fc..73807b81845 100644 --- a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py +++ b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime from dateutil import relativedelta -from odoo import api, fields, models +from odoo.fields import Date +from odoo import api, models class PayslipLinesContributionRegister(models.TransientModel): _name = 'payslip.lines.contribution.register' _description = 'Payslip Lines by Contribution Registers' - date_from = fields.Date(string='Date From', required=True, - default=datetime.now().strftime('%Y-%m-01')) - date_to = fields.Date(string='Date To', required=True, - default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10]) + date_from = Date(string='Date From', required=True, + default=Date.today().replace(day=1)) + date_to = Date(string='Date To', required=True, + default=Date.today() + relativedelta.relativedelta(months=+1, day=1, days=-1)) @api.multi def print_report(self): diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py index 6cf9ed9301c..00ec37eb0cd 100644 --- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py +++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py @@ -3,7 +3,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError -from datetime import datetime, date class HrPayslipEmployees(models.TransientModel): @@ -24,8 +23,8 @@ class HrPayslipEmployees(models.TransientModel): def compute_sheet(self): self.ensure_one() if not self.env.context.get('active_id'): - from_date = datetime.strptime(self.env.context.get('default_date_start'), '%Y-%m-%d') - end_date = datetime.strptime(self.env.context.get('default_date_end'), '%Y-%m-%d') + from_date = fields.Date.to_date(self.env.context.get('default_date_start')) + end_date = fields.Date.to_date(self.env.context.get('default_date_end')) payslip_run = self.env['hr.payslip.run'].create({ 'name': from_date.strftime('%B %Y'), 'date_start': from_date, diff --git a/addons/hr_payroll_account/models/hr_payroll_account.py b/addons/hr_payroll_account/models/hr_payroll_account.py index 9263b8f69e3..2c992ae06e2 100644 --- a/addons/hr_payroll_account/models/hr_payroll_account.py +++ b/addons/hr_payroll_account/models/hr_payroll_account.py @@ -54,13 +54,13 @@ class HrPayslip(models.Model): @api.multi def action_payslip_done(self): res = super(HrPayslip, self).action_payslip_done() - precision = self.env['decimal.precision'].precision_get('Payroll') for slip in self: line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip.date or slip.date_to + currency = slip.company_id.currency_id name = _('Payslip of %s') % (slip.employee_id.name) move_dict = { @@ -70,8 +70,8 @@ class HrPayslip(models.Model): 'date': date, } for line in slip.line_ids.filtered(lambda line: line.category_id): - amount = slip.credit_note and -line.total or line.total - if float_is_zero(amount, precision_digits=precision): + amount = currency.round(slip.credit_note and -line.total or line.total) + if currency.is_zero(amount): continue debit_account_id = line.salary_rule_id.account_debit.id credit_account_id = line.salary_rule_id.account_credit.id @@ -106,7 +106,7 @@ class HrPayslip(models.Model): line_ids.append(credit_line) credit_sum += credit_line[2]['credit'] - credit_line[2]['debit'] - if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: + if currency.compare_amounts(credit_sum, debit_sum) == -1: acc_id = slip.journal_id.default_credit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name)) @@ -117,11 +117,11 @@ class HrPayslip(models.Model): 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, - 'credit': debit_sum - credit_sum, + 'credit': currency.round(debit_sum - credit_sum), }) line_ids.append(adjust_credit) - elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: + elif currency.compare_amounts(debit_sum, credit_sum) == -1: acc_id = slip.journal_id.default_debit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (slip.journal_id.name)) @@ -131,7 +131,7 @@ class HrPayslip(models.Model): 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, - 'debit': credit_sum - debit_sum, + 'debit': currency.round(credit_sum - debit_sum), 'credit': 0.0, }) line_ids.append(adjust_debit) diff --git a/addons/hr_skills/static/src/css/hr_skills.scss b/addons/hr_skills/static/src/css/hr_skills.scss index 12a7496bd84..cb1a7072922 100644 --- a/addons/hr_skills/static/src/css/hr_skills.scss +++ b/addons/hr_skills/static/src/css/hr_skills.scss @@ -11,6 +11,10 @@ background-color: #e9ecef; } + td.o_skill_cell { + width: 33%; + } + .o_group_resume tbody tr.o_resume_group_header th { font-weight: normal; } diff --git a/addons/hr_skills/static/src/xml/skills_templates.xml b/addons/hr_skills/static/src/xml/skills_templates.xml index 90e29117630..b6d89e04a3d 100644 --- a/addons/hr_skills/static/src/xml/skills_templates.xml +++ b/addons/hr_skills/static/src/xml/skills_templates.xml @@ -4,10 +4,10 @@ <t t-name="hr_skill_data_row"> <tr class="o_data_row" t-att-data-id="id"> - <td class="o_data_cell"> + <td class="o_data_cell o_skill_cell"> <t t-esc="data.skill_id.data.display_name"/> </td> - <td class="o_data_cell"> + <td class="o_data_cell o_skill_cell"> <t t-esc="data.skill_level_id.data.display_name"/> </td> </tr> diff --git a/addons/hr_skills/views/hr_views.xml b/addons/hr_skills/views/hr_views.xml index b85d4f4d0e4..e5cc42bfc4e 100644 --- a/addons/hr_skills/views/hr_views.xml +++ b/addons/hr_skills/views/hr_views.xml @@ -49,7 +49,7 @@ <!-- This field uses a custom tree view rendered by the 'hr_resume' widget. Adding fields in the tree arch below makes them accessible to the widget --> - <field class="" nolabel="1" name="resume_line_ids" widget="hr_resume"> + <field mode="tree" nolabel="1" name="resume_line_ids" widget="hr_resume"> <tree> <field name="line_type_id"/> <field name="name"/> @@ -62,7 +62,7 @@ </div> <div class="col-lg-4 o_group_skills"> <separator string="Skills"/> - <field nolabel="1" name="employee_skill_ids" widget="hr_skills"> + <field mode="tree" nolabel="1" name="employee_skill_ids" widget="hr_skills"> <tree> <field name="skill_type_id" invisible="1"/> <field name="skill_id"/> @@ -89,7 +89,7 @@ <!-- This field uses a custom tree view rendered by the 'hr_resume' widget. Adding fields in the tree arch below makes them accessible to the widget --> - <field class="" nolabel="1" name="resume_line_ids" widget="hr_resume"> + <field mode="tree" nolabel="1" name="resume_line_ids" widget="hr_resume"> <tree> <field name="line_type_id"/> <field name="name"/> @@ -102,7 +102,7 @@ </div> <div class="col-lg-4 o_group_skills"> <separator string="Skills"/> - <field nolabel="1" name="employee_skill_ids" widget="hr_skills"> + <field mode="tree" nolabel="1" name="employee_skill_ids" widget="hr_skills"> <tree> <field name="skill_type_id" invisible="1"/> <field name="skill_id"/> diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 42a229ec7bb..df57a6c770e 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -166,6 +166,7 @@ class ImLivechatChannel(models.Model): 'public': 'private', 'email_send': False, }) + mail_channel._broadcast([operator_partner_id]) return mail_channel.sudo().channel_info()[0] @api.model diff --git a/addons/l10n_ch/i18n_extra/de.po b/addons/l10n_ch/i18n_extra/de.po index acfab8ce872..278f2bf8a0f 100644 --- a/addons/l10n_ch/i18n_extra/de.po +++ b/addons/l10n_ch/i18n_extra/de.po @@ -4,26 +4,25 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0c\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" -"PO-Revision-Date: 2016-12-15 14:35+0100\n" -"Last-Translator: <didier.donze@camptocamp.com>\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" +"PO-Revision-Date: 2019-02-12 12:06+0530\n" +"Last-Translator: didier.donze@camptocamp.com\n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" +"X-Generator: Poedit 2.2.1\n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "0% Exkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "0% Import." @@ -34,118 +33,105 @@ msgid "100% dédouanement TVA" msgstr "100% Verzollung MwSt." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "100% Imp." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "2.5%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "2.5% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "2.5% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "2.5% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "2.5% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "2.5% Invest. Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" -msgstr "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" +msgstr "3.7%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." -msgstr "3.8% Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." +msgstr "3.7% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" -msgstr "3.8% Einkauf" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" +msgstr "3.7% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." -msgstr "3.8% Einkauf Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." +msgstr "3.7% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" -msgstr "3.8% Invest." +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" +msgstr "3.7% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." -msgstr "3.8% Invest. Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." +msgstr "3.7% Invest. Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" -msgstr "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" +msgstr "7.7%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." -msgstr "8.0% Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." +msgstr "7.7% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" -msgstr "8.0% Einkauf" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" +msgstr "7.7% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." -msgstr "8.0% Einkauf Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." +msgstr "7.7% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." -msgstr "8.0% Invest." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "7.7% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." -msgstr "8.0% Invest. Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." +msgstr "7.7% Invest. Inkl." #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4200 @@ -255,7 +241,8 @@ msgstr "Abschreibungen auf Fahrzeuge" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" -msgstr "Abschreibungen und Wertberichtigungen feste Einrichtungen und Installationen" +msgstr "" +"Abschreibungen und Wertberichtigungen feste Einrichtungen und Installationen" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2270 @@ -313,37 +300,6 @@ msgid "Avances et prêts" msgstr "Vorschüsse und Darlehen" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "Bank" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "Bankkonten" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "" -"Boolean option field indicating whether or not the alternate layout (the one " -"printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "" -"Boolean value telling whether or not the ISR corresponding to this invoice " -"has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "" -"Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "Patente, Know-how, Lizenzen, Rechte, Entwicklungen" @@ -359,11 +315,6 @@ msgid "Bénéfice / perte reporté" msgstr "Gewinnvortrag oder Verlustvortrag" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "CHF ESR Referenznummer" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "" "Capital actions, capital social, droits de participations ou capital de " @@ -521,11 +472,6 @@ msgid "Créanciers" msgstr "Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "Währung" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" @@ -578,11 +524,6 @@ msgid "Déductions sur ventes" msgstr "Erlösminderungen" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "Wasser" @@ -598,11 +539,6 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "Darlehen" @@ -677,34 +613,6 @@ msgid "Hypothèques" msgstr "Hypotheken" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "" -"ISR number split in blocks of 5 characters (right-justified), to generate " -"ISR report." -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "Geschäftsliegenschaften" @@ -752,51 +660,6 @@ msgid "Installations de stockage" msgstr "Installations de stockage" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "Fahrzeugleasing und -mieten" @@ -807,6 +670,11 @@ msgid "Leasing immobilisations corporelles meubles" msgstr "Leasingaufwand mobile Sachanlagen" #. module: l10n_ch +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "Büromaschinen, Informatik, Kommunikationstechnologie" @@ -852,11 +720,6 @@ msgid "Mobilier et installations" msgstr "Mobiliar und Einrichtungen" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "Werkzeuge und Geräte" @@ -882,13 +745,6 @@ msgid "Plan comptable 2015" msgstr "Kontenrahmen 2015" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "" -"Postal reference of the bank, formated with '-' and without the padding " -"zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "Aufwand für bezogene Dienstleistungen" @@ -899,30 +755,6 @@ msgid "Prestations propres" msgstr "Eigenleistungen" #. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "" -"Print the coordinates of your bank under the 'Payment for' title of the " -"ISR.\n" -" Your address will be moved to the 'in favour " -"of' section." -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "Ertrag Nebenbetrieb" @@ -1035,11 +867,6 @@ msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "CH Bilanz- und Erfolgsrechnungen" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1080,13 +907,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1100,13 +927,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1120,6 +947,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1145,6 +982,11 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "MwSt. 0%" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "MwSt. 0% auf Importe von Gütern und Dienstleistungen" @@ -1155,6 +997,11 @@ msgid "TVA 0% exclue" msgstr "MwSt. 0% ausgeschlossen" #. module: l10n_ch +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "MwSt. 2.5%" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "MwSt. 2,5 % auf Einkäufe B&S (inkl. USt)" @@ -1175,44 +1022,59 @@ msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "MwSt. 2,5% auf Invest. und sonst. Aufw. (USt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" -msgstr "MwSt. 3,8% auf Einkäufe B&S (inkl. LSt)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" +msgstr "MwSt. 3,7%" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" +msgstr "MwSt. 3,7% auf Einkäufe B&S (inkl. LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "MwSt. 3,8% auf Einkäufe B&S (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" +msgstr "MwSt. 3,7% auf Einkäufe B&S (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" -msgstr "MwSt. 3,8% auf Invest. und sonst. Aufw. (inkl. LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" +msgstr "MwSt. 3,7% auf Invest. und sonst. Aufw. (inkl. LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" -msgstr "MwSt. 3,8% auf Invest. und sonst. Aufw. (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" +msgstr "MwSt. 3,7% auf Invest. und sonst. Aufw. (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" -msgstr "MwSt. 8.0% auf Einkäufe B&S (inkl. NRZ)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" +msgstr "MwSt. 7,7%" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" -msgstr "MwSt. 8,0% auf Einkäufe B&S (NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" +msgstr "MwSt. 7,7% auf Einkäufe B&S (inkl. NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" -msgstr "MwSt. 8,0% auf Invest. und sonst. Aufw. (inkl. NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" +msgstr "MwSt. 7,7% auf Einkäufe B&S (NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" -msgstr "MwSt. 8,0% auf Invest. und sonst. Aufw. (NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" +msgstr "MwSt. 7,7% auf Invest. und sonst. Aufw. (inkl. NRZ)" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" +msgstr "MwSt. 7,7% auf Invest. und sonst. Aufw. (NRZ)" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2200 @@ -1230,61 +1092,34 @@ msgid "TVA due a 2.5% (TR)" msgstr "Zu zahlende MwSt. bei 2,5% (USt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" -msgstr "Zu zahlende MwSt. bei 3,8% (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" +msgstr "Zu zahlende MwSt. bei 3,7% (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "Zu zahlende MwSt. bei 8,0% (NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" -msgstr "Zu zahlende MwSt. bei 2,5% (inkl. USt)" - -#. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" -msgstr "Zu zahlende MwSt. bei 3,8% (inkl. LSt)" - -#. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" -msgstr "Zu zahlende MwSt. bei 8,0% (inkl. NRZ)" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in CHF." -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" +msgstr "Zu zahlende MwSt. bei 2,5% (inkl. USt)" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in EUR." -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" +msgstr "Zu zahlende MwSt. bei 3,7% (inkl. LSt)" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" +msgstr "Zu zahlende MwSt. bei 7,7% (inkl. NRZ)" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1060 @@ -1297,11 +1132,6 @@ msgid "Titres à long terme" msgstr "Wertschriften" #. module: l10n_ch -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "Interne Verrechnungen" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "Nicht fakturierte Dienstleistungen" @@ -1377,30 +1207,3 @@ msgstr "Produktionserlöse" #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "Fahrzeuge" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "" -"You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or " -"an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal " -"reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the " -"invoice with the postal to be used to receive the related payment. A default " -"account will be automatically set for all invoices created after you defined " -"a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" diff --git a/addons/l10n_ch/i18n_extra/it.po b/addons/l10n_ch/i18n_extra/it.po index 820569f207b..2af6ef38dc0 100644 --- a/addons/l10n_ch/i18n_extra/it.po +++ b/addons/l10n_ch/i18n_extra/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" "PO-Revision-Date: 2014-12-02 09:47+0000\n" "Last-Translator: <>\n" "Language-Team: \n" @@ -17,201 +17,173 @@ msgstr "" "Plural-Forms: \n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_100_import #: model:account.tax.template,name:l10n_ch.vat_100_import msgid "100% dédouanement TVA" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4200 #: model:account.account.template,name:l10n_ch.ch_coa_4200 msgid "Achats de marchandises destinées à la revente" msgstr "Costi delle merci di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2030 #: model:account.account.template,name:l10n_ch.ch_coa_2030 msgid "Acomptes de clients" msgstr "Acconti ricevuti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1208 #: model:account.account.template,name:l10n_ch.ch_coa_1208 msgid "Acomptes sur les marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1218 #: model:account.account.template,name:l10n_ch.ch_coa_1218 msgid "Acomptes sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1149 #: model:account.account.template,name:l10n_ch.ch_coa_1149 msgid "Ajustement de la valeur des avances et des prêts" msgstr "Rettifica valore anticipi e prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1199 #: model:account.account.template,name:l10n_ch.ch_coa_1199 msgid "Ajustement de la valeur des créances à court terme" msgstr "Rettifica valore crediti diversi a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1449 #: model:account.account.template,name:l10n_ch.ch_coa_1449 msgid "Ajustement de la valeur des créances à long terme" msgstr "Rettifica valore crediti a lungo termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1779 #: model:account.account.template,name:l10n_ch.ch_coa_1779 msgid "Ajustement de la valeur des goodwill" msgstr "Rettifica valore goodwill" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1489 #: model:account.account.template,name:l10n_ch.ch_coa_1489 msgid "Ajustement de la valeur des participations" msgstr "Rettifica valore partecipazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1069 -#: model:account.account,name:l10n_ch.1_ch_coa_1409 #: model:account.account.template,name:l10n_ch.ch_coa_1069 #: model:account.account.template,name:l10n_ch.ch_coa_1409 msgid "Ajustement de la valeur des titres" msgstr "Rettifica valore titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6800 #: model:account.account.template,name:l10n_ch.ch_coa_6800 msgid "" "Amortissements et ajustements de valeur des postes sur immobilisations " @@ -219,187 +191,126 @@ msgid "" msgstr "Ammortamenti e rettifiche di valore dell’attivo fisso" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1519 #: model:account.account.template,name:l10n_ch.ch_coa_1519 msgid "Amortissements sur le mobilier et les installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1599 #: model:account.account.template,name:l10n_ch.ch_coa_1599 msgid "Amortissements sur les autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1709 #: model:account.account.template,name:l10n_ch.ch_coa_1709 msgid "Amortissements sur les brevets, know-how, licences, droits, dév." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1609 #: model:account.account.template,name:l10n_ch.ch_coa_1609 msgid "Amortissements sur les immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1559 #: model:account.account.template,name:l10n_ch.ch_coa_1559 msgid "Amortissements sur les installations de stockage" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1529 #: model:account.account.template,name:l10n_ch.ch_coa_1529 msgid "Amortissements sur les machines de bureau, inf. et syst. comm." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1509 #: model:account.account.template,name:l10n_ch.ch_coa_1509 msgid "Amortissements sur les machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1549 #: model:account.account.template,name:l10n_ch.ch_coa_1549 msgid "Amortissements sur les outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1539 #: model:account.account.template,name:l10n_ch.ch_coa_1539 msgid "Amortissements sur les véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1579 #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2270 #: model:account.account.template,name:l10n_ch.ch_coa_2270 msgid "Assurances sociales et institutions de prévoyance" msgstr "Assicurazioni sociali e istituti di previdenza" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6300 #: model:account.account.template,name:l10n_ch.ch_coa_6300 msgid "Assurances-choses, droits, taxes, autorisations" msgstr "Assicurazioni cose, contributi, tasse e autorizzazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5800 #: model:account.account.template,name:l10n_ch.ch_coa_5800 msgid "Autres charges du personnel" msgstr "Altri costi del personale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6700 #: model:account.account.template,name:l10n_ch.ch_coa_6700 msgid "Autres charges d‘exploitation" msgstr "Altri costi d’esercizio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1190 #: model:account.account.template,name:l10n_ch.ch_coa_1190 msgid "Autres créances à court terme" msgstr "Altri crediti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2210 #: model:account.account.template,name:l10n_ch.ch_coa_2210 msgid "Autres dettes à court terme" msgstr "Altri debiti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2140 #: model:account.account.template,name:l10n_ch.ch_coa_2140 msgid "Autres dettes à court terme rémunérées" msgstr "Altri debiti onerosi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2500 #: model:account.account.template,name:l10n_ch.ch_coa_2500 msgid "Autres dettes à long terme" msgstr "Altri debiti a lungo termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1590 #: model:account.account.template,name:l10n_ch.ch_coa_1590 msgid "Autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3600 #: model:account.account.template,name:l10n_ch.ch_coa_3600 msgid "Autres ventes et prestations de services" msgstr "Altri ricavi e prestazioni di servizi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1140 #: model:account.account.template,name:l10n_ch.ch_coa_1140 msgid "Avances et prêts" msgstr "Anticipi e prestiti" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "" -"Boolean option field indicating whether or not the alternate layout (the one " -"printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "" -"Boolean value telling whether or not the ISR corresponding to this invoice " -"has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "" -"Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1700 #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "Patenti, know-how, licenze, diritti e sviluppo" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2979 #: model:account.account.template,name:l10n_ch.ch_coa_2979 msgid "Bénéfice / perte de l’exercice" msgstr "Utile/perdita annuale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2970 #: model:account.account.template,name:l10n_ch.ch_coa_2970 msgid "Bénéfice / perte reporté" msgstr "Utile / perdita riportata" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1850 #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "" "Capital actions, capital social, droits de participations ou capital de " @@ -409,7 +320,6 @@ msgstr "" "della fondazione non versati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2800 #: model:account.account.template,name:l10n_ch.ch_coa_2800 msgid "Capital-actions, capital social, capital de fondation" msgstr "" @@ -417,109 +327,91 @@ msgstr "" "della fondazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4521 #: model:account.account.template,name:l10n_ch.ch_coa_4521 msgid "Charbon, briquettes, bois" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7010 #: model:account.account.template,name:l10n_ch.ch_coa_7010 msgid "Charges accessoires" msgstr "Costi attività accessoria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6000 #: model:account.account.template,name:l10n_ch.ch_coa_6000 msgid "Charges de locaux" msgstr "Costi dei locali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4000 #: model:account.account.template,name:l10n_ch.ch_coa_4000 msgid "Charges de matériel de l‘atelier" msgstr "Costi materiale per la fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5900 #: model:account.account.template,name:l10n_ch.ch_coa_5900 msgid "Charges de personnels temporaires" msgstr "Prestazioni di terz" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6200 #: model:account.account.template,name:l10n_ch.ch_coa_6200 msgid "Charges de véhicules et de transport" msgstr "Costi auto e di trasporto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7510 #: model:account.account.template,name:l10n_ch.ch_coa_7510 msgid "Charges des immeubles d‘exploitation" msgstr "Costi immobili aziendali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6500 #: model:account.account.template,name:l10n_ch.ch_coa_6500 msgid "Charges d‘administration" msgstr "Costi amministrativi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6400 #: model:account.account.template,name:l10n_ch.ch_coa_6400 msgid "Charges d’énergie et évacuation des déchets" msgstr "Costi energia e smaltimento" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6570 #: model:account.account.template,name:l10n_ch.ch_coa_6570 msgid "Charges et leasing d’informatique" msgstr "Costi informatici incluso leasing" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8500 #: model:account.account.template,name:l10n_ch.ch_coa_8500 msgid "Charges extraordinaires, exceptionnelles ou hors période" msgstr "Costi straordinari, unici o relativi ad altri periodi contabili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6900 #: model:account.account.template,name:l10n_ch.ch_coa_6900 msgid "Charges financières" msgstr "Costi finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8000 #: model:account.account.template,name:l10n_ch.ch_coa_8000 msgid "Charges hors exploitation" msgstr "Costi estranei" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1300 #: model:account.account.template,name:l10n_ch.ch_coa_1300 msgid "Charges payées d‘avance" msgstr "Costi anticipati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5700 #: model:account.account.template,name:l10n_ch.ch_coa_5700 msgid "Charges sociales" msgstr "Oneri sociali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2300 #: model:account.account.template,name:l10n_ch.ch_coa_2300 msgid "Charges à payer" msgstr "Costi da pagare" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3803 #: model:account.account.template,name:l10n_ch.ch_coa_3803 msgid "Commissions de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4903 #: model:account.account.template,name:l10n_ch.ch_coa_4903 msgid "Commissions obtenues sur achats" msgstr "" @@ -530,147 +422,113 @@ msgid "Companies" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1099 #: model:account.account.template,name:l10n_ch.ch_coa_1099 msgid "Compte d'attente autre" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1091 #: model:account.account.template,name:l10n_ch.ch_coa_1091 msgid "Compte d'attente pour salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3710 #: model:account.account.template,name:l10n_ch.ch_coa_3710 msgid "Consommations propres" msgstr "Consumo proprio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1269 #: model:account.account.template,name:l10n_ch.ch_coa_1269 msgid "Correction de la valeur de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1279 #: model:account.account.template,name:l10n_ch.ch_coa_1279 msgid "Corrections de la valeur des stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1209 #: model:account.account.template,name:l10n_ch.ch_coa_1209 msgid "Corrections de la valeur des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1289 #: model:account.account.template,name:l10n_ch.ch_coa_1289 msgid "Corrections de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1219 #: model:account.account.template,name:l10n_ch.ch_coa_1219 msgid "Corrections de la valeur sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1180 #: model:account.account.template,name:l10n_ch.ch_coa_1180 msgid "Créances envers les assurances sociales et institutions de prévoyance" msgstr "Crediti da assicurazioni sociali e istituti di previdenza" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2000 #: model:account.account.template,name:l10n_ch.ch_coa_2000 msgid "Créanciers" msgstr "Debiti per forniture e prestazioni (creditori)" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2100 -#: model:account.account,name:l10n_ch.1_ch_coa_2400 #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" msgstr "Banca passiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2160 #: model:account.account.template,name:l10n_ch.ch_coa_2160 msgid "Dettes envers l'actionnaire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3806 -#: model:account.account,name:l10n_ch.1_ch_coa_4906 #: model:account.account.template,name:l10n_ch.ch_coa_3806 #: model:account.account.template,name:l10n_ch.ch_coa_4906 msgid "Différences de change" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2261 #: model:account.account.template,name:l10n_ch.ch_coa_2261 msgid "Dividendes" msgstr "Dividendi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4071 #: model:account.account.template,name:l10n_ch.ch_coa_4071 msgid "Droits de douanes à l'importation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1109 #: model:account.account.template,name:l10n_ch.ch_coa_1109 msgid "Ducroire" msgstr "Delcredere" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1100 #: model:account.account.template,name:l10n_ch.ch_coa_1100 msgid "Débiteurs" msgstr "Crediti da forniture e prestazioni (debitori)" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2201 #: model:account.account.template,name:l10n_ch.ch_coa_2201 msgid "Décompte TVA" msgstr "IVA, rendiconto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4009 #: model:account.account.template,name:l10n_ch.ch_coa_4009 msgid "Déductions obtenues sur achats" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3009 #: model:account.account.template,name:l10n_ch.ch_coa_3009 msgid "Déductions sur ventes" msgstr "Diminuzione di ricavi" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4540 #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4500 #: model:account.account.template,name:l10n_ch.ch_coa_4500 msgid "Electricité" msgstr "" @@ -681,323 +539,202 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2450 #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "Prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2430 #: model:account.account.template,name:l10n_ch.ch_coa_2430 msgid "Emprunts obligataires" msgstr "Prestiti obbligazionari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2120 -#: model:account.account,name:l10n_ch.1_ch_coa_2420 #: model:account.account.template,name:l10n_ch.ch_coa_2120 #: model:account.account.template,name:l10n_ch.ch_coa_2420 msgid "Engagements de financement par leasing" msgstr "Impegni leasing finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6100 #: model:account.account.template,name:l10n_ch.ch_coa_6100 msgid "" "Entretien, réparations et remplacement des inst. servant à l’exploitation" msgstr "Manutenzioni, riparazioni e sostituzione immobilizzazioni mobiliari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1570 #: model:account.account.template,name:l10n_ch.ch_coa_1570 msgid "Equipements et Installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3800 -#: model:account.account,name:l10n_ch.1_ch_coa_4900 #: model:account.account.template,name:l10n_ch.ch_coa_3800 #: model:account.account.template,name:l10n_ch.ch_coa_4900 msgid "Escomptes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4530 #: model:account.account.template,name:l10n_ch.ch_coa_4530 msgid "Essence" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3804 #: model:account.account.template,name:l10n_ch.ch_coa_3804 msgid "Frais d'encaissement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3807 #: model:account.account.template,name:l10n_ch.ch_coa_3807 msgid "Frais d'expédition" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4072 #: model:account.account.template,name:l10n_ch.ch_coa_4072 msgid "Frais de transport à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4070 #: model:account.account.template,name:l10n_ch.ch_coa_4070 msgid "Frêts à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4510 #: model:account.account.template,name:l10n_ch.ch_coa_4510 msgid "Gaz" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1770 #: model:account.account.template,name:l10n_ch.ch_coa_1770 msgid "Goodwill" msgstr "Goodwill" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1441 -#: model:account.account,name:l10n_ch.1_ch_coa_2451 #: model:account.account.template,name:l10n_ch.ch_coa_1441 #: model:account.account.template,name:l10n_ch.ch_coa_2451 msgid "Hypothèques" msgstr "Prestiti ipotecari" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "" -"ISR number split in blocks of 5 characters (right-justified), to generate " -"ISR report." -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1600 #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "Immobili aziendali" #. module: l10n_ch -#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_import #: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_import msgid "Import/Export" msgstr "Import/Export" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1176 #: model:account.account.template,name:l10n_ch.ch_coa_1176 msgid "Impôt anticipé" msgstr "Imposta preventiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2206 #: model:account.account.template,name:l10n_ch.ch_coa_2206 msgid "Impôt anticipé dû" msgstr "Imposta preventiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1171 #: model:account.account.template,name:l10n_ch.ch_coa_1171 msgid "Impôt préalable: TVA s/investissements et autres charges d’exploitation" msgstr "IVA, imposta precedente su investimenti e altri costi d’esercizio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1170 #: model:account.account.template,name:l10n_ch.ch_coa_1170 msgid "Impôt préalable: TVA s/matériel, marchandises, prestations et énergie" msgstr "IVA, Imposta precedente su materiale, merce, servizi e energia" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1189 -#: model:account.account,name:l10n_ch.1_ch_coa_2279 #: model:account.account.template,name:l10n_ch.ch_coa_1189 #: model:account.account.template,name:l10n_ch.ch_coa_2279 msgid "Impôt à la source" msgstr "Imposte alla fonte" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2208 -#: model:account.account,name:l10n_ch.1_ch_coa_8900 #: model:account.account.template,name:l10n_ch.ch_coa_2208 #: model:account.account.template,name:l10n_ch.ch_coa_8900 msgid "Impôts directs" msgstr "Imposte dirette" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1550 #: model:account.account.template,name:l10n_ch.ch_coa_1550 msgid "Installations de stockage" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6260 #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "Leasing e noleggio auto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6105 #: model:account.account.template,name:l10n_ch.ch_coa_6105 msgid "Leasing immobilisations corporelles meubles" msgstr "Costi leasing immobilizzazioni mobiliari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1520 +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "Macchine ufficio, informatica e tecnologia della comunicazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1500 #: model:account.account.template,name:l10n_ch.ch_coa_1500 msgid "Machines et appareils" msgstr "Macchine e attrezzature" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1200 #: model:account.account.template,name:l10n_ch.ch_coa_1200 msgid "Marchandises commerciales" msgstr "Merce di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1250 #: model:account.account.template,name:l10n_ch.ch_coa_1250 msgid "Marchandises en consignation" msgstr "Merce in consegna" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1220 #: model:account.account.template,name:l10n_ch.ch_coa_1220 msgid "Matières auxiliaires" msgstr "Materia ausiliaria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1230 #: model:account.account.template,name:l10n_ch.ch_coa_1230 msgid "Matières consommables" msgstr "Materiale di consumo" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1210 #: model:account.account.template,name:l10n_ch.ch_coa_1210 msgid "Matières premières" msgstr "Materia prima" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4520 #: model:account.account.template,name:l10n_ch.ch_coa_4520 msgid "Mazout" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1510 #: model:account.account.template,name:l10n_ch.ch_coa_1510 msgid "Mobilier et installations" msgstr "Mobilio e installazioni" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1540 #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "Utensili e apparecchiature" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1480 #: model:account.account.template,name:l10n_ch.ch_coa_1480 msgid "Participations" msgstr "Partecipazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4086 #: model:account.account.template,name:l10n_ch.ch_coa_4086 msgid "Pertes de matières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3805 #: model:account.account.template,name:l10n_ch.ch_coa_3805 msgid "Pertes sur créances clients, variation ducroire" msgstr "" @@ -1008,92 +745,51 @@ msgid "Plan comptable 2015" msgstr "Piano Conti 2015" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "" -"Postal reference of the bank, formated with '-' and without the padding " -"zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4400 #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "Lavori di terzi / prestazioni di subappaltanti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3700 #: model:account.account.template,name:l10n_ch.ch_coa_3700 msgid "Prestations propres" msgstr "Lavori interni" #. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "" -"Print the coordinates of your bank under the 'Payment for' title of the " -"ISR.\n" -" Your address will be moved to the 'in favour " -"of' section." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7000 #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "Ricavi attività accessoria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7500 #: model:account.account.template,name:l10n_ch.ch_coa_7500 msgid "Produits des immeubles d‘exploitation" msgstr "Ricavi immobili aziendali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2301 #: model:account.account.template,name:l10n_ch.ch_coa_2301 msgid "Produits encaissés d’avance" msgstr "Ricavi incassati dell’anno seguente" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8510 #: model:account.account.template,name:l10n_ch.ch_coa_8510 msgid "Produits extraordinaires, exceptionnels ou hors période" msgstr "Ricavi straordinari, unici o relativi ad altri periodi contabili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6950 #: model:account.account.template,name:l10n_ch.ch_coa_6950 msgid "Produits financiers" msgstr "Ricavi finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8100 #: model:account.account.template,name:l10n_ch.ch_coa_8100 msgid "Produits hors exploitation" msgstr "Ricavi estranei" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1301 #: model:account.account.template,name:l10n_ch.ch_coa_1301 msgid "Produits à recevoir" msgstr "Ricavi da incassare" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2980 #: model:account.account.template,name:l10n_ch.ch_coa_2980 msgid "" "Propres actions, parts sociales, droits de participations (poste négatif)" @@ -1101,97 +797,77 @@ msgstr "" "Azioni proprie, parti sociali, diritti di partecipazione (posta negativa)" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2600 #: model:account.account.template,name:l10n_ch.ch_coa_2600 msgid "Provisions" msgstr "Accantonamenti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2330 #: model:account.account.template,name:l10n_ch.ch_coa_2330 msgid "Provisions à court terme" msgstr "Accantonamenti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1440 #: model:account.account.template,name:l10n_ch.ch_coa_1440 msgid "Prêts" msgstr "Prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6600 #: model:account.account.template,name:l10n_ch.ch_coa_6600 msgid "Publicité" msgstr "Costi pubblicitari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3801 #: model:account.account.template,name:l10n_ch.ch_coa_3801 msgid "Rabais et réduction de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4901 #: model:account.account.template,name:l10n_ch.ch_coa_4901 msgid "Rabais et réductions de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3802 -#: model:account.account,name:l10n_ch.1_ch_coa_4092 #: model:account.account.template,name:l10n_ch.ch_coa_3802 #: model:account.account.template,name:l10n_ch.ch_coa_4092 msgid "Ristournes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2940 #: model:account.account.template,name:l10n_ch.ch_coa_2940 msgid "Réserves d‘évaluation" msgstr "Riserve da rivalutazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2960 #: model:account.account.template,name:l10n_ch.ch_coa_2960 msgid "Réserves libres" msgstr "Riserve facoltative da utili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2950 #: model:account.account.template,name:l10n_ch.ch_coa_2950 msgid "Réserves légales issues du bénéfice" msgstr "Riserva legale da utili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2900 #: model:account.account.template,name:l10n_ch.ch_coa_2900 msgid "Réserves légales issues du capital" msgstr "Riserva legale da capitale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5000 #: model:account.account.template,name:l10n_ch.ch_coa_5000 msgid "Salaires" msgstr "Salari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1260 #: model:account.account.template,name:l10n_ch.ch_coa_1260 msgid "Stocks de produits finis" msgstr "Prodotti finiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1270 #: model:account.account.template,name:l10n_ch.ch_coa_1270 msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1232,13 +908,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1252,13 +928,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1272,6 +948,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1297,304 +983,228 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_import +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "IVA 0% Importazioni di bene e servizi" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_exclude #: model:account.tax.template,name:l10n_ch.vat_O_exclude msgid "TVA 0% exclue" msgstr "IVA 0% Esclusa" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase_incl +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase #: model:account.tax.template,name:l10n_ch.vat_25_purchase msgid "TVA 2.5% sur achat B&S (TR)" msgstr "TVA 2.5% sur achat B&S (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,name:l10n_ch.vat_25_invest_incl msgid "TVA 2.5% sur invest. et autres ch. (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest #: model:account.tax.template,name:l10n_ch.vat_25_invest msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "IVA 2.5% Investimenti e altri costi (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "TVA 3.8% sur achat B&S (TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" -msgstr "IVA 3.8% Investimenti e altri costi (TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" -msgstr "TVA 8.0% sur achat B&S (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" -msgstr "IVA 8.0% Investimenti e altri costi (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" +msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2200 #: model:account.account.template,name:l10n_ch.ch_coa_2200 msgid "TVA due" msgstr "IVA dovuta" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_XO #: model:account.tax.template,name:l10n_ch.vat_XO msgid "TVA due a 0% (Exportations)" msgstr "IVA Vendite 0% (Export)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25 #: model:account.tax.template,name:l10n_ch.vat_25 msgid "TVA due a 2.5% (TR)" msgstr "IVA Vendite al 2.5% (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38 -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" -msgstr "IVA Vendite al 3.8% (TS)" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80 -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" -msgstr "IVA Vendite al 8.0% (TN)" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_incl -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_incl -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_incl -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in CHF." +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in EUR." +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1060 #: model:account.account.template,name:l10n_ch.ch_coa_1060 msgid "Titres" msgstr "Titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1400 #: model:account.account.template,name:l10n_ch.ch_coa_1400 msgid "Titres à long terme" msgstr "Titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_transfer_account_id -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1280 #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "Prodotti in corso di fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3940 #: model:account.account.template,name:l10n_ch.ch_coa_3940 msgid "Variation de la valeur des prestations non facturées" msgstr "Variazione prestazioni di servizi non fatturate" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1287 #: model:account.account.template,name:l10n_ch.ch_coa_1287 msgid "Variation de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1277 #: model:account.account.template,name:l10n_ch.ch_coa_1277 msgid "Variation de stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1267 #: model:account.account.template,name:l10n_ch.ch_coa_1267 msgid "Variation de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1207 -#: model:account.account,name:l10n_ch.1_ch_coa_4800 #: model:account.account.template,name:l10n_ch.ch_coa_1207 #: model:account.account.template,name:l10n_ch.ch_coa_4800 msgid "Variation des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4801 #: model:account.account.template,name:l10n_ch.ch_coa_4801 msgid "Variation des stocks de matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3901 #: model:account.account.template,name:l10n_ch.ch_coa_3901 msgid "Variation des stocks de produits finis" msgstr "Variazione delle scorte di prodotti finiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3900 #: model:account.account.template,name:l10n_ch.ch_coa_3900 msgid "Variation des stocks de produits semi-finis" msgstr "Variazione delle scorte di prodotti in corso di fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1217 #: model:account.account.template,name:l10n_ch.ch_coa_1217 msgid "Variation des stocks des matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4008 -#: model:account.account,name:l10n_ch.1_ch_coa_4080 #: model:account.account.template,name:l10n_ch.ch_coa_4008 #: model:account.account.template,name:l10n_ch.ch_coa_4080 msgid "Variations de stocks" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3200 #: model:account.account.template,name:l10n_ch.ch_coa_3200 msgid "Ventes de marchandises" msgstr "Ricavi merci di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3400 #: model:account.account.template,name:l10n_ch.ch_coa_3400 msgid "Ventes de prestations" msgstr "Ricavi prestazioni di servizi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3000 #: model:account.account.template,name:l10n_ch.ch_coa_3000 msgid "Ventes de produits fabriqués" msgstr "Ricavi prodotti fabbricati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1530 #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "Veicoli" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "" -"You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or " -"an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal " -"reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the " -"invoice with the postal to be used to receive the related payment. A default " -"account will be automatically set for all invoices created after you defined " -"a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" diff --git a/addons/l10n_ch/i18n_extra/l10n_ch.pot b/addons/l10n_ch/i18n_extra/l10n_ch.pot index e2c03bcba8b..73b91c2b356 100644 --- a/addons/l10n_ch/i18n_extra/l10n_ch.pot +++ b/addons/l10n_ch/i18n_extra/l10n_ch.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.saas~15+e\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" -"PO-Revision-Date: 2017-03-14 15:52+0000\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" +"PO-Revision-Date: 2019-02-12 05:39+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,496 +16,393 @@ msgstr "" "Plural-Forms: \n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_100_import #: model:account.tax.template,name:l10n_ch.vat_100_import msgid "100% dédouanement TVA" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4200 #: model:account.account.template,name:l10n_ch.ch_coa_4200 msgid "Achats de marchandises destinées à la revente" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2030 #: model:account.account.template,name:l10n_ch.ch_coa_2030 msgid "Acomptes de clients" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1208 #: model:account.account.template,name:l10n_ch.ch_coa_1208 msgid "Acomptes sur les marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1218 #: model:account.account.template,name:l10n_ch.ch_coa_1218 msgid "Acomptes sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1149 #: model:account.account.template,name:l10n_ch.ch_coa_1149 msgid "Ajustement de la valeur des avances et des prêts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1199 #: model:account.account.template,name:l10n_ch.ch_coa_1199 msgid "Ajustement de la valeur des créances à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1449 #: model:account.account.template,name:l10n_ch.ch_coa_1449 msgid "Ajustement de la valeur des créances à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1779 #: model:account.account.template,name:l10n_ch.ch_coa_1779 msgid "Ajustement de la valeur des goodwill" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1489 #: model:account.account.template,name:l10n_ch.ch_coa_1489 msgid "Ajustement de la valeur des participations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1069 -#: model:account.account,name:l10n_ch.1_ch_coa_1409 #: model:account.account.template,name:l10n_ch.ch_coa_1069 #: model:account.account.template,name:l10n_ch.ch_coa_1409 msgid "Ajustement de la valeur des titres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6800 #: model:account.account.template,name:l10n_ch.ch_coa_6800 msgid "Amortissements et ajustements de valeur des postes sur immobilisations corporelles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1519 #: model:account.account.template,name:l10n_ch.ch_coa_1519 msgid "Amortissements sur le mobilier et les installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1599 #: model:account.account.template,name:l10n_ch.ch_coa_1599 msgid "Amortissements sur les autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1709 #: model:account.account.template,name:l10n_ch.ch_coa_1709 msgid "Amortissements sur les brevets, know-how, licences, droits, dév." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1609 #: model:account.account.template,name:l10n_ch.ch_coa_1609 msgid "Amortissements sur les immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1559 #: model:account.account.template,name:l10n_ch.ch_coa_1559 msgid "Amortissements sur les installations de stockage" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1529 #: model:account.account.template,name:l10n_ch.ch_coa_1529 msgid "Amortissements sur les machines de bureau, inf. et syst. comm." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1509 #: model:account.account.template,name:l10n_ch.ch_coa_1509 msgid "Amortissements sur les machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1549 #: model:account.account.template,name:l10n_ch.ch_coa_1549 msgid "Amortissements sur les outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1539 #: model:account.account.template,name:l10n_ch.ch_coa_1539 msgid "Amortissements sur les véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1579 #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2270 #: model:account.account.template,name:l10n_ch.ch_coa_2270 msgid "Assurances sociales et institutions de prévoyance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6300 #: model:account.account.template,name:l10n_ch.ch_coa_6300 msgid "Assurances-choses, droits, taxes, autorisations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5800 #: model:account.account.template,name:l10n_ch.ch_coa_5800 msgid "Autres charges du personnel" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6700 #: model:account.account.template,name:l10n_ch.ch_coa_6700 msgid "Autres charges d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1190 #: model:account.account.template,name:l10n_ch.ch_coa_1190 msgid "Autres créances à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2210 #: model:account.account.template,name:l10n_ch.ch_coa_2210 msgid "Autres dettes à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2140 #: model:account.account.template,name:l10n_ch.ch_coa_2140 msgid "Autres dettes à court terme rémunérées" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2500 #: model:account.account.template,name:l10n_ch.ch_coa_2500 msgid "Autres dettes à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1590 #: model:account.account.template,name:l10n_ch.ch_coa_1590 msgid "Autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3600 #: model:account.account.template,name:l10n_ch.ch_coa_3600 msgid "Autres ventes et prestations de services" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1140 #: model:account.account.template,name:l10n_ch.ch_coa_1140 msgid "Avances et prêts" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "Boolean value telling whether or not the ISR corresponding to this invoice has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1700 #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2979 #: model:account.account.template,name:l10n_ch.ch_coa_2979 msgid "Bénéfice / perte de l’exercice" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2970 #: model:account.account.template,name:l10n_ch.ch_coa_2970 msgid "Bénéfice / perte reporté" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1850 #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "Capital actions, capital social, droits de participations ou capital de fondation non versés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2800 #: model:account.account.template,name:l10n_ch.ch_coa_2800 msgid "Capital-actions, capital social, capital de fondation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4521 #: model:account.account.template,name:l10n_ch.ch_coa_4521 msgid "Charbon, briquettes, bois" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7010 #: model:account.account.template,name:l10n_ch.ch_coa_7010 msgid "Charges accessoires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6000 #: model:account.account.template,name:l10n_ch.ch_coa_6000 msgid "Charges de locaux" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4000 #: model:account.account.template,name:l10n_ch.ch_coa_4000 msgid "Charges de matériel de l‘atelier" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5900 #: model:account.account.template,name:l10n_ch.ch_coa_5900 msgid "Charges de personnels temporaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6200 #: model:account.account.template,name:l10n_ch.ch_coa_6200 msgid "Charges de véhicules et de transport" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7510 #: model:account.account.template,name:l10n_ch.ch_coa_7510 msgid "Charges des immeubles d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6500 #: model:account.account.template,name:l10n_ch.ch_coa_6500 msgid "Charges d‘administration" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6400 #: model:account.account.template,name:l10n_ch.ch_coa_6400 msgid "Charges d’énergie et évacuation des déchets" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6570 #: model:account.account.template,name:l10n_ch.ch_coa_6570 msgid "Charges et leasing d’informatique" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8500 #: model:account.account.template,name:l10n_ch.ch_coa_8500 msgid "Charges extraordinaires, exceptionnelles ou hors période" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6900 #: model:account.account.template,name:l10n_ch.ch_coa_6900 msgid "Charges financières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8000 #: model:account.account.template,name:l10n_ch.ch_coa_8000 msgid "Charges hors exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1300 #: model:account.account.template,name:l10n_ch.ch_coa_1300 msgid "Charges payées d‘avance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5700 #: model:account.account.template,name:l10n_ch.ch_coa_5700 msgid "Charges sociales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2300 #: model:account.account.template,name:l10n_ch.ch_coa_2300 msgid "Charges à payer" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3803 #: model:account.account.template,name:l10n_ch.ch_coa_3803 msgid "Commissions de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4903 #: model:account.account.template,name:l10n_ch.ch_coa_4903 msgid "Commissions obtenues sur achats" msgstr "" @@ -516,147 +413,113 @@ msgid "Companies" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1099 #: model:account.account.template,name:l10n_ch.ch_coa_1099 msgid "Compte d'attente autre" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1091 #: model:account.account.template,name:l10n_ch.ch_coa_1091 msgid "Compte d'attente pour salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3710 #: model:account.account.template,name:l10n_ch.ch_coa_3710 msgid "Consommations propres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1269 #: model:account.account.template,name:l10n_ch.ch_coa_1269 msgid "Correction de la valeur de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1279 #: model:account.account.template,name:l10n_ch.ch_coa_1279 msgid "Corrections de la valeur des stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1209 #: model:account.account.template,name:l10n_ch.ch_coa_1209 msgid "Corrections de la valeur des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1289 #: model:account.account.template,name:l10n_ch.ch_coa_1289 msgid "Corrections de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1219 #: model:account.account.template,name:l10n_ch.ch_coa_1219 msgid "Corrections de la valeur sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1180 #: model:account.account.template,name:l10n_ch.ch_coa_1180 msgid "Créances envers les assurances sociales et institutions de prévoyance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2000 #: model:account.account.template,name:l10n_ch.ch_coa_2000 msgid "Créanciers" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2100 -#: model:account.account,name:l10n_ch.1_ch_coa_2400 #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2160 #: model:account.account.template,name:l10n_ch.ch_coa_2160 msgid "Dettes envers l'actionnaire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3806 -#: model:account.account,name:l10n_ch.1_ch_coa_4906 #: model:account.account.template,name:l10n_ch.ch_coa_3806 #: model:account.account.template,name:l10n_ch.ch_coa_4906 msgid "Différences de change" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2261 #: model:account.account.template,name:l10n_ch.ch_coa_2261 msgid "Dividendes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4071 #: model:account.account.template,name:l10n_ch.ch_coa_4071 msgid "Droits de douanes à l'importation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1109 #: model:account.account.template,name:l10n_ch.ch_coa_1109 msgid "Ducroire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1100 #: model:account.account.template,name:l10n_ch.ch_coa_1100 msgid "Débiteurs" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2201 #: model:account.account.template,name:l10n_ch.ch_coa_2201 msgid "Décompte TVA" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4009 #: model:account.account.template,name:l10n_ch.ch_coa_4009 msgid "Déductions obtenues sur achats" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3009 #: model:account.account.template,name:l10n_ch.ch_coa_3009 msgid "Déductions sur ventes" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4540 #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4500 #: model:account.account.template,name:l10n_ch.ch_coa_4500 msgid "Electricité" msgstr "" @@ -667,320 +530,201 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2450 #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2430 #: model:account.account.template,name:l10n_ch.ch_coa_2430 msgid "Emprunts obligataires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2120 -#: model:account.account,name:l10n_ch.1_ch_coa_2420 #: model:account.account.template,name:l10n_ch.ch_coa_2120 #: model:account.account.template,name:l10n_ch.ch_coa_2420 msgid "Engagements de financement par leasing" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6100 #: model:account.account.template,name:l10n_ch.ch_coa_6100 msgid "Entretien, réparations et remplacement des inst. servant à l’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1570 #: model:account.account.template,name:l10n_ch.ch_coa_1570 msgid "Equipements et Installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3800 -#: model:account.account,name:l10n_ch.1_ch_coa_4900 #: model:account.account.template,name:l10n_ch.ch_coa_3800 #: model:account.account.template,name:l10n_ch.ch_coa_4900 msgid "Escomptes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4530 #: model:account.account.template,name:l10n_ch.ch_coa_4530 msgid "Essence" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3804 #: model:account.account.template,name:l10n_ch.ch_coa_3804 msgid "Frais d'encaissement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3807 #: model:account.account.template,name:l10n_ch.ch_coa_3807 msgid "Frais d'expédition" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4072 #: model:account.account.template,name:l10n_ch.ch_coa_4072 msgid "Frais de transport à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4070 #: model:account.account.template,name:l10n_ch.ch_coa_4070 msgid "Frêts à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4510 #: model:account.account.template,name:l10n_ch.ch_coa_4510 msgid "Gaz" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1770 #: model:account.account.template,name:l10n_ch.ch_coa_1770 msgid "Goodwill" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1441 -#: model:account.account,name:l10n_ch.1_ch_coa_2451 #: model:account.account.template,name:l10n_ch.ch_coa_1441 #: model:account.account.template,name:l10n_ch.ch_coa_2451 msgid "Hypothèques" msgstr "" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "ISR number split in blocks of 5 characters (right-justified), to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1600 #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_import #: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_import msgid "Import/Export" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1176 #: model:account.account.template,name:l10n_ch.ch_coa_1176 msgid "Impôt anticipé" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2206 #: model:account.account.template,name:l10n_ch.ch_coa_2206 msgid "Impôt anticipé dû" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1171 #: model:account.account.template,name:l10n_ch.ch_coa_1171 msgid "Impôt préalable: TVA s/investissements et autres charges d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1170 #: model:account.account.template,name:l10n_ch.ch_coa_1170 msgid "Impôt préalable: TVA s/matériel, marchandises, prestations et énergie" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1189 -#: model:account.account,name:l10n_ch.1_ch_coa_2279 #: model:account.account.template,name:l10n_ch.ch_coa_1189 #: model:account.account.template,name:l10n_ch.ch_coa_2279 msgid "Impôt à la source" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2208 -#: model:account.account,name:l10n_ch.1_ch_coa_8900 #: model:account.account.template,name:l10n_ch.ch_coa_2208 #: model:account.account.template,name:l10n_ch.ch_coa_8900 msgid "Impôts directs" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1550 #: model:account.account.template,name:l10n_ch.ch_coa_1550 msgid "Installations de stockage" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6260 #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6105 #: model:account.account.template,name:l10n_ch.ch_coa_6105 msgid "Leasing immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1520 +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1500 #: model:account.account.template,name:l10n_ch.ch_coa_1500 msgid "Machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1200 #: model:account.account.template,name:l10n_ch.ch_coa_1200 msgid "Marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1250 #: model:account.account.template,name:l10n_ch.ch_coa_1250 msgid "Marchandises en consignation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1220 #: model:account.account.template,name:l10n_ch.ch_coa_1220 msgid "Matières auxiliaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1230 #: model:account.account.template,name:l10n_ch.ch_coa_1230 msgid "Matières consommables" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1210 #: model:account.account.template,name:l10n_ch.ch_coa_1210 msgid "Matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4520 #: model:account.account.template,name:l10n_ch.ch_coa_4520 msgid "Mazout" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1510 #: model:account.account.template,name:l10n_ch.ch_coa_1510 msgid "Mobilier et installations" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1540 #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1480 #: model:account.account.template,name:l10n_ch.ch_coa_1480 msgid "Participations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4086 #: model:account.account.template,name:l10n_ch.ch_coa_4086 msgid "Pertes de matières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3805 #: model:account.account.template,name:l10n_ch.ch_coa_3805 msgid "Pertes sur créances clients, variation ducroire" msgstr "" @@ -991,183 +735,127 @@ msgid "Plan comptable 2015" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "Postal reference of the bank, formated with '-' and without the padding zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4400 #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3700 #: model:account.account.template,name:l10n_ch.ch_coa_3700 msgid "Prestations propres" msgstr "" #. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "Print the coordinates of your bank under the 'Payment for' title of the ISR.\n" -" Your address will be moved to the 'in favour of' section." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7000 #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7500 #: model:account.account.template,name:l10n_ch.ch_coa_7500 msgid "Produits des immeubles d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2301 #: model:account.account.template,name:l10n_ch.ch_coa_2301 msgid "Produits encaissés d’avance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8510 #: model:account.account.template,name:l10n_ch.ch_coa_8510 msgid "Produits extraordinaires, exceptionnels ou hors période" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6950 #: model:account.account.template,name:l10n_ch.ch_coa_6950 msgid "Produits financiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8100 #: model:account.account.template,name:l10n_ch.ch_coa_8100 msgid "Produits hors exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1301 #: model:account.account.template,name:l10n_ch.ch_coa_1301 msgid "Produits à recevoir" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2980 #: model:account.account.template,name:l10n_ch.ch_coa_2980 msgid "Propres actions, parts sociales, droits de participations (poste négatif)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2600 #: model:account.account.template,name:l10n_ch.ch_coa_2600 msgid "Provisions" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2330 #: model:account.account.template,name:l10n_ch.ch_coa_2330 msgid "Provisions à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1440 #: model:account.account.template,name:l10n_ch.ch_coa_1440 msgid "Prêts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6600 #: model:account.account.template,name:l10n_ch.ch_coa_6600 msgid "Publicité" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3801 #: model:account.account.template,name:l10n_ch.ch_coa_3801 msgid "Rabais et réduction de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4901 #: model:account.account.template,name:l10n_ch.ch_coa_4901 msgid "Rabais et réductions de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3802 -#: model:account.account,name:l10n_ch.1_ch_coa_4092 #: model:account.account.template,name:l10n_ch.ch_coa_3802 #: model:account.account.template,name:l10n_ch.ch_coa_4092 msgid "Ristournes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2940 #: model:account.account.template,name:l10n_ch.ch_coa_2940 msgid "Réserves d‘évaluation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2960 #: model:account.account.template,name:l10n_ch.ch_coa_2960 msgid "Réserves libres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2950 #: model:account.account.template,name:l10n_ch.ch_coa_2950 msgid "Réserves légales issues du bénéfice" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2900 #: model:account.account.template,name:l10n_ch.ch_coa_2900 msgid "Réserves légales issues du capital" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5000 #: model:account.account.template,name:l10n_ch.ch_coa_5000 msgid "Salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1260 #: model:account.account.template,name:l10n_ch.ch_coa_1260 msgid "Stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1270 #: model:account.account.template,name:l10n_ch.ch_coa_1270 msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1208,13 +896,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1228,13 +916,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1248,6 +936,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1273,297 +971,228 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_import +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_exclude #: model:account.tax.template,name:l10n_ch.vat_O_exclude msgid "TVA 0% exclue" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase_incl +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase #: model:account.tax.template,name:l10n_ch.vat_25_purchase msgid "TVA 2.5% sur achat B&S (TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,name:l10n_ch.vat_25_invest_incl msgid "TVA 2.5% sur invest. et autres ch. (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest #: model:account.tax.template,name:l10n_ch.vat_25_invest msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2200 -#: model:account.account.template,name:l10n_ch.ch_coa_2200 -msgid "TVA due" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_XO -#: model:account.tax.template,name:l10n_ch.vat_XO -msgid "TVA due a 0% (Exportations)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25 -#: model:account.tax.template,name:l10n_ch.vat_25 -msgid "TVA due a 2.5% (TR)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38 -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80 -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_incl -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" +#: model:account.account.template,name:l10n_ch.ch_coa_2200 +msgid "TVA due" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_incl -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" +#: model:account.tax.template,name:l10n_ch.vat_XO +msgid "TVA due a 0% (Exportations)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_incl -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_25 +msgid "TVA due a 2.5% (TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "The postal reference of the bank, used to generate ISR payment slips in CHF." +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "The postal reference of the bank, used to generate ISR payment slips in EUR." +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1060 #: model:account.account.template,name:l10n_ch.ch_coa_1060 msgid "Titres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1400 #: model:account.account.template,name:l10n_ch.ch_coa_1400 msgid "Titres à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_transfer_account_id -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1280 #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3940 #: model:account.account.template,name:l10n_ch.ch_coa_3940 msgid "Variation de la valeur des prestations non facturées" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1287 #: model:account.account.template,name:l10n_ch.ch_coa_1287 msgid "Variation de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1277 #: model:account.account.template,name:l10n_ch.ch_coa_1277 msgid "Variation de stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1267 #: model:account.account.template,name:l10n_ch.ch_coa_1267 msgid "Variation de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1207 -#: model:account.account,name:l10n_ch.1_ch_coa_4800 #: model:account.account.template,name:l10n_ch.ch_coa_1207 #: model:account.account.template,name:l10n_ch.ch_coa_4800 msgid "Variation des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4801 #: model:account.account.template,name:l10n_ch.ch_coa_4801 msgid "Variation des stocks de matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3901 #: model:account.account.template,name:l10n_ch.ch_coa_3901 msgid "Variation des stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3900 #: model:account.account.template,name:l10n_ch.ch_coa_3900 msgid "Variation des stocks de produits semi-finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1217 #: model:account.account.template,name:l10n_ch.ch_coa_1217 msgid "Variation des stocks des matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4008 -#: model:account.account,name:l10n_ch.1_ch_coa_4080 #: model:account.account.template,name:l10n_ch.ch_coa_4008 #: model:account.account.template,name:l10n_ch.ch_coa_4080 msgid "Variations de stocks" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3200 #: model:account.account.template,name:l10n_ch.ch_coa_3200 msgid "Ventes de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3400 #: model:account.account.template,name:l10n_ch.ch_coa_3400 msgid "Ventes de prestations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3000 #: model:account.account.template,name:l10n_ch.ch_coa_3000 msgid "Ventes de produits fabriqués" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1530 #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the invoice with the postal to be used to receive the related payment. A default account will be automatically set for all invoices created after you defined a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" - diff --git a/addons/l10n_it/__manifest__.py b/addons/l10n_it/__manifest__.py index f78432b06b3..fd621d872fd 100644 --- a/addons/l10n_it/__manifest__.py +++ b/addons/l10n_it/__manifest__.py @@ -17,7 +17,7 @@ Piano dei conti italiano di un'impresa generica. Italian accounting chart and localization. """, 'category': 'Localization', - 'website': 'http://www.openerp-italia.org/', + 'website': 'http://www.odoo.com/', 'data': [ 'data/l10n_it_chart_data.xml', 'data/account.account.template.csv', diff --git a/addons/payment_ccavenue/__init__.py b/addons/l10n_it_edi/__init__.py index e52ad6f83e3..90b17558ffc 100644 --- a/addons/payment_ccavenue/__init__.py +++ b/addons/l10n_it_edi/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import controllers from . import models -from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers +from . import tests diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py new file mode 100644 index 00000000000..fc24a276e68 --- /dev/null +++ b/addons/l10n_it_edi/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Italy - E-invoicing', + 'version': '0.3', + 'depends': ['l10n_it'], + 'author': 'Odoo', + 'description': """ +E-invoice implementation + """, + 'category': 'Localization', + 'website': 'http://www.odoo.com/', + 'data': [ + 'security/ir.model.access.csv', + 'data/invoice_it_template.xml', + 'views/l10n_it_view.xml', + ], + 'demo': [ + 'data/account_invoice_demo.xml', + ], +} diff --git a/addons/l10n_it_edi/data/account_invoice_demo.xml b/addons/l10n_it_edi/data/account_invoice_demo.xml new file mode 100644 index 00000000000..c68a3871df5 --- /dev/null +++ b/addons/l10n_it_edi/data/account_invoice_demo.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <!-- add VAT, codice fiscal and tax system for main company --> + <record id="base.main_company" model="res.company"> + <field name="vat">IT00410123456</field> + <field name="l10n_it_codice_fiscale">0123456789987654</field> + <field name="l10n_it_tax_system">RF01</field> + <field name="zip">12345</field> + </record> + + <record id="base.res_partner_1" model="res.partner"> + <field name="vat">IT11111111123</field> + </record> + <record id="base.res_partner_2" model="res.partner"> + <field name="vat">IT11111111234</field> + </record> + <record id="base.res_partner_12" model="res.partner"> + <field name="vat">IT11111112345</field> + </record> + + </data> +</odoo> diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml new file mode 100644 index 00000000000..c3af915c58c --- /dev/null +++ b/addons/l10n_it_edi/data/invoice_it_template.xml @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <template id="account_invoice_line_it_FatturaPA"> + <t t-set="taxes" t-value="line.invoice_line_tax_ids.compute_all(line.price_unit)"/> + <DettaglioLinee> + <NumeroLinea t-esc="line_counter"/> + <CodiceArticolo t-if="line.product_id.barcode"> + <!--2.2.1.3--> + <CodiceTipo>EAN</CodiceTipo> + <CodiceValore t-esc="line.product_id.barcode"/> + </CodiceArticolo> + <CodiceArticolo t-if="line.product_id.default_code"> + <CodiceTipo>INTERNAL</CodiceTipo> + <CodiceValore t-esc="line.product_id.default_code"/> + </CodiceArticolo> + <Descrizione> + <t t-esc="line.name[:1000]"/> + <t t-if="not line.name" t-esc="'NO NAME'"/> + </Descrizione> + <Quantita t-esc="format_numbers(line.quantity)"/> + <UnitaMisura t-if="line.uom_id.category_id.measure_type != 'unit'" t-esc="line.uom_id.name"/> + <PrezzoUnitario t-esc="format_monetary(taxes['total_excluded'], currency)"/> + <ScontoMaggiorazione t-if="line.discount != 0"> + <!-- [2.2.1.10] --> + <Tipo t-esc="discount_type(line.discount)"/> + <Percentuale t-esc="format_numbers(abs(line.discount))"/> + </ScontoMaggiorazione> + <PrezzoTotale t-esc="format_monetary(line.price_subtotal, currency)"/> + <!-- without tax, must include any discounts and any extra charge--> + <AliquotaIVA t-if="line.invoice_line_tax_ids.amount_type == 'percent'" t-esc="format_numbers(line.invoice_line_tax_ids.amount)"/> + <AliquotaIVA t-if="line.invoice_line_tax_ids.amount_type != 'percent'" t-esc="'0.00'"/> + <Natura t-if="line.invoice_line_tax_ids.l10n_it_has_exoneration" t-esc="line.invoice_line_tax_ids.l10n_it_kind_exoneration"/> + </DettaglioLinee> + </template> + + <template id="account_invoice_it_FatturaPA_sede"> + <Sede> + <Indirizzo><t t-if="partner.street" t-esc="partner.street"/> <t t-if="partner.street2" t-esc="partner.street2"/></Indirizzo> + <CAP><t t-if="partner.country_id.code != 'IT'" t-esc="'00000'"/><t t-else="" t-esc="partner.zip"/></CAP> + <Comune t-esc="partner.city"/> + <Provincia t-if="partner.country_id.code == 'IT'" t-esc="partner.state_id.code"/> + <Nazione t-esc="partner.country_id.code"/> + </Sede> + </template> + + <template id="account_invoice_it_FatturaPA_export"> + <t t-set="currency" t-value="record.currency_id or record.company_currency_id"/> + <t t-set="bank" t-value="record.partner_bank_id"/> + <p:FatturaElettronica t-att-versione="formato_trasmissione" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd"> + <FatturaElettronicaHeader> + <DatiTrasmissione> + <IdTrasmittente> + <IdPaese t-esc="get_vat_country(record.company_id.vat)"/> + <IdCodice t-esc="record.company_id.l10n_it_codice_fiscale"/> + </IdTrasmittente> + <ProgressivoInvio t-esc="record.number.replace('/','')[-10:]"/> + <FormatoTrasmissione t-esc="formato_trasmissione"/> + <CodiceDestinatario t-if="record.commercial_partner_id.l10n_it_pa_index and record.commercial_partner_id.country_id.code == 'IT'" t-esc="record.commercial_partner_id.l10n_it_pa_index.upper()"/> + <CodiceDestinatario t-if="not record.commercial_partner_id.l10n_it_pa_index and record.commercial_partner_id.country_id.code == 'IT'" t-esc="'0000000'"/> + <CodiceDestinatario t-if="record.commercial_partner_id.country_id.code != 'IT'" t-esc="'XXXXXXX'"/> + <ContattiTrasmittente> + <Telefono t-if="format_phone(record.company_id.partner_id.phone)" t-esc="format_phone(record.company_id.partner_id.phone)"/> + <Telefono t-if="not format_phone(record.company_id.partner_id.phone) and format_phone(record.company_id.partner_id.mobile)" t-esc="format_phone(record.company_id.partner_id.mobile)"/> + <Email t-if="record.company_id.email" t-esc="record.company_id.email"/> + </ContattiTrasmittente> + <PECDestinatario t-if="record.commercial_partner_id.l10n_it_pec_email" t-esc="record.commercial_partner_id.l10n_it_pec_email"/> + </DatiTrasmissione> + <CedentePrestatore> + <DatiAnagrafici> + <IdFiscaleIVA> + <IdPaese t-esc="get_vat_country(record.company_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.company_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-esc="record.company_id.partner_id.display_name"/> + </Anagrafica> + <RegimeFiscale t-esc="record.company_id.l10n_it_tax_system"/> + </DatiAnagrafici> + <t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede"> + <t t-set="partner" t-value="record.company_id.partner_id"/> + </t> + <IscrizioneREA t-if="record.company_id.l10n_it_has_eco_index"> + <!--1.2.4--> + <Ufficio t-esc="record.company_id.l10n_it_eco_index_office.code"/> + <NumeroREA t-esc="record.company_id.l10n_it_eco_index_number"/> + <CapitaleSociale t-if="record.company_id.l10n_it_eco_index_share_capital != 0" t-esc="format_numbers_two(record.company_id.l10n_it_eco_index_share_capital)"/> + <SocioUnico t-if="record.company_id.l10n_it_eco_index_sole_shareholder != 'NO'" t-esc="record.company_id.l10n_it_eco_index_sole_shareholder"/> + <StatoLiquidazione t-esc="record.company_id.l10n_it_eco_index_liquidation_state"/> + </IscrizioneREA> + </CedentePrestatore> + <RappresentanteFiscale t-if="record.company_id.l10n_it_has_tax_representative"> + <!--1.3--> + <DatiAnagrafici> + <IdFiscaleIVA> + <IdPaese t-esc="get_vat_country(record.company_id.l10n_it_tax_representative_partner_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.company_id.l10n_it_tax_representative_partner_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="record.company_id.l10n_it_tax_representative_partner_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_tax_representative_partner_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-if="record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.display_name"/> + <Nome t-if="not record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.name"/> + <Cognome t-if="not record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.name"/> + </Anagrafica> + </DatiAnagrafici> + </RappresentanteFiscale> + <CessionarioCommittente> + <DatiAnagrafici> + <IdFiscaleIVA t-if="record.commercial_partner_id.vat"> + <IdPaese t-esc="get_vat_country(record.commercial_partner_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.commercial_partner_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="not record.commercial_partner_id.vat" t-esc="record.commercial_partner_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-if="record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.display_name"/> + <Nome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/> + <Cognome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/> + </Anagrafica> + </DatiAnagrafici> + <t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede"> + <t t-set="partner" t-value="record.commercial_partner_id"/> + </t> + </CessionarioCommittente> + </FatturaElettronicaHeader> + <FatturaElettronicaBody> + <DatiGenerali> + <DatiGeneraliDocumento> + <!--2.1.1--> + <TipoDocumento t-esc="document_type"/> + <Divisa t-esc="currency.name"/> + <Data t-esc="format_date(record.date_invoice)"/> + <Numero t-esc="record.number[-20:]"/> + <DatiBollo t-if="record.l10n_it_stamp_duty"> + <!--2.1.1.6--> + <BolloVirtuale>SI</BolloVirtuale> + <ImportoBollo t-esc="format_numbers(record.l10n_it_stamp_duty)"/> + </DatiBollo> + </DatiGeneraliDocumento> + <DatiDDT t-if="record.l10n_it_ddt_id"> + <!--2.1.8--> + <NumeroDDT t-esc="record.l10n_it_ddt_id.name"/> + <DataDDT t-esc="format_date(record.l10n_it_ddt_id.date)"/> + </DatiDDT> + </DatiGenerali> + <DatiBeniServizi> + <!-- Invoice lines. --> + <t t-set="line_counter" t-value="0"/> + <t t-foreach="record.invoice_line_ids.filtered(lambda l: not l.display_type)" t-as="line"> + <t t-set="line_counter" t-value="line_counter + 1"/> + <t t-call="l10n_it_edi.account_invoice_line_it_FatturaPA"/> + </t> + <t t-foreach="record.tax_line_ids" t-as="tax_line"> + <DatiRiepilogo> + <!--2.2.2--> + <AliquotaIVA t-esc="format_numbers(tax_line.tax_id.amount)"/> + <Natura t-if="tax_line.tax_id.l10n_it_has_exoneration" t-esc="tax_line.tax_id.l10n_it_kind_exoneration"/> + <ImponibileImporto t-esc="format_monetary(tax_line.base, currency)"/> + <Imposta t-esc="format_monetary(tax_line.amount, currency)"/> + <EsigibilitaIVA t-if="not tax_line.tax_id.l10n_it_has_exoneration or tax_line.tax_id.l10n_it_kind_exoneration=='N6'" t-esc="tax_line.tax_id.l10n_it_vat_due_date"/> + <RiferimentoNormativo t-if="tax_line.tax_id.l10n_it_has_exoneration" t-esc="tax_line.tax_id.l10n_it_law_reference"/> + </DatiRiepilogo> + </t> + + </DatiBeniServizi> + <Allegati t-if="pdf"> + <NomeAttachment t-esc="pdf_name"/> + <FormatoAttachment>PDF</FormatoAttachment> + <Attachment t-esc="pdf"/> + </Allegati> + </FatturaElettronicaBody> + </p:FatturaElettronica> + </template> + </data> +</odoo> diff --git a/addons/l10n_it_edi/models/__init__.py b/addons/l10n_it_edi/models/__init__.py new file mode 100644 index 00000000000..a7b2d212437 --- /dev/null +++ b/addons/l10n_it_edi/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import res_partner +from . import res_company +from . import account_invoice +from . import ir_mail_server +from . import ddt diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py new file mode 100644 index 00000000000..1ba9b6ec213 --- /dev/null +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -0,0 +1,758 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import zipfile +import io +import logging +import re + +from datetime import date, datetime +from lxml import etree + +from odoo import api, fields, models, _ +from odoo.tools import float_repr +from odoo.exceptions import UserError, ValidationError +from odoo.addons.base.models.ir_mail_server import MailDeliveryException +from odoo.tests.common import Form + + +_logger = logging.getLogger(__name__) + +DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d' + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + _name = 'account.invoice' + + l10n_it_send_state = fields.Selection([ + ('new', 'New'), + ('other', 'Other'), + ('to_send', 'Not yet send'), + ('sent', 'Sent, waiting for response'), + ('invalid', 'Sent, but invalid'), + ('delivered', 'This invoice is delivered'), + ('delivered_accepted', 'This invoice is delivered and accepted by destinatory'), + ('delivered_refused', 'This invoice is delivered and refused by destinatory'), + ('delivered_expired', 'This invoice is delivered and expired (expiry of the maximum term for communication of acceptance/refusal)'), + ('failed_delivery', 'Delivery impossible, ES certify that it has received the invoice and that the file \ + could not be delivered to the addressee') # ok we must do nothing + ], default='to_send', copy=False) + + l10n_it_stamp_duty = fields.Float(default=0, string="Dati Bollo", size=15, readonly=True, states={'draft': [('readonly', False)]}) + + l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False) + + l10n_it_einvoice_name = fields.Char(readonly=True, copy=False) + + l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", copy=False) + + @api.multi + def invoice_validate(self): + # Clean context from default_type to avoid making attachment + # with wrong values in subsequent operations + cleaned_ctx = dict(self.env.context) + cleaned_ctx.pop('default_type', None) + self = self.with_context(cleaned_ctx) + + super(AccountInvoice, self).invoice_validate() + for invoice in self: + if invoice.company_id.country_id != self.env.ref('base.it'): + continue + if invoice.type == 'in_invoice' or invoice.type == 'in_refund': + invoice.l10n_it_send_state = "other" + continue + + invoice._check_before_xml_exporting() + + invoice.invoice_generate_xml() + if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6: + invoice.message_post( + body=(_("Invoices for PA are not managed by Odoo, you can download the document and send it on your own.")) + ) + invoice.l10n_it_send_state = "other" + continue + invoice.l10n_it_send_state = "to_send" + invoice.send_pec_mail() + + def _check_before_xml_exporting(self): + seller = self.company_id + buyer = self.commercial_partner_id + + # <1.1.1.1> + if not seller.country_id: + raise UserError(_("%s must have a country") % (seller.display_name)) + + # <1.1.1.2> + if not seller.vat: + raise UserError(_("%s must have a VAT number") % (seller.display_name)) + elif len(seller.vat) > 30: + raise UserError(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.") % (seller.display_name, seller.vat)) + + # <1.2.1.2> + if not seller.l10n_it_codice_fiscale: + raise UserError(_("%s must have a codice fiscale number") % (seller.display_name)) + + # <1.2.1.8> + if not seller.l10n_it_tax_system: + raise UserError("The seller's company must have a tax system.") + + # <1.2.2> + if not seller.street and not seller.street2: + raise UserError(_("%s must have a street.") % (seller.display_name)) + if not seller.zip: + raise UserError(_("%s must have a post code.") % (seller.display_name)) + if len(seller.zip) != 5 and seller.country_id.code == 'IT': + raise UserError(_("%s must have a post code of length 5.") % (seller.display_name)) + if not seller.city: + raise UserError(_("%s must have a city.") % (seller.display_name)) + if not seller.country_id: + raise UserError(_("%s must have a country.") % (seller.display_name)) + + # <1.4.1> + if not buyer.vat and not buyer.l10n_it_codice_fiscale: + raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name)) + + # <1.4.2> + if not buyer.street and not buyer.street2: + raise UserError(_("%s must have a street.") % (buyer.display_name)) + if not buyer.zip: + raise UserError(_("%s must have a post code.") % (buyer.display_name)) + if len(buyer.zip) != 5 and buyer.country_id.code == 'IT': + raise UserError(_("%s must have a post code of length 5.") % (buyer.display_name)) + if not buyer.city: + raise UserError(_("%s must have a city.") % (buyer.display_name)) + if not buyer.country_id: + raise UserError(_("%s must have a country.") % (buyer.display_name)) + + # <2.2.1> + for invoice_line in self.invoice_line_ids: + if len(invoice_line.invoice_line_tax_ids) != 1: + raise UserError(_("You must select one and only one tax by line.")) + + for tax_line in self.tax_line_ids: + if not tax_line.tax_id.l10n_it_has_exoneration and tax_line.tax_id.amount == 0: + raise ValidationError(_("%s has an amount of 0.0, you must indicate the kind of exoneration." % tax_line.name)) + + @api.multi + def invoice_generate_xml(self): + for invoice in self: + if invoice.l10n_it_einvoice_id and invoice.l10n_it_send_state not in ['invalid', 'to_send']: + raise UserError(_("You can't regenerate an E-Invoice when the first one is sent and there are no errors")) + if invoice.l10n_it_einvoice_id: + invoice.l10n_it_einvoice_id.unlink() + + a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + n = invoice.id + progressive_number = "" + while n: + (n,m) = divmod(n,len(a)) + progressive_number = a[m] + progressive_number + + report_name = '%(country_code)s%(codice)s_%(progressive_number)s.xml' % { + 'country_code': invoice.company_id.country_id.code, + 'codice': invoice.company_id.l10n_it_codice_fiscale, + 'progressive_number': progressive_number.zfill(5), + } + invoice.l10n_it_einvoice_name = report_name + + data = b"<?xml version='1.0' encoding='UTF-8'?>" + invoice._export_as_xml() + description = _('Italian invoice: %s') % invoice.type + invoice.l10n_it_einvoice_id = self.env['ir.attachment'].create({ + 'name': report_name, + 'res_id': invoice.id, + 'res_model': invoice._name, + 'datas': base64.encodestring(data), + 'datas_fname': report_name, + 'description': description, + 'type': 'binary', + }) + + invoice.message_post( + body=(_("E-Invoice is generated on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + + def _export_as_xml(self): + ''' Create the xml file content. + :return: The XML content as str. + ''' + self.ensure_one() + + def format_date(dt): + # Format the date in the italian standard. + dt = dt or datetime.now() + return dt.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + def format_monetary(number, currency): + # Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001). + return float_repr(number, min(2, currency.decimal_places)) + + def format_numbers(number): + #format number to str with between 2 and 8 decimals (event if it's .00) + number_splited = str(number).split('.') + if len(number_splited) == 1: + return "%.02f" % number + + cents = number_splited[1] + if len(cents) > 8: + return "%.08f" % number + return float_repr(number, max(2, len(cents))) + + def format_numbers_two(number): + #format number to str with 2 (event if it's .00) + return "%.02f" % number + + def discount_type(discount): + return 'SC' if discount > 0 else 'MG' + + def format_phone(number): + if not number: + return False + number = number.replace(' ', '').replace('/', '').replace('.', '') + if len(number) > 4 and len(number) < 13: + return number + return False + + def get_vat_number(vat): + return vat[2:].replace(' ', '') + + def get_vat_country(vat): + return vat[:2].upper() + + formato_trasmissione = "FPR12" + if len(self.commercial_partner_id.l10n_it_pa_index or '1') == 6: + formato_trasmissione = "FPA12" + + if self.type == 'out_invoice': + document_type = 'TD01' + elif self.type == 'out_refund': + document_type = 'TD04' + else: + document_type = 'TD0X' + + pdf = self.env.ref('account.account_invoices').render_qweb_pdf(self.id)[0] + pdf = base64.b64encode(pdf) + pdf_name = re.sub(r'\W+', '', self.number) + '.pdf' + + # Create file content. + template_values = { + 'record': self, + 'format_date': format_date, + 'format_monetary': format_monetary, + 'format_numbers': format_numbers, + 'format_numbers_two': format_numbers_two, + 'format_phone': format_phone, + 'discount_type': discount_type, + 'get_vat_number': get_vat_number, + 'get_vat_country': get_vat_country, + 'abs': abs, + 'formato_trasmissione': formato_trasmissione, + 'document_type': document_type, + 'pdf': pdf, + 'pdf_name': pdf_name, + } + content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export').render(template_values) + return content + + @api.multi + def send_pec_mail(self): + self.ensure_one() + allowed_state = ['to_send', 'invalid'] + + if not self.company_id.l10n_it_mail_pec_server_id or not self.company_id.l10n_it_address_send_fatturapa: + self.message_post( + body=(_("Error when sending mail with E-Invoice: Your company must have a mail PEC server and must indicate the mail PEC that will send electronic invoice.")) + ) + self.l10n_it_send_state = 'invalid' + return + + if self.l10n_it_send_state not in allowed_state: + raise UserError(_("%s isn't in a right state. It must be in a 'Not yet send' or 'Invalid' state.") % (self.display_name)) + + message = self.env['mail.message'].create({ + 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_id.name), + 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.user.company_id.l10n_it_address_recipient_fatturapa), + 'email_from': self.env.user.company_id.l10n_it_address_send_fatturapa, + 'mail_server_id': self.env.user.company_id.l10n_it_mail_pec_server_id.id, + 'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)], + }) + + mail_fattura = self.env['mail.mail'].create({ + 'mail_message_id': message.id, + 'email_to': self.env.user.company_id.l10n_it_address_recipient_fatturapa, + }) + try: + mail_fattura.send(raise_exception=True) + self.message_post( + body=(_("Mail sent on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + self.l10n_it_send_state = 'sent' + except MailDeliveryException as error: + self.message_post( + body=(_("Error when sending mail with E-Invoice: %s") % (error.args[0])) + ) + self.l10n_it_send_state = 'invalid' + + def _import_xml_invoice(self, content, attachment): + ''' Extract invoice values from the E-Invoice xml tree passed as parameter. + + :param content: The tree of the xml file. + :return: A dictionary containing account.invoice values to create/update it. + ''' + + try: + tree = etree.fromstring(content) + except: + _logger.info('Error during decoding XML file') + return self.env['account.invoice'] + + invoices = self.env['account.invoice'] + + # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch + for body_tree in tree.xpath('//FatturaElettronicaBody', namespaces=tree.nsmap): + + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento', namespaces=tree.nsmap) + if elements and elements[0].text and elements[0].text == 'TD01': + self_ctx = self.with_context(type='in_invoice') + elif elements and elements[0].text and elements[0].text == 'TD04': + self_ctx = self.with_context(type='in_refund') + else: + _logger.info(_('Document type not managed: %s.') % (elements[0].text)) + + # type must be present in the context to get the right behavior of the _default_journal method (account.invoice). + # journal_id must be present in the context to get the right behavior of the _default_account method (account.invoice.line). + + elements = tree.xpath('//CessionarioCommittente//IdCodice', namespaces=tree.nsmap) + company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1) + if not company: + elements = tree.xpath('//CessionarioCommittente//CodiceFiscale', namespaces=tree.nsmap) + company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1) + + if company: + self_ctx = self_ctx.with_context(company_id=company.id) + else: + company = self.env.user.company_id + if elements: + _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text) + else: + _logger.info(_('Company not found. The company\'s user is set by default.')) + + if not self.env.user._is_superuser(): + if self.env.user.company_id != company: + raise UserError(_("You can only import invoice concern your current company: %s") % self.env.user.company_id.display_name) + + journal_id = self_ctx._default_journal().id + self_ctx = self_ctx.with_context(journal_id=journal_id) + + # self could be a single record (editing) or be empty (new). + with Form(self_ctx, view='account.invoice_supplier_form') as invoice_form: + message_to_log = [] + + invoice_form.company_id = company + + # Refund type. + # TD01 == invoice + # TD02 == advance/down payment on invoice + # TD03 == advance/down payment on fee + # TD04 == credit note + # TD05 == debit note + # TD06 == fee + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento', namespaces=tree.nsmap) + if elements and elements[0].text and elements[0].text == 'TD01': + invoice_form.type = 'in_invoice' + elif elements and elements[0].text and elements[0].text == 'TD04': + invoice_form.type = 'in_refund' + + # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> + elements = tree.xpath('//CedentePrestatore//IdCodice', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//CedentePrestatore//CodiceFiscale', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//DatiTrasmissione//Email', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if partner: + invoice_form.partner_id = partner + else: + message_to_log.append("%s<br/>%s" % ( + _("Vendor not found, useful informations from XML file:"), + self._compose_info_message( + tree, './/CedentePrestatore'))) + + # Numbering attributed by the transmitter. <1.1.2> + elements = tree.xpath('//ProgressivoInvio', namespaces=tree.nsmap) + if elements: + invoice_form.name = elements[0].text + + elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero', namespaces=body_tree.nsmap) + if elements: + invoice_form.reference = elements[0].text + + # Currency. <2.1.1.2> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa', namespaces=body_tree.nsmap) + if elements: + currency_str = elements[0].text + currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False) + if currency != self.env.user.company_id.currency_id and currency.active: + invoice_form.currency_id = currency + + # Date. <2.1.1.3> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Data', namespaces=body_tree.nsmap) + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.date_invoice = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + # Dati Bollo. <2.1.1.6> + elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo', namespaces=body_tree.nsmap) + if elements: + invoice_form.l10n_it_stamp_duty = float(elements[0].text) + + # List of all amount discount (will be add after all article to avoid to have a negative sum) + discount_list = [] + percentage_global_discount = 1.0 + + # Global discount. <2.1.1.8> + discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione', namespaces=body_tree.nsmap) + total_discount_amount = 0.0 + if discount_elements: + for discount_element in discount_elements: + discount_line = discount_element.xpath('.//Tipo', namespaces=body_tree.nsmap) + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = discount_element.xpath('.//Percentuale', namespaces=body_tree.nsmap) + if discount_percentage and discount_percentage[0].text: + percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign + + discount_amount_text = discount_element.xpath('.//Importo', namespaces=body_tree.nsmap) + if discount_amount_text and discount_amount_text[0].text: + discount_amount = float(discount_amount_text[0].text) * discount_sign * -1 + discount = {} + discount["seq"] = 0 + + if discount_amount < 0: + discount["name"] = _('GLOBAL DISCOUNT') + else: + discount["name"] = _('GLOBAL EXTRA CHARGE') + discount["amount"] = discount_amount + discount["tax"] = [] + discount_list.append(discount) + + # Comment. <2.1.1.11> + elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale', namespaces=body_tree.nsmap) + for element in elements: + invoice_form.comment = '%s%s\n' % (invoice_form.comment or '', element.text) + + # Informations relative to the purchase order, the contract, the agreement, + # the reception phase or invoices previously transmitted + # <2.1.2> - <2.1.6> + for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']: + elements = body_tree.xpath('.//DatiGenerali/' + document_type, namespaces=body_tree.nsmap) + if elements: + for element in elements: + message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"), + self._compose_info_message(element, '.'))) + + # Dati DDT. <2.1.8> + elements = body_tree.xpath('.//DatiGenerali/DatiDDT', namespaces=body_tree.nsmap) + if elements: + message_to_log.append("%s<br/>%s" % ( + _("Transport informations from XML file:"), + self._compose_info_message(body_tree, './/DatiGenerali/DatiDDT'))) + + # Due date. <2.4.2.5> + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento', namespaces=body_tree.nsmap) + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.date_due = fields.Date.to_string(date_obj) + + # Total amount. <2.4.2.6> + elements = body_tree.xpath('.//ImportoPagamento', namespaces=body_tree.nsmap) + amount_total_import = 0 + for element in elements: + amount_total_import += float(element.text) + if amount_total_import: + message_to_log.append(_("Total amount from the XML File: %s") % ( + amount_total_import)) + + # Bank account. <2.4.2.13> + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN', namespaces=body_tree.nsmap) + if elements: + if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id: + bank = self.env['res.partner.bank'].search([ + ('acc_number', '=', elements[0].text), + ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id) + ]) + else: + bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)]) + if bank: + invoice_form.partner_bank_id = bank + else: + message_to_log.append("%s<br/>%s" % ( + _("Bank account not found, useful informations from XML file:"), + self._compose_multi_info_message( + body_tree, ['.//DatiPagamento//Beneficiario', + './/DatiPagamento//IstitutoFinanziario', + './/DatiPagamento//IBAN', + './/DatiPagamento//ABI', + './/DatiPagamento//CAB', + './/DatiPagamento//BIC', + './/DatiPagamento//ModalitaPagamento']))) + else: + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento', namespaces=body_tree.nsmap) + if elements: + message_to_log.append("%s<br/>%s" % ( + _("Bank account not found, useful informations from XML file:"), + self._compose_info_message(body_tree, './/DatiPagamento'))) + + # Invoice lines. <2.2.1> + elements = body_tree.xpath('.//DettaglioLinee', namespaces=body_tree.nsmap) + if elements: + for element in elements: + with invoice_form.invoice_line_ids.new() as invoice_line_form: + + # Sequence. + line_elements = element.xpath('.//NumeroLinea', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.sequence = int(line_elements[0].text) * 2 + + # Product. + line_elements = element.xpath('.//Descrizione', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.name = " ".join(line_elements[0].text.split()) + + elements_code = element.xpath('.//CodiceArticolo', namespaces=body_tree.nsmap) + if elements_code: + for element_code in elements_code: + type_code = element_code.xpath('.//CodiceTipo', namespaces=body_tree.nsmap)[0] + code = element_code.xpath('.//CodiceValore', namespaces=body_tree.nsmap)[0] + if type_code.text == 'EAN': + product = self.env['product.product'].search([('barcode', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + if partner: + product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)]) + if product_supplier and product_supplier.product_id: + invoice_line_form.product_id = product_supplier.product_id + break + if not invoice_line_form.product_id: + for element_code in elements_code: + code = element_code.xpath('.//CodiceValore', namespaces=body_tree.nsmap)[0] + product = self.env['product.product'].search([('default_code', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + + # Price Unit. + line_elements = element.xpath('.//PrezzoUnitario', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.price_unit = float(line_elements[0].text) + + # Quantity. + line_elements = element.xpath('.//Quantita', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.quantity = float(line_elements[0].text) + else: + invoice_line_form.quantity = 1 + + # Taxes + tax_element = element.xpath('.//AliquotaIVA', namespaces=body_tree.nsmap) + natura_element = element.xpath('.//Natura', namespaces=body_tree.nsmap) + invoice_line_form.invoice_line_tax_ids.clear() + if tax_element and tax_element[0].text: + percentage = float(tax_element[0].text) + if natura_element and natura_element[0].text: + l10n_it_kind_exoneration = natura_element[0].text + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration), + ], limit=1) + else: + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ], limit=1) + l10n_it_kind_exoneration = '' + + if tax: + invoice_line_form.invoice_line_tax_ids.add(tax) + else: + if l10n_it_kind_exoneration: + message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % ( + percentage, + l10n_it_kind_exoneration, + invoice_line_form.name)) + else: + message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % ( + percentage, + invoice_line_form.name)) + + # Discount in cascade mode. + # if 3 discounts : -10% -50€ -20% + # the result must be : (((price -10%)-50€) -20%) + # Generic form : (((price -P1%)-A1€) -P2%) + # It will be split in two parts: fix amount and pourcent amount + # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€) + # pourcent: 1-(1-P2)*(1-P5) + # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all + # percentage present after the fix amount) + line_elements = element.xpath('.//ScontoMaggiorazione', namespaces=body_tree.nsmap) + total_discount_amount = 0.0 + total_discount_percentage = percentage_global_discount + if line_elements: + for line_element in line_elements: + discount_line = line_element.xpath('.//Tipo', namespaces=body_tree.nsmap) + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = line_element.xpath('.//Percentuale', namespaces=body_tree.nsmap) + if discount_percentage and discount_percentage[0].text: + pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign + total_discount_percentage *= pourcentage_actual + total_discount_amount *= pourcentage_actual + + discount_amount = line_element.xpath('.//Importo', namespaces=body_tree.nsmap) + if discount_amount and discount_amount[0].text: + total_discount_amount += float(discount_amount[0].text) * discount_sign * -1 + + # Save amount discount. + if total_discount_amount != 0: + discount = {} + discount["seq"] = invoice_line_form.sequence + 1 + + if total_discount_amount < 0: + discount["name"] = _('DISCOUNT: ') + invoice_line_form.name + else: + discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name + discount["amount"] = total_discount_amount + discount["tax"] = [] + for tax in invoice_line_form.invoice_line_tax_ids: + discount["tax"].append(tax) + discount_list.append(discount) + invoice_line_form.discount = (1 - total_discount_percentage) * 100 + + # Apply amount discount. + for discount in discount_list: + with invoice_form.invoice_line_ids.new() as invoice_line_form_discount: + invoice_line_form_discount.invoice_line_tax_ids.clear() + invoice_line_form_discount.sequence = discount["seq"] + invoice_line_form_discount.name = discount["name"] + invoice_line_form_discount.price_unit = discount["amount"] + + new_invoice = invoice_form.save() + new_invoice.l10n_it_send_state = "other" + + elements = body_tree.xpath('.//Allegati', namespaces=body_tree.nsmap) + if elements: + for element in elements: + name_attachment = element.xpath('.//NomeAttachment', namespaces=body_tree.nsmap)[0].text + attachment_64 = str.encode(element.xpath('.//Attachment', namespaces=body_tree.nsmap)[0].text) + attachment_64 = self.env['ir.attachment'].create({ + 'name': name_attachment, + 'datas': attachment_64, + 'datas_fname': name_attachment, + 'type': 'binary', + }) + + # default_res_id is had to context to avoid facturx to import his content + new_invoice.with_context(default_res_id=new_invoice.id).message_post( + body=(_("Attachment from XML")), + attachment_ids=[attachment_64.id] + ) + + for message in message_to_log: + new_invoice.message_post(body=message) + + if attachment: + new_invoice.l10n_it_einvoice_name = attachment.name + attachment.write({'res_model': 'account.invoice', 'res_id': new_invoice.id}) + new_invoice.message_post(attachment_ids=[attachment.id]) + invoices += new_invoice + return invoices + + def _compose_info_message(self, tree, element_tags): + output_str = "" + elements = tree.xpath(element_tags, namespaces=tree.nsmap) + for element in elements: + output_str += "<ul>" + for line in element.iter(): + if line.text: + text = " ".join(line.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (line.tag, text) + output_str += "</ul>" + return output_str + + def _compose_multi_info_message(self, tree, element_tags): + output_str = "<ul>" + + for element_tag in element_tags: + elements = tree.xpath(element_tag, namespaces=tree.nsmap) + if not elements: + continue + for element in elements: + text = " ".join(element.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (element.tag, text) + return output_str + "</ul>" + +class AccountTax(models.Model): + _name = "account.tax" + _inherit = "account.tax" + + l10n_it_vat_due_date = fields.Selection([ + ("I", "[I] IVA ad esigibilità immediata"), + ("D", "[D] IVA ad esigibilità differita"), + ("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date") + + l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.") + l10n_it_kind_exoneration = fields.Selection(selection=[ + ("N1", "[N1] Escluse ex art. 15"), + ("N2", "[N2] Non soggette"), + ("N3", "[N3] Non imponibili"), + ("N4", "[N4] Esenti"), + ("N5", "[N5] Regime del margine / IVA non esposta in fattura"), + ("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"), + ("N7", "[N7] IVA assolta in altro stato UE (vendite a distanza ex art. 40 c. 3 e 4 e art. 41 c. 1 lett. b, DL 331/93; prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-sexies lett. f, g, art. 74-sexies DPR 633/72)")], + string="Exoneration", + help="Exoneration type", + default="N1") + l10n_it_law_reference = fields.Char(string="Law Reference", size=100) + + @api.constrains('l10n_it_has_exoneration', + 'l10n_it_kind_exoneration', + 'l10n_it_law_reference', + 'amount', + 'l10n_it_vat_due_date') + def _check_exoneration_with_no_tax(self): + for tax in self: + if tax.l10n_it_has_exoneration: + if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0: + raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.") + if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S': + raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'")) + +class ImportInvoiceImportWizard(models.TransientModel): + _name = 'account.invoice.import.wizard' + _inherit = 'account.invoice.import.wizard' + + @api.multi + def _create_invoice_from_file(self, attachment): + if attachment.mimetype == 'application/xml' and re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.name): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', attachment.name)], limit=1): + # invoice already exist + raise UserError(_('E-invoice already exist: %s') % attachment.name) + self = self.with_context(default_journal_id= self.journal_id.id) + invoice = self.env['account.invoice']._import_xml_invoice(base64.decodestring(attachment.datas), attachment) + else: + invoice = super(ImportInvoiceImportWizard, self)._create_invoice_from_file(attachment) + return invoice diff --git a/addons/l10n_it_edi/models/ddt.py b/addons/l10n_it_edi/models/ddt.py new file mode 100644 index 00000000000..61464a7d76a --- /dev/null +++ b/addons/l10n_it_edi/models/ddt.py @@ -0,0 +1,21 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + +class L10nItDdt(models.Model): + _name = 'l10n_it.ddt' + _description = 'Transport Document' + + invoice_id = fields.One2many('account.invoice', 'l10n_it_ddt_id', string='Invoice Reference', + ondelete='cascade') + + name = fields.Char(string="Numero DDT", size=20, help="Transport document number", required=True) + date = fields.Date(string="Data DDT", help="Transport document date", required=True) + + @api.multi + def name_get(self): + res = [] + for ddt in self: + res.append((ddt.id, ("%s (%s)") % (ddt.name, ddt.date))) + return res diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py new file mode 100644 index 00000000000..3bfce47100d --- /dev/null +++ b/addons/l10n_it_edi/models/ir_mail_server.py @@ -0,0 +1,358 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import zipfile +import io +import re +import logging +import email +import dateutil +import pytz +import base64 +try: + from xmlrpc import client as xmlrpclib +except ImportError: + import xmlrpclib + + +from lxml import etree +from datetime import datetime + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import ValidationError + + +_logger = logging.getLogger(__name__) + +class FetchmailServer(models.Model): + _name = 'fetchmail.server' + _inherit = 'fetchmail.server' + + l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.") + l10n_it_last_uid = fields.Integer(string='Last message UID', default=1) + + @api.constrains('l10n_it_is_pec', 'type') + def _check_pec(self): + for record in self: + if record.l10n_it_is_pec and record.type != 'imap': + raise ValidationError("PEC mail server must be of type IMAP.") + + @api.multi + def fetch_mail(self): + """ WARNING: meant for cron usage only - will commit() after each email! """ + + MailThread = self.env['mail.thread'] + for server in self.filtered(lambda s: s.l10n_it_is_pec): + _logger.info('start checking for new emails on %s PEC server %s', server.type, server.name) + + count, failed = 0, 0 + imap_server = None + try: + imap_server = server.connect() + imap_server.select() + + result, data = imap_server.uid('search', None, '(FROM "@pec.fatturapa.it")', '(UID %s:*)' % (server.l10n_it_last_uid)) + new_max_uid = server.l10n_it_last_uid + for uid in data[0].split(): + if int(uid) <= server.l10n_it_last_uid: + # We get always minimum 1 message. If no new message, we receive the newest already managed. + continue + + result, data = imap_server.uid('fetch', uid, '(RFC822)') + + if not data[0]: + continue + message = data[0][1] + + # To leave the mail in the state in which they were. + if "Seen" not in data[1].decode("utf-8"): + imap_server.uid('STORE', uid, '+FLAGS', '\\Seen') + else: + imap_server.uid('STORE', uid, '-FLAGS', '\\Seen') + + # See details in message_process() in mail_thread.py + if isinstance(message, xmlrpclib.Binary): + message = bytes(message.data) + if isinstance(message, str): + message = message.encode('utf-8') + msg_txt = email.message_from_bytes(message) + + try: + self._attachment_invoice(msg_txt) + new_max_uid = max(new_max_uid, int(uid)) + except Exception: + _logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True) + failed += 1 + self._cr.commit() + count += 1 + server.write({'l10n_it_last_uid': new_max_uid}) + _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed) + except Exception: + _logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True) + finally: + if imap_server: + imap_server.close() + imap_server.logout() + server.write({'date': fields.Datetime.now()}) + return super(FetchmailServer, self.filtered(lambda s: not s.l10n_it_is_pec)).fetch_mail() + + def _attachment_invoice(self, msg_txt): + body, attachments = self.env['mail.thread']._message_extract_payload(msg_txt) + from_address = tools.decode_smtp_header(msg_txt.get('from')) + for attachment in attachments: + split_attachment = attachment.fname.rpartition('.') + if len(split_attachment) < 3: + _logger.info('E-invoice filename not compliant: %s', attachment.fname) + continue + attachment_name = split_attachment[0] + attachment_ext = split_attachment[2] + split_underscore = attachment_name.rsplit('_', 2) + if len(split_underscore) < 2: + _logger.info('E-invoice filename not compliant: %s', attachment.fname) + continue + + if attachment_ext != 'zip': + if split_underscore[1] in ['RC', 'NS', 'MC', 'MT', 'EC', 'SE', 'NE', 'DT']: + # we have a receipt + self._message_receipt_invoice(split_underscore[1], attachment) + elif re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname): + # we have a new E-invoice + self._create_invoice_from_mail(attachment.content, attachment.fname, from_address) + else: + if split_underscore[1] == 'AT': + # Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito + self._message_AT_invoice(attachment) + else: + _logger.info('New E-invoice in zip file: %s', attachment.fname) + self._create_invoice_from_mail_with_zip(attachment, from_address) + + def _create_invoice_from_mail(self, att_content, att_name, from_address): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1): + # invoice already exist + _logger.info('E-invoice already exist: %s', att_name) + return + + invoice_attachment = self.env['ir.attachment'].create({ + 'name': att_name, + 'datas': base64.encodestring(att_content), + 'datas_fname': att_name, + 'type': 'binary', + }) + + invoice = self.env['account.invoice']._import_xml_invoice(att_content, invoice_attachment) + invoice.l10n_it_send_state = "new" + invoice.source_email = from_address + self._cr.commit() + + _logger.info('New E-invoice: %s', att_name) + + + def _create_invoice_from_mail_with_zip(self, attachment_zip, from_address): + with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z: + for att_name in z.namelist(): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1): + # invoice already exist + _logger.info('E-invoice in zip file (%s) already exist: %s', attachment_zip.fname, att_name) + continue + att_content = z.open(att_name).read() + + self._create_invoice_from_mail(att_content, att_name, from_address) + + def _message_AT_invoice(self, attachment_zip): + with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z: + for attachment_name in z.namelist(): + split_name_attachment = attachment_name.rpartition('.') + if len(split_name_attachment) < 3: + continue + split_underscore = split_name_attachment[0].rsplit('_', 2) + if len(split_underscore) < 2: + continue + if split_underscore[1] == 'AT': + attachment = z.open(attachment_name).read() + _logger.info('New AT receipt for: %s', split_underscore[0]) + try: + tree = etree.fromstring(attachment) + except: + _logger.info('Error in decoding new receipt file: %s', attachment_name) + return + + elements = tree.xpath('//NomeFile', namespaces=tree.nsmap) + if elements and elements[0].text: + filename = elements[0].text + else: + return + + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename)]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', filename) + return + + related_invoice.l10n_it_send_state = 'failed_delivery' + info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//MessageId', '//PecMessageId', '//Note']) + related_invoice.message_post( + body=(_("ES certify that it has received the invoice and that the file \ + could not be delivered to the addressee. <br/>%s") % (info)) + ) + + def _message_receipt_invoice(self, receipt_type, attachment): + try: + tree = etree.fromstring(attachment.content) + except: + _logger.info('Error in decoding new receipt file: %s', attachment.fname) + return {} + + elements = tree.xpath('//NomeFile', namespaces=tree.nsmap) + if elements and elements[0].text: + filename = elements[0].text + else: + return {} + + if receipt_type == 'RC': + # Delivery receipt + # This is the receipt sent by the ES to the transmitting subject to communicate + # delivery of the file to the addressee + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'delivered' + info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//DataOraConsegna', '//Note']) + related_invoice.message_post( + body=(_("E-Invoice is delivery to the destinatory:<br/>%s") % (info)) + ) + + elif receipt_type == 'NS': + # Rejection notice + # This is the receipt sent by the ES to the transmitting subject if one or more of + # the checks carried out by the ES on the file received do not have a successful result. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'invalid' + error = self._return_error_xml(tree) + related_invoice.message_post( + body=(_("Errors in the E-Invoice :<br/>%s") % (error)) + ) + activity_vals = { + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id + } + related_invoice.activity_schedule(summary='Rejection notice', **activity_vals) + + elif receipt_type == 'MC': + # Failed delivery notice + # This is the receipt sent by the ES to the transmitting subject if the file is not + # delivered to the addressee. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + info = self._return_multi_line_xml(tree, [ + '//IdentificativoSdI', + '//DataOraRicezione', + '//Descrizione', + '//MessageId', + '//Note']) + related_invoice.message_post( + body=(_("The E-invoice is not delivered to the addressee. The Exchange System is\ + unable to deliver the file to the Public Administration. The Exchange System will\ + contact the PA to report the problem and request that they provide a solution. \ + During the following 15 days, the Exchange System will try to forward the FatturaPA\ + file to the Administration in question again. More informations:<br/>%s") % (info)) + ) + + elif receipt_type == 'NE': + # Outcome notice + # This is the receipt sent by the ES to the invoice sender to communicate the result + # (acceptance or refusal of the invoice) of the checks carried out on the document by + # the addressee. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'delivered')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + elements = tree.xpath('//Esito', namespaces=tree.nsmap) + if elements and elements[0].text: + if elements[0].text == 'EC01': + related_invoice.l10n_it_send_state = 'delivered_accepted' + elif elements[0].text == 'EC02': + related_invoice.l10n_it_send_state = 'delivered_refused' + + info = self._return_multi_line_xml(tree, + ['//Esito', + '//Descrizione', + '//IdentificativoSdI', + '//DataOraRicezione', + '//DataOraConsegna', + '//Note' + ]) + related_invoice.message_post( + body=(_("Outcome notice: %s<br/>%s") % (related_invoice.l10n_it_send_state, info)) + ) + if related_invoice.l10n_it_send_state == 'delivered_refused': + activity_vals = { + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id + } + related_invoice.activity_schedule(summary='Outcome notice: Refused', **activity_vals) + + # elif receipt_type == 'MT': + # Metadata file + # This is the file sent by the ES to the addressee together with the invoice file, + # containing the main reference data of the file useful for processing, including + # the IdentificativoSDI. + # Useless for Odoo + + elif receipt_type == 'DT': + # Deadline passed notice + # This is the receipt sent by the ES to both the invoice sender and the invoice + # addressee to communicate the expiry of the maximum term for communication of + # acceptance/refusal. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), ('l10n_it_send_state', '=', 'delivered')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'delivered_expired' + info = self._return_multi_line_xml(tree, [ + '//Descrizione', + '//IdentificativoSdI', + '//Note']) + related_invoice.message_post( + body=(_("Expiration of the maximum term for communication of acceptance/refusal:\ + %s<br/>%s") % (filename, info)) + ) + + def _return_multi_line_xml(self, tree, element_tags): + output_str = "<ul>" + + for element_tag in element_tags: + elements = tree.xpath(element_tag, namespaces=tree.nsmap) + if not elements: + continue + for element in elements: + text = " ".join(element.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (element.tag, text) + return output_str + "</ul>" + + def _return_error_xml(self, tree): + output_str = "<ul>" + + elements = tree.xpath('//Errore', namespaces=tree.nsmap) + if not elements: + return + for element in elements: + descrizione = " ".join(element[1].text.split()) + if descrizione: + output_str += "<li>Errore %s: %s</li>" % (element[0].text, descrizione) + return output_str + "</ul>" diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py new file mode 100644 index 00000000000..a0cce53b9a5 --- /dev/null +++ b/addons/l10n_it_edi/models/res_company.py @@ -0,0 +1,110 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +TAX_SYSTEM = [ + ("RF01", "[RF01] Ordinario"), + ("RF02", "[RF02] Contribuenti minimi (art.1, c.96-117, L. 244/07)"), + ("RF04", "[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)"), + ("RF05", "[RF05] Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)"), + ("RF06", "[RF06] Commercio fiammiferi (art.74, c.1, DPR 633/72)"), + ("RF07", "[RF07] Editoria (art.74, c.1, DPR 633/72)"), + ("RF08", "[RF08] Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)"), + ("RF09", "[RF09] Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)"), + ("RF10", "[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)"), + ("RF11", "[RF11] Agenzie viaggi e turismo (art.74-ter, DPR 633/72)"), + ("RF12", "[RF12] Agriturismo (art.5, c.2, L. 413/91)"), + ("RF13", "[RF13] Vendite a domicilio (art.25-bis, c.6, DPR 600/73)"), + ("RF14", "[RF14] Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)"), + ("RF15", "[RF15] Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)"), + ("RF16", "[RF16] IVA per cassa P.A. (art.6, c.5, DPR 633/72)"), + ("RF17", "[RF17] IVA per cassa (art. 32-bis, DL 83/2012)"), + ("RF18", "[RF18] Altro"), + ("RF19", "[RF19] Regime forfettario (art.1, c.54-89, L. 190/2014)"), +] + +class ResCompany(models.Model): + _name = 'res.company' + _inherit = 'res.company' + + l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16, related='partner_id.l10n_it_codice_fiscale', + store=True, readonly=False, help="Fiscal code of your company") + l10n_it_tax_system = fields.Selection(selection=TAX_SYSTEM, string="Tax System", + help="Please select the Tax system to which you are subjected.") + + # PEC server + l10n_it_mail_pec_server_id = fields.Many2one('ir.mail_server', string="Server PEC", + help="Configure your PEC-mail server to send electronic invoices.") + l10n_it_address_recipient_fatturapa = fields.Char(string="Government PEC-mail", + help="Enter Government PEC-mail address. Ex: sdi01@pec.fatturapa.it") + l10n_it_address_send_fatturapa = fields.Char(string="Company PEC-mail", + help="Enter your company PEC-mail address. Ex: yourcompany@pec.mail.it") + + + # Economic and Administrative Index + l10n_it_has_eco_index = fields.Boolean(default=False, + help="The seller/provider is a company listed on the register of companies and as\ + such must also indicate the registration data on all documents (art. 2250, Italian\ + Civil Code)") + l10n_it_eco_index_office = fields.Many2one('res.country.state', domain="[('country_id','=','IT')]", + string="Province of the register-of-companies office") + l10n_it_eco_index_number = fields.Char(string="Number in register of companies", size=20, + help="This field must contain the number under which the\ + seller/provider is listed on the register of companies.") + l10n_it_eco_index_share_capital = fields.Float(default="0.0", size=15, string="Share capital actually paid up", + help="Mandatory if the seller/provider is a company with share\ + capital (SpA, SApA, Srl), this field must contain the amount\ + of share capital actually paid up as resulting from the last\ + financial statement") + l10n_it_eco_index_sole_shareholder = fields.Selection( + [ + ("NO", "Not a limited liability company"), + ("SU", "Socio unico"), + ("SM", "Più soci")], + string="Shareholder") + l10n_it_eco_index_liquidation_state = fields.Selection( + [ + ("LS", "The company is in a state of liquidation"), + ("LN", "The company is not in a state of liquidation")], + string="Liquidation state") + + + # Tax representative + l10n_it_has_tax_representative = fields.Boolean(default=False, + help="The seller/provider is a non-resident subject which\ + carries out transactions in Italy with relevance for VAT\ + purposes and which takes avail of a tax representative in\ + Italy") + l10n_it_tax_representative_partner_id = fields.Many2one('res.partner', string='Tax representative partner') + + @api.constrains('l10n_it_has_eco_index', + 'l10n_it_eco_index_office', + 'l10n_it_eco_index_number', + 'l10n_it_eco_index_share_capital', + 'l10n_it_eco_index_sole_shareholder', + 'l10n_it_eco_index_liquidation_state') + def _check_eco_admin_index(self): + for record in self: + if not record.l10n_it_has_eco_index: + continue + if not record.l10n_it_eco_index_office\ + or not record.l10n_it_eco_index_number\ + or not record.l10n_it_eco_index_share_capital\ + or not record.l10n_it_eco_index_sole_shareholder\ + or not record.l10n_it_eco_index_liquidation_state: + raise ValidationError("All fields about the Economic and Administrative Index must be completed.") + + @api.constrains('l10n_it_has_tax_representative', + 'l10n_it_tax_representative_partner_id') + def _check_tax_representative(self): + for record in self: + if not record.l10n_it_has_tax_representative: + continue + if not record.l10n_it_tax_representative_partner_id: + raise ValidationError("You must select a tax representative.") + if not record.l10n_it_tax_representative_partner_id.vat: + raise ValidationError("Your tax representative partner must have a tax number.") + if not record.l10n_it_tax_representative_partner_id.country_id: + raise ValidationError("Your tax representative partner must have a country.") diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py new file mode 100644 index 00000000000..e0a78f24d58 --- /dev/null +++ b/addons/l10n_it_edi/models/res_partner.py @@ -0,0 +1,28 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +class ResPartner(models.Model): + _name = 'res.partner' + _inherit = 'res.partner' + + l10n_it_pec_email = fields.Char(string="PEC e-mail") + l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16) + l10n_it_pa_index = fields.Char(string="PA index", + size=7, + help="Must contain the 6-character (or 7) code, present in the PA\ + Index in the information relative to the electronic invoicing service,\ + associated with the office which, within the addressee administration, deals\ + with receiving (and processing) the invoice.") + + _sql_constraints = [ + ('l10n_it_codice_fiscale', + "CHECK(l10n_it_codice_fiscale IS NULL OR LENGTH(l10n_it_codice_fiscale) >= 11)", + "Codice fiscale must have between 11 and 16 characters."), + + ('l10n_it_pa_index', + "CHECK(l10n_it_pa_index IS NULL OR LENGTH(l10n_it_pa_index) >= 6)", + "PA index must have between 6 and 7 characters."), + ] diff --git a/addons/l10n_it_edi/security/ir.model.access.csv b/addons/l10n_it_edi/security/ir.model.access.csv new file mode 100644 index 00000000000..22c0da9c159 --- /dev/null +++ b/addons/l10n_it_edi/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_it_ddt_manager","it_ddt manager","model_l10n_it_ddt","account.group_account_invoice",1,1,1,1
\ No newline at end of file diff --git a/addons/l10n_it_edi/tests/__init__.py b/addons/l10n_it_edi/tests/__init__.py new file mode 100644 index 00000000000..d7b285d123c --- /dev/null +++ b/addons/l10n_it_edi/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_generate diff --git a/addons/l10n_it_edi/tests/test_generate.py b/addons/l10n_it_edi/tests/test_generate.py new file mode 100644 index 00000000000..60592ade8c7 --- /dev/null +++ b/addons/l10n_it_edi/tests/test_generate.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import time + +from odoo import fields +from odoo.tests.common import TransactionCase, Form + +class TestItalianElectronicInvoice(TransactionCase): + def test_state(self): + f = Form(self.env['account.invoice']) + f.partner_id = self.env.ref('base.res_partner_12') + with f.invoice_line_ids.new() as l: + l.product_id = self.env.ref('product.product_product_3') + invoice = f.save() + + # I check that Initially customer invoice state is "Draft" + self.assertEqual(invoice.state, 'draft') + + # I called the "Confirm Draft Invoices" wizard + w = Form(self.env['account.invoice.confirm']).save() + # I clicked on Confirm Invoices Button + w.with_context( + active_model='account.invoice', + active_id=invoice.id, + active_ids=invoice.ids, + type='out_invoice', + ).invoice_confirm() + + # I check that customer invoice state is "Open" + self.assertEqual(invoice.state, 'open') + + # Electronic invoice must be present and have the same name as l10n_it_einvoice_name + self.assertEqual(invoice.l10n_it_einvoice_id.name, invoice.l10n_it_einvoice_name) diff --git a/addons/l10n_it_edi/views/l10n_it_view.xml b/addons/l10n_it_edi/views/l10n_it_view.xml new file mode 100644 index 00000000000..7ae8222bf79 --- /dev/null +++ b/addons/l10n_it_edi/views/l10n_it_view.xml @@ -0,0 +1,265 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="fetchmail_server_form_l10n_it" model="ir.ui.view"> + <field name="name">fetchmail.server.form.l10n.it</field> + <field name="model">fetchmail.server</field> + <field name="priority">20</field> + <field name="inherit_id" ref="fetchmail.view_email_server_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='date']" position="after"> + <field name="l10n_it_is_pec"/> + </xpath> + </data> + </field> + </record> + + <record id="account_tax_form_l10n_it" model="ir.ui.view"> + <field name="name">account.tax.form.l10n.it</field> + <field name="model">account.tax</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.view_tax_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//page" position="inside"> + <group> + <field name="l10n_it_vat_due_date"/> + <field name="l10n_it_has_exoneration" readonly="False"/> + <field name="l10n_it_kind_exoneration" attrs="{'invisible': [('l10n_it_has_exoneration', '=', False)]}"/> + <field name="l10n_it_law_reference" attrs="{'invisible': [('l10n_it_has_exoneration', '=', False)]}"/> + </group> + </xpath> + </data> + </field> + </record> + + <record id="res_partner_form_l10n_it" model="ir.ui.view"> + <field name="name">res.partner.form.l10n.it</field> + <field name="model">res.partner</field> + <field name="priority">20</field> + <field name="inherit_id" ref="base.view_partner_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='category_id']" position="after"> + <field name="l10n_it_pec_email" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + <field name="l10n_it_codice_fiscale" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + <field name="l10n_it_pa_index" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + </xpath> + </data> + </field> + </record> + + <record id="res_company_form_l10n_it" model="ir.ui.view"> + <field name="name">res.company.form.l10n.it</field> + <field name="model">res.company</field> + <field name="priority">20</field> + <field name="inherit_id" ref="base.view_company_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//div[hasclass('o_address_format')]" position="after"> + <field name="l10n_it_mail_pec_server_id"/> + <field name="l10n_it_address_send_fatturapa"/> + <field name="l10n_it_address_recipient_fatturapa"/> + </xpath> + <xpath expr="//field[@name='vat']" position="after"> + <field name="l10n_it_codice_fiscale"/> + <field name="l10n_it_tax_system"/> + </xpath> + <xpath expr="//page" position="after"> + <page string="Electronic Invoicing"> + <group> + <separator string="Economic and Administrative Index" colspan="4"/> + <div colspan="4"> + The seller/provider is a company listed on the register of companies and as + such must also indicate the registration data on all documents (art. 2250, Italian + Civil Code) + </div> + <group> + <field name="l10n_it_has_eco_index" string="Company listed on the register of companies"/> + </group> + <group attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"> + <field name="l10n_it_eco_index_office"/> + <field name="l10n_it_eco_index_number"/> + <field name="l10n_it_eco_index_share_capital"/> + <field name="l10n_it_eco_index_sole_shareholder"/> + <field name="l10n_it_eco_index_liquidation_state"/> + </group> + </group> + <group> + <separator string="Tax representative" colspan="4"/> + <div colspan="4"> + The seller/provider is a non-resident subject which carries out transactions in Italy + with relevance for VAT purposes and which takes avail of a tax representative in Italy + </div> + <group> + <field name="l10n_it_has_tax_representative" string="Company have a tax representative"/> + </group> + <group attrs="{'invisible': [('l10n_it_has_tax_representative', '=', False)]}"> + <field name="l10n_it_tax_representative_partner_id"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="invoice_supplier_tree_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.supplier.tree.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_tree"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='type']" position="after"> + <field name="l10n_it_send_state" invisible="1" widget="label_selection" options="{'classes': {'to_send': 'default', 'invalid': 'danger', 'sent': 'warning', + 'delivered': 'success', 'delivered_accepted': 'success', 'delivered_refused': 'success', 'delivered_expired': 'success', 'failed_delivery': 'success'}}"/> + + <button icon="fa-paper-plane-o" class="btn-outline-warning disabled" aria-label="Sent" title="Sent" attrs="{'invisible': [('l10n_it_send_state', '!=', 'sent')]}"/> + <button icon="fa-exclamation-triangle" class="btn-outline-danger disabled" aria-label="Error" title="Error" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}"/> + <button icon="fa-check" class="btn-outline-success disabled" aria-label="Delivered" title="Delivered" attrs="{'invisible': [('l10n_it_send_state', 'not in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]}"/> + </xpath> + </data> + </field> + </record> + + <record id="invoice_kanban_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.kanban.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_kanban"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='currency_id']" position="after"> + <field name="l10n_it_send_state"/> + </xpath> + + <xpath expr="//div[hasclass('o_kanban_record_headings')]" position="inside"> + <i class="text-success fa fa-plus-circle" t-if="record.l10n_it_send_state.raw_value == 'new'"/> + <i class="text-warning fa fa-paper-plane-o" t-if="record.l10n_it_send_state.raw_value == 'sent'"/> + <i class="text-danger fa fa-exclamation-triangle" t-if="record.l10n_it_send_state.raw_value == 'invalid'"/> + <i class="text-success fa fa-check" t-if="['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'].indexOf(record.l10n_it_send_state.raw_value) >= 0" /> + </xpath> + </data> + </field> + </record> + + <record id="account_invoice_form_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.form.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//button[@name='preview_invoice']" position="after"> + <button name="invoice_validate" type="object" icon="fa-envelope-o" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}" class="btn btn-primary" string="Resend"/> + <button name="invoice_validate" type="object" icon="fa-envelope-o" attrs="{'invisible': ['|',('state', 'in', ['draft', 'cancel']),('l10n_it_send_state', 'not in', ['to_send', False])]}" class="btn btn-primary" string="Send"/> + </xpath> + <xpath expr="//field[@name='type']" position="before"> + <div class="alert alert-success" role="alert" attrs="{'invisible': [('l10n_it_send_state', 'not in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]}"> + <i class="fa fa-check" aria-label="Delivered" title="Delivered"></i> <field name="l10n_it_send_state" readonly="1"/> + </div> + <div class="alert alert-warning" role="alert" attrs="{'invisible': [('l10n_it_send_state', '!=', 'sent')]}"> + <i class="fa fa-paper-plane-o"/> E-Invoice sent, waiting for a response + </div> + <div class="alert alert-danger" role="alert" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}"> + <i class="fa fa-exclamation-triangle"/> E-Invoice check failed. You can modify the invoice, and resend it. + </div> + </xpath> + <xpath expr="//page[@name='other_info']" position="after"> + <page string="Electronic Invoicing"> + <group> + <group> + <field name="l10n_it_stamp_duty"/> + </group> + <group> + <field name="l10n_it_ddt_id"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="account_invoice_supplier_form_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.supplier.form.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_supplier_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='date_due']" position="replace"> + <field name="date_due" attrs="{'readonly': [('state', 'in', ['open', 'in_payment', 'paid'])]}" force_save="1"/> + </xpath> + <xpath expr="//page[@name='other_info']" position="after"> + <page string="Electronic Invoicing"> + <group> + <group> + <field name="l10n_it_stamp_duty"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="view_account_invoice_filter_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.select.l10n.it</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.view_account_invoice_filter"/> + <field name="arch" type="xml"> + <xpath expr="//filter[@name='late']" position="after"> + <separator/> + <filter name="error" string="E-invoice error" domain="[('l10n_it_send_state', '=', 'invalid')]"/> + <filter name="sent" string="E-invoice sent" domain="[('l10n_it_send_state', '=', 'sent')]"/> + <filter name="no_sent" string="E-invoice to send" domain="[('l10n_it_send_state', 'in', ['to_send',False])]"/> + <filter name="delivered" string="E-invoice delivered" domain="[('l10n_it_send_state', 'in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]"/> + </xpath> + <xpath expr="//filter[@name='status']" position="after"> + <filter name="send_status" string="Send status" context="{'group_by':'l10n_it_send_state'}"/> + </xpath> + </field> + </record> + + <record id="l10n_it_ddt" model="ir.ui.view"> + <field name="name">ddt.form.l10n.it</field> + <field name="model">l10n_it.ddt</field> + <field name="arch" type="xml"> + <form> + <group> + <field name="name"/> + <field name="date"/> + </group> + </form> + </field> + </record> + + <record id="l10n_it_ddt_list_view" model="ir.ui.view"> + <field name="name">l10n_it.ddt.list.view</field> + <field name="model">l10n_it.ddt</field> + <field name="arch" type="xml"> + <tree> + <field name="name"/> + <field name="date"/> + </tree> + </field> + </record> + + <record id="action_ddt_account" model="ir.actions.act_window"> + <field name="name">Transport Document</field> + <field name="res_model">l10n_it.ddt</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="l10n_it_ddt_list_view"/> + </record> + + <menuitem + name="DDT" + parent="account.account_account_menu" + action="action_ddt_account" + id="menu_action_ddt_account" + sequence="15" + groups="base.group_no_one"/> +</odoo> diff --git a/addons/l10n_mx/data/account_data.xml b/addons/l10n_mx/data/account_data.xml index 782597ee8ea..8176c32b5f3 100644 --- a/addons/l10n_mx/data/account_data.xml +++ b/addons/l10n_mx/data/account_data.xml @@ -11,6 +11,10 @@ <field name="name">IVA 16% </field> </record> + <record id="tax_group_iva_8" model="account.tax.group"> + <field name="name">IVA 8%</field> + </record> + <record id="tax_group_iva_ret_4" model="account.tax.group"> <field name="name">IVA Retencion 4%</field> </record> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index adda588dcbd..13f78068494 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -30,6 +30,10 @@ <field name="name">DIOT: 0%</field> <field name="applicability">taxes</field> </record> + <record id="tag_diot_8" model="account.account.tag"> + <field name="name">DIOT: 8%</field> + <field name="applicability">taxes</field> + </record> <record id="tag_diot_ret" model="account.account.tag"> <field name="name">DIOT: Retención</field> <field name="applicability">taxes</field> @@ -190,5 +194,35 @@ <field name="cash_basis_account_id" ref="cuenta118_01"/> <field name="tag_ids" eval="[(6,0,[ref('tag_diot_16')])]"/> </record> + + <record id="tax16" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) COMPRAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="account_id" ref="cuenta119_01"/> + <field name="refund_account_id" ref="cuenta119_01"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account_id" ref="cuenta118_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_diot_8')])]"/> + </record> + + <record id="tax17" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) VENTAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="account_id" ref="cuenta209_01"/> + <field name="refund_account_id" ref="cuenta209_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account_id" ref="cuenta208_01"/> + </record> </data> </odoo> diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index d1226711fd8..105cd650ac1 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -231,7 +231,9 @@ <td align="center" style="min-width: 590px;"> <t t-set="access_action" t-value="record.with_context(force_website=True).get_access_action()"/> <t t-set="is_online" t-value="access_action and access_action['type'] == 'ir.actions.act_url'"/> - <t t-set="access_url" t-value="is_online and record._get_share_url(redirect=True, signup_partner=True) or ''"/> + <t t-set="base_url" t-value="record.get_base_url()"/> + <t t-set="share_url" t-value="record._get_share_url(redirect=True, signup_partner=True)"/> + <t t-set="access_url" t-value="is_online and share_url and base_url + share_url or ''"/> <t t-set="access_name"> View <t t-esc="model_description or 'document'"/> </t> diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 2e91a4d9c05..5ba1fd08ea9 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -756,7 +756,11 @@ class Channel(models.Model): self.ensure_one() if self.channel_message_ids.ids: last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message - self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({ + channel_partner = self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]) + if channel_partner.seen_message_id.id == last_message_id: + # last message seen by user is already up-to-date + return + channel_partner.write({ 'seen_message_id': last_message_id, 'fetched_message_id': last_message_id, }) @@ -765,26 +769,37 @@ class Channel(models.Model): 'last_message_id': last_message_id, 'partner_id': self.env.user.partner_id.id, } - self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]]) + if self.channel_type == 'chat': + self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]]) + else: + data['channel_id'] = self.id + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), data) return last_message_id @api.multi def channel_fetched(self): """ Broadcast the channel_fetched notification to channel members + :param channel_ids : list of channel id that has been fetched by current user """ - self.ensure_one() - if not self.channel_message_ids.ids: - return - last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message - self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({ - 'fetched_message_id': last_message_id, - }) - data = { - 'info': 'channel_fetched', - 'last_message_id': last_message_id, - 'partner_id': self.env.user.partner_id.id, - } - self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]]) + for channel in self: + if not channel.channel_message_ids.ids: + return + if channel.channel_type != 'chat': + return + last_message_id = channel.channel_message_ids.ids[0] # zero is the index of the last message + channel_partner = self.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', self.env.user.partner_id.id)]) + if channel_partner.fetched_message_id.id == last_message_id: + # last message fetched by user is already up-to-date + return + channel_partner.write({ + 'fetched_message_id': last_message_id, + }) + data = { + 'info': 'channel_fetched', + 'last_message_id': last_message_id, + 'partner_id': self.env.user.partner_id.id, + } + self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', channel.id), data]]) @api.multi def channel_invite(self, partner_ids): diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 24b6a069d46..3c37988a5bb 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -559,7 +559,7 @@ class Message(models.Model): 'model': message.model, 'last_message_date': message.date, 'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg', - 'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids) + 'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids.sudo()) } failures_infos.append(info) return failures_infos diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 64db895b321..44c6cec5687 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -744,7 +744,9 @@ class MailThread(models.AbstractModel): access_link = self._notify_get_action_link('view') if message.model: - model_name = self.env['ir.model']._get(message.model).display_name + model = self.env['ir.model'].with_context( + lang=self.env.context.get('lang', self.env.user.lang)) + model_name = model._get(message.model).display_name view_title = _('View %s') % model_name else: view_title = _('View') diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 330e56b77b4..72971fbeee9 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -90,6 +90,7 @@ var Chatter = Widget.extend({ this._$topbar = this.$('.o_chatter_topbar'); if(!this._disableAttachmentBox) { this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', { + displayCounter: !!this.fields.thread, count: this.record.data.message_attachment_count || 0, })); } @@ -362,7 +363,9 @@ var Chatter = Widget.extend({ if (this._isAttachmentBoxOpen) { this._fetchAttachments().then(this._openAttachmentBox.bind(this)); } - this.trigger_up('reload', { fieldNames: ['message_attachment_count'] }); + if (this.fields.thread) { + this.trigger_up('reload', { fieldNames: ['message_attachment_count'] }); + } }, /** * @private @@ -490,7 +493,9 @@ var Chatter = Widget.extend({ }) .then(function () { self._reloadAttachmentBox(); - self.fields.thread.removeAttachments([ev.data.attachmentId]); + if (self.fields.thread) { + self.fields.thread.removeAttachments([ev.data.attachmentId]); + } self.trigger_up('reload'); }); } diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js index 7db3474ebb2..9f0f37264db 100644 --- a/addons/mail/static/src/js/models/threads/channel.js +++ b/addons/mail/static/src/js/models/threads/channel.js @@ -1,7 +1,6 @@ odoo.define('mail.model.Channel', function (require) { "use strict"; -var ChannelSeenMixin = require('mail.model.ChannelSeenMixin'); var SearchableThread = require('mail.model.SearchableThread'); var ThreadTypingMixin = require('mail.model.ThreadTypingMixin'); var mailUtils = require('mail.utils'); @@ -17,7 +16,7 @@ var time = require('web.time'); * Any piece of code in JS that make use of channels must ideally interact with * such objects, instead of direct data from the server. */ -var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, { +var Channel = SearchableThread.extend(ThreadTypingMixin, { /** * @override * @param {Object} params @@ -50,7 +49,6 @@ var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, { init: function (params) { var self = this; this._super.apply(this, arguments); - ChannelSeenMixin.init.apply(this, arguments); ThreadTypingMixin.init.apply(this, arguments); var data = params.data; @@ -387,24 +385,10 @@ var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, { */ _markAsRead: function () { var superDef = this._super.apply(this, arguments); - var seenDef = this._throttleNotifySeen(); + var seenDef = this._notifySeen(); return $.when(superDef, seenDef); }, /** - * @override {mail.model.ThreadSeenMixin} - * @private - * @returns {$.Promise} - */ - _notifyFetched: function () { - return this._rpc({ - model: 'mail.channel', - method: 'channel_fetched', - args: [[this._id]], - }, { - shadow: true - }); - }, - /** * @override {mail.model.ThreadTypingMixin} * @private * @param {Object} params @@ -420,20 +404,16 @@ var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, { }, { shadow: true }); }, /** - * @override {mail.model.ThreadSeenMixin} * @private * @returns {$.Promise<integer>} resolved with ID of last seen message */ _notifySeen: function () { var self = this; - this._cancelThrottledNotifyFetched(); return this._rpc({ model: 'mail.channel', method: 'channel_seen', args: [[this._id]], - }, { - shadow: true - }).then(function (lastSeenMessageID) { + }, { shadow: true }).then(function (lastSeenMessageID) { self._lastSeenMessageID = lastSeenMessageID; return lastSeenMessageID; }); diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js index fb524515193..c0ba9c527b0 100644 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -1,6 +1,7 @@ odoo.define('mail.model.DMChat', function (require) { "use strict"; +var ChannelSeenMixin = require('mail.model.ChannelSeenMixin'); var TwoUserChannel = require('mail.model.TwoUserChannel'); var core = require('web.core'); @@ -11,7 +12,7 @@ var _t = core._t; * Any piece of code in JS that make use of DMs must ideally interact with * such objects, instead of direct data from the server. */ -var DMChat = TwoUserChannel.extend({ +var DMChat = TwoUserChannel.extend(ChannelSeenMixin, { /** * @override * @param {Object} params @@ -27,6 +28,7 @@ var DMChat = TwoUserChannel.extend({ */ init: function (params) { this._super.apply(this, arguments); + ChannelSeenMixin.init.apply(this, arguments); var data = params.data; diff --git a/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js b/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js index 8a59db70aa2..20a5bba5407 100644 --- a/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js +++ b/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js @@ -1,8 +1,6 @@ odoo.define('mail.model.ChannelSeenMixin', function (require) { "use strict"; -var CCThrottleFunction = require('mail.model.CCThrottleFunction'); - var session = require('web.session'); /** @@ -25,12 +23,6 @@ var ChannelSeenMixin = { init: function (params) { this._seenPartnersInfo = params.data.seen_partners_info || []; - this._throttleNotifySeen = _.throttle(this._notifySeen.bind(this), 3000); - this._throttleNotifyFetched = CCThrottleFunction({ - duration: 3000, - func: this._notifyFetched.bind(this), - }); - this.on('message_added', this, this._onSeenMessageAdded); }, @@ -221,34 +213,6 @@ var ChannelSeenMixin = { }, //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - */ - _cancelThrottledNotifyFetched: function () { - this._throttleNotifyFetched.cancel(); - this._throttleNotifyFetched.clear(); - }, - /** - * @abstract - * @private - * @returns {$.Promise} - */ - _notifyFetched: function () { - return $.when(); - }, - /** - * @abstract - * @private - * @returns {$.Promise<integer>} resolved with ID of last seen message - */ - _notifySeen: function () { - return $.when(); - }, - - //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -256,7 +220,7 @@ var ChannelSeenMixin = { * @private */ _onSeenMessageAdded: function () { - this._throttleNotifyFetched(); + this.call('mail_service', 'notifyChannelFetched', { channelID: this._id }); }, }; diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 204a88bb8e9..5eb588d24e7 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -53,6 +53,12 @@ var MailManager = AbstractService.extend({ */ start: function () { this._super.apply(this, arguments); + + this._throttleNotifyChannelFetched = _.throttle(this._notifyChannelFetched.bind(this), 3000); + // List of channel that have requested the service to notify when + // it has been fetched, so that throttled RPCs batch the ids. + this._toNotifyChannelFetchedIDs = []; + this._initializeInternalState(); this._listenOnBuses(); this._fetchMailStateFromServer(); @@ -339,6 +345,14 @@ var MailManager = AbstractService.extend({ } }, /** + * @param {Object} data + * @param {integer} data.channelID + */ + notifyChannelFetched: function (data) { + this._toNotifyChannelFetchedIDs = _.uniq(this._toNotifyChannelFetchedIDs.concat(data.channelID)); + this._throttleNotifyChannelFetched(); + }, + /** * @param {integer|string} threadID */ openThread: function (threadID) { @@ -919,6 +933,23 @@ var MailManager = AbstractService.extend({ return new Message(this, data); }, /** + * @private + * @returns {$.Promise} + */ + _notifyChannelFetched: function () { + var channelIDs = this._toNotifyChannelFetchedIDs; + this._toNotifyChannelFetchedIDs = []; + if (_.isEmpty(channelIDs)) { + // no channel to notify fetched + return $.when(); + } + return this._rpc({ + model: 'mail.channel', + method: 'channel_fetched', + args: [channelIDs], + }, { shadow: true }); + }, + /** * shows a popup to notify a new received message. * This will also rename the browser tab if this is not the active tab. * diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index b77fa3e65fd..fb3527b2b90 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -80,7 +80,9 @@ MailManager.include({ if (!channel) { return; } - channel.updateSeenPartnersInfo(data); + if (channel.hasSeenFeature()) { + channel.updateSeenPartnersInfo(data); + } }, /** * Called when a new or updated message is received on a channel @@ -126,7 +128,9 @@ MailManager.include({ if (!channel) { return; } - channel.updateSeenPartnersInfo(data); + if (channel.hasSeenFeature()) { + channel.updateSeenPartnersInfo(data); + } if (session.partner_id !== data.partner_id) { return; } @@ -236,6 +240,27 @@ MailManager.include({ } }, /** + * Called when receiving a multi_user_channel seen notification. Only + * the current user is notified. This must be handled as if this is a + * channel seen notification. + * + * Note that this is a 'res.partner' notification because only the current + * user is notified on channel seen. This is a consequence from disabling + * the seen feature on multi_user_channel, because we still need to get + * the last seen message ID in order to display the "New Messages" separator + * in Discuss. + * + * @private + * @param {Object} data + * @param {integer} data.channel_id + * @param {string} data.info 'channel_seen' + * @param {integer} data.last_message_id + * @param {integer} data.partner_id + */ + _handlePartnerChannnelSeenNotification: function (data) { + this._handleChannelSeenNotification(data.channel_id, data); + }, + /** * Add or remove failure when receiving a failure update message * * @private @@ -387,6 +412,8 @@ MailManager.include({ this._handlePartnerMailFailureNotification(data); } else if (data.type === 'user_connection') { this._handlePartnerUserConnectionNotification(data); + } else if (data.info === 'channel_seen') { + this._handlePartnerChannnelSeenNotification(data); } else { this._handlePartnerChannelNotification(data); } diff --git a/addons/mail/static/src/js/thread_widget.js b/addons/mail/static/src/js/thread_widget.js index 2e2b7e222c6..cbac2c3b417 100644 --- a/addons/mail/static/src/js/thread_widget.js +++ b/addons/mail/static/src/js/thread_widget.js @@ -103,6 +103,7 @@ var ThreadWidget = Widget.extend({ this._messageSeenPopover.popover('hide'); } this._destroyOpenSeenPopoverIDs(); + this._super(); }, /** * @param {mail.model.AbstractThread} thread the thread to render. diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js index 11a7f8a7926..e5496e0bc3c 100644 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ b/addons/mail/static/src/js/thread_windows/thread_window.js @@ -402,6 +402,9 @@ var ThreadWindow = AbstractThreadWindow.extend({ * @param {integer} channelID */ _onUpdateChannel: function (channelID) { + if (!this.hasThread()) { + return; + } if (this._thread.getID() !== channelID) { return; } diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss index 84a826922ff..04ffee37478 100644 --- a/addons/mail/static/src/scss/thread.scss +++ b/addons/mail/static/src/scss/thread.scss @@ -294,7 +294,7 @@ opacity: 0.6; &.o_all_seen { - color: $o-enterprise-primary-color; + color: $o-enterprise-color; } &:hover { @@ -308,7 +308,7 @@ .fa-check:nth-child(2) { position: absolute; - top: 1px; + top: -1px; left: 0px; } } diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index 7f8629f1bae..7fe1eeebd77 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -105,11 +105,17 @@ </t> + <!-- + Chatter attachment paperclip button. + + @param {boolean} [displayCounter] + @param {integer} [count] + --> <t t-name="mail.chatter.Attachment.Button"> <button class="btn btn-link o_chatter_button_attachment" title="View all the attachments of the current record"> <span> <i class="fa fa-paperclip"/> - <span class="o_chatter_attachment_button_count"> + <span t-if="displayCounter" class="o_chatter_attachment_button_count"> <t t-esc="count"/> </span> </span> diff --git a/addons/mail/static/tests/discuss_seen_indicator_tests.js b/addons/mail/static/tests/discuss_seen_indicator_tests.js index 41fd6a508d6..4db61a56ed1 100644 --- a/addons/mail/static/tests/discuss_seen_indicator_tests.js +++ b/addons/mail/static/tests/discuss_seen_indicator_tests.js @@ -80,10 +80,11 @@ QUnit.test('2 members: 1 myself message not received initially', function (asser assert.expect(2); var done = assert.async(); - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -106,12 +107,12 @@ QUnit.test('2 members: 1 myself message not received initially', function (asser this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [3, "Me"], @@ -130,10 +131,10 @@ QUnit.test('2 members: 1 myself message not received initially', function (asser services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); @@ -147,10 +148,11 @@ QUnit.test('2 members: 1 myself message received by everyone initially', functio assert.expect(4); var done = assert.async(); - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -173,12 +175,12 @@ QUnit.test('2 members: 1 myself message received by everyone initially', functio this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [3, "Me"], @@ -197,10 +199,10 @@ QUnit.test('2 members: 1 myself message received by everyone initially', functio services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); assert.isVisible($seenIcon, "should display seen icon on message"); assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); @@ -219,10 +221,11 @@ QUnit.test('2 members: 1 myself message seen by everyone initially', function (a assert.expect(4); var done = assert.async(); - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -245,12 +248,12 @@ QUnit.test('2 members: 1 myself message seen by everyone initially', function (a this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [3, "Me"], @@ -269,10 +272,10 @@ QUnit.test('2 members: 1 myself message seen by everyone initially', function (a services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); assert.isVisible($seenIcon, "should display seen icon on message"); assert.containsN($seenIcon, '.fa-check', 2, "should display 2 checks"); @@ -292,10 +295,11 @@ QUnit.test('2 members: 1 myself message received by everyone (initially not rece var done = assert.async(); var self = this; - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -318,12 +322,12 @@ QUnit.test('2 members: 1 myself message received by everyone (initially not rece this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [3, "Me"], @@ -342,10 +346,10 @@ QUnit.test('2 members: 1 myself message received by everyone (initially not rece services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); @@ -377,10 +381,11 @@ QUnit.test('2 members: 1 myself message seen by everyone (initially not received var done = assert.async(); var self = this; - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -403,12 +408,12 @@ QUnit.test('2 members: 1 myself message seen by everyone (initially not received this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [3, "Me"], @@ -427,10 +432,10 @@ QUnit.test('2 members: 1 myself message seen by everyone (initially not received services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); @@ -462,10 +467,11 @@ QUnit.test('2 members: 1 other message seen by me (initially not received)', fun var done = assert.async(); var self = this; - var general = { + var dmChat = { id: 1, - channel_type: "channel", - name: "general", + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', members: [{ id: 2, name: "Someone", @@ -488,12 +494,12 @@ QUnit.test('2 members: 1 other message seen by me (initially not received)', fun this.data.initMessaging = { channel_slots: { - channel_channel: [general], + channel_direct_message: [dmChat], }, }; this.data['mail.channel'] = { fields: {}, - records: [general], + records: [dmChat], }; this.data['mail.message'].records = [{ author_id: [2, "Other"], @@ -512,10 +518,10 @@ QUnit.test('2 members: 1 other message seen by me (initially not received)', fun services: this.services, session: { partner_id: 3 }, }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); + testUtils.dom.click($dmChat); assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); @@ -535,7 +541,11 @@ QUnit.test('2 members: 1 other message seen by me (initially not received)', fun }); }); -QUnit.test('3 members: 1 myself message received by some initially', function (assert) { +QUnit.skip('3 members: 1 myself message received by some initially', function (assert) { + // IMPORTANT: This test make sense if this feature is on multi user channels, + // which was the case in case at some point. It has been disabled for + // performance reasons. We are unsure if this will come back in the future, + // so we keep to tests in 'skip' mode in the mean time. assert.expect(4); var done = assert.async(); @@ -624,7 +634,12 @@ QUnit.test('3 members: 1 myself message received by some initially', function (a }); }); -QUnit.test('several members: 1 myself message seen by everyone (initially not received)', function (assert) { +QUnit.skip('several members: 1 myself message seen by everyone (initially not received)', function (assert) { + // IMPORTANT: This test make sense if this feature is on multi user channels, + // which was the case in case at some point. It has been disabled for + // performance reasons. We are unsure if this will come back in the future, + // so we keep to tests in 'skip' mode in the mean time. + // seen icon and content should change for my messages based on other users // actions (received and/or seen the message) assert.expect(26); @@ -826,7 +841,12 @@ QUnit.test('several members: 1 myself message seen by everyone (initially not re }); }); -QUnit.test('several members: other message seen by everyone (initially not received)', function (assert) { +QUnit.skip('several members: other message seen by everyone (initially not received)', function (assert) { + // IMPORTANT: This test make sense if this feature is on multi user channels, + // which was the case in case at some point. It has been disabled for + // performance reasons. We are unsure if this will come back in the future, + // so we keep to tests in 'skip' mode in the mean time. + // seen icon should never be visible for message of others assert.expect(9); var done = assert.async(); @@ -989,7 +1009,11 @@ QUnit.test('several members: other message seen by everyone (initially not recei }); }); -QUnit.test('several members: only show seen icons from last message seen by everyone', function (assert) { +QUnit.skip('several members: only show seen icons from last message seen by everyone', function (assert) { + // IMPORTANT: This test make sense if this feature is on multi user channels, + // which was the case in case at some point. It has been disabled for + // performance reasons. We are unsure if this will come back in the future, + // so we keep to tests in 'skip' mode in the mean time. assert.expect(12); var done = assert.async(); diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js index 8f4851ada0d..00abb2d0503 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js @@ -586,10 +586,18 @@ QUnit.test('grouped preview for needaction messages linked to same document', fu messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: channel seen notification", function ( assert ) { +QUnit.test("messaging menu widget: channel seen notification", function (assert) { assert.expect(4); - this.data.initMessaging.channel_slots.channel_channel[0].message_unread_counter = 1; + this.data.initMessaging.channel_slots = { + channel_direct_message: [{ + id: 1, + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', + message_unread_counter: 1, + }], + }; var messagingMenu = new MessagingMenu(); testUtils.mock.addMockEnvironment(messagingMenu, { @@ -605,7 +613,7 @@ QUnit.test("messaging menu widget: channel seen notification", function ( assert "should have correct messaging menu counter (1 unread message in channel)"); assert.strictEqual(messagingMenu.$('.o_preview_counter').text().replace(/\s/g, ''), '(1)', - "should display 1 unread message on general channel preview"); + "should display 1 unread message on DM channel preview"); // Simulate received channel seen notification var message = { @@ -621,7 +629,51 @@ QUnit.test("messaging menu widget: channel seen notification", function ( assert assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should no longer have a messaging menu counter (no unread message in channel)"); assert.strictEqual(messagingMenu.$('.o_preview_counter').text().replace(/\s/g, ''), '', - "should no longer display unread message on general channel preview"); + "should no longer display unread message on DM channel preview"); + + messagingMenu.destroy(); +}); + +QUnit.test("messaging menu widget: no traceback when receiving channel_fetched notification with blank thread window", function (assert) { + assert.expect(3); + + this.data.initMessaging.channel_slots = { + channel_direct_message: [{ + id: 1, + channel_type: "chat", + direct_partner: [{ id: 2, name: 'Someone', im_status: '' }], + name: 'DM', + message_unread_counter: 1, + }], + }; + + var messagingMenu = new MessagingMenu(); + testUtils.mock.addMockEnvironment(messagingMenu, { + services: this.services, + data: this.data, + session: { partner_id: 3 }, + }); + + messagingMenu.appendTo($('#qunit-fixture')); + messagingMenu.$('.dropdown-toggle').click(); + assert.containsOnce(messagingMenu, '.o_new_message', + "should have button to open blank thread window"); + + testUtils.dom.click(messagingMenu.$('.o_new_message')); + var $threadWindow = $('.o_thread_window'); + assert.strictEqual($threadWindow.length, 1, "should have an open thread window"); + assert.ok($threadWindow.hasClass('o_thread_less'), "should be a blank thread window"); + + // Simulate received channel fetched notification + var message = { + info: 'channel_fetched', + last_message_id: 1, + partner_id: 2, + }; + var notifications = [ + [['myDB', 'mail.channel', 1], message] + ]; + messagingMenu.call('bus_service', 'trigger', 'notification', notifications); messagingMenu.destroy(); }); diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index d660c6b624b..c83f52f68c6 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -729,6 +729,12 @@ class MassMailing(models.Model): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() + res_ids = self.get_recipients() + except_mailed = self.env['mail.mail.statistics'].search([ + ('model', '=', self.mailing_model_real), + ('res_id', 'in', res_ids), + ('exception', '!=', False), + ('mass_mailing_id', '=', self.id)]).unlink() self.write({'state': 'in_queue'}) def action_view_sent(self): diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index be92616c509..97c9ebadb5f 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -238,7 +238,7 @@ class MrpWorkcenterProductivity(models.Model): d2 = fields.Datetime.from_string(blocktime.date_end) diff = d2 - d1 if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.resource_calendar_id: - r = blocktime.workcenter_id.get_work_days_data(d1, d2)['hours'] + r = blocktime.workcenter_id._get_work_days_data(d1, d2)['hours'] blocktime.duration = round(r * 60, 2) else: blocktime.duration = round(diff.total_seconds() / 60.0, 2) diff --git a/addons/note/static/src/js/systray_activity_menu.js b/addons/note/static/src/js/systray_activity_menu.js index e2e484ca10f..f8a5e8e288e 100644 --- a/addons/note/static/src/js/systray_activity_menu.js +++ b/addons/note/static/src/js/systray_activity_menu.js @@ -118,7 +118,7 @@ ActivityMenu.include({ _onNoteDateTimeSetClick: function (ev) { ev.preventDefault(); ev.stopPropagation(); - this.noteDateTimeWidget.$input.click(); + this.noteDateTimeWidget.$input.select(); }, /** * Saving note (quick create) and updating activity preview diff --git a/addons/note/static/tests/systray_activity_menu_tests.js b/addons/note/static/tests/systray_activity_menu_tests.js index 8c4e2dde60a..151af26dfbc 100644 --- a/addons/note/static/tests/systray_activity_menu_tests.js +++ b/addons/note/static/tests/systray_activity_menu_tests.js @@ -108,7 +108,6 @@ QUnit.test('note activity menu widget: create note from activity menu', function // creating quick note with date testUtils.dom.click(activityMenu.$('.o_note_show')); activityMenu.$('input.o_note_input').val("New Note"); - testUtils.dom.click(activityMenu.$('.o_note_set_datetime')); testUtils.dom.click(activityMenu.$(".o_note_save")); assert.strictEqual(activityMenu.$('.o_notification_counter').text(), '2', "should increment activity notification counter after creating a second note"); diff --git a/addons/payment/data/payment_acquirer_data.xml b/addons/payment/data/payment_acquirer_data.xml index 4b4aa5f400e..27d27ea4258 100644 --- a/addons/payment/data/payment_acquirer_data.xml +++ b/addons/payment/data/payment_acquirer_data.xml @@ -265,26 +265,4 @@ ref("payment.payment_icon_cc_codensa_easy_credit")])]'/> </record> - <record id="payment_acquirer_ccavenue" model="payment.acquirer"> - <field name="name">CCAvenue</field> - <field name="image" type="base64" file="payment_ccavenue/static/src/img/ccavenue_icon.png"/> - <field name="view_template_id" ref="default_acquirer_button"/> - <field name="module_id" ref="base.module_payment_ccavenue"/> - <field name="description" type="html"> - <p> - CCAvenue provides complete, simple and secure online payment gateway services and e-business solutions to Indian websites, with real time Credit Card, Debit Card and Net Banking. - </p> - <ul class="list-inline"> - <li class="list-inline-item"><i class="fa fa-check"/>Online Payment</li> - <li class="list-inline-item"><i class="fa fa-check"/>Payment Status Tracking</li> - </ul> - </field> - <!-- https://www.ccavenue.com/widest_range_of_payment_options.jsp# --> - <field name="payment_icon_ids" eval='[(6, 0, [ref("payment.payment_icon_cc_mastercard"), - ref("payment.payment_icon_cc_diners_club_intl"), - ref("payment.payment_icon_cc_jcb"), - ref("payment.payment_icon_cc_american_express"), - ref("payment.payment_icon_cc_visa")])]'/> - </record> - </odoo> diff --git a/addons/payment_ccavenue/__manifest__.py b/addons/payment_ccavenue/__manifest__.py deleted file mode 100644 index ac51c2d7891..00000000000 --- a/addons/payment_ccavenue/__manifest__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -{ - 'name': 'CCAvenue Payment Acquirer', - 'category': 'Accounting', - 'summary': 'Payment Acquirer: CCAvenue Implementation', - 'description': """CCAvenue Payment Acquirer""", - 'depends': ['payment'], - 'external_dependencies': {'python': ['Crypto']}, - 'data': [ - 'views/payment_acquirer.xml', - 'views/payment_ccavenue_templates.xml', - 'data/payment_ccavenue_data.xml', - ], - 'post_init_hook': 'create_missing_journal_for_acquirers', -} diff --git a/addons/payment_ccavenue/controllers/main.py b/addons/payment_ccavenue/controllers/main.py deleted file mode 100644 index 957df9864b5..00000000000 --- a/addons/payment_ccavenue/controllers/main.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import logging -import pprint -import werkzeug - -from odoo import http -from odoo.http import request - - -_logger = logging.getLogger(__name__) - - -class CCAvenueController(http.Controller): - _return_url = '/payment/ccavenue/return/' - _cancel_url = '/payment/ccavenue/cancel/' - - @http.route(['/payment/ccavenue/return', '/payment/ccavenue/cancel'], type='http', auth='public', csrf=False) - def ccavenue_return(self, **post): - _logger.info('CCAvenue: Entering form_feedback with post data %s', pprint.pformat(post)) - if post: - request.env['payment.transaction'].sudo().form_feedback(post, 'ccavenue') - return werkzeug.utils.redirect('/payment/process') diff --git a/addons/payment_ccavenue/data/payment_ccavenue_data.xml b/addons/payment_ccavenue/data/payment_ccavenue_data.xml deleted file mode 100644 index 63220392c8a..00000000000 --- a/addons/payment_ccavenue/data/payment_ccavenue_data.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo noupdate="1"> - <record id="payment.payment_acquirer_ccavenue" model="payment.acquirer"> - <field name="provider">ccavenue</field> - <field name="company_id" ref="base.main_company"/> - <field name="view_template_id" ref="ccavenue_form"/> - <field name="environment">test</field> - <field name="pre_msg"><![CDATA[ - <p>You will be redirected to the CCAvenue website after clicking on the payment button.</p>]]> - </field> - <field name="ccavenue_merchant_id">dummy</field> - <field name="ccavenue_access_code">dummy</field> - <field name="ccavenue_working_key">dummy</field> - </record> -</odoo> diff --git a/addons/payment_ccavenue/models/payment.py b/addons/payment_ccavenue/models/payment.py deleted file mode 100755 index 2f3ed126548..00000000000 --- a/addons/payment_ccavenue/models/payment.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import logging - -from binascii import hexlify, unhexlify -from hashlib import md5 -from werkzeug import urls - -from odoo import api, fields, models, _ -from odoo.addons.payment.models.payment_acquirer import ValidationError -from odoo.addons.payment_ccavenue.controllers.main import CCAvenueController -from odoo.tools.float_utils import float_compare - - -_logger = logging.getLogger(__name__) - -try: - from Crypto.Cipher import AES -except ImportError: - _logger.info('pyCrypto library not found. If you plan to use payment_ccavenue, please install the library from https://pypi.python.org/pypi/pyCrypto') - - -class PaymentAcquirer(models.Model): - _inherit = 'payment.acquirer' - - provider = fields.Selection(selection_add=[('ccavenue', 'CCAvenue')]) - ccavenue_merchant_id = fields.Char(string='CCAvenue Merchant ID', required_if_provider='ccavenue', groups='base.group_user') - ccavenue_access_code = fields.Char(string='Access Code', required_if_provider='ccavenue', groups='base.group_user') - ccavenue_working_key = fields.Char(string='Working Key', required_if_provider='ccavenue', groups='base.group_user') - - def _get_ccavenue_urls(self, environment): - """ CCAvenue URLs""" - if environment == 'prod': - return {'ccavenue_form_url': 'https://secure.ccavenue.com/transaction/transaction.do?command=initiateTransaction'} - else: - return {'ccavenue_form_url': 'https://test.ccavenue.com/transaction/transaction.do?command=initiateTransaction'} - - def _ccavenue_pad(self, data): - length = 16 - (len(data) % 16) - data += chr(length) * length - return data - - @api.multi - def _ccavenue_encrypt_text(self, plaintext): - self.ensure_one() - iv = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - plaintext = self._ccavenue_pad(plaintext) - dec_digest = md5(self.ccavenue_working_key.encode('utf-8')).digest() - enc_cipher = AES.new(dec_digest, AES.MODE_CBC, iv) - encrypted_text = hexlify(enc_cipher.encrypt(plaintext)).decode('utf-8') - return encrypted_text - - @api.multi - def _ccavenue_decrypt_text(self, ciphertext): - self.ensure_one() - iv = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - dec_digest = md5(self.ccavenue_working_key.encode('utf-8')).digest() - dec_cipher = AES.new(dec_digest, AES.MODE_CBC, iv) - decrypted_text = dec_cipher.decrypt(unhexlify(ciphertext)).decode('utf-8') - return decrypted_text - - @api.multi - def ccavenue_form_generate_values(self, values): - self.ensure_one() - base_url = self.env['ir.config_parameter'].get_param('web.base.url') - ccavenue_values = dict(access_code=self.ccavenue_access_code, - merchant_id=self.ccavenue_merchant_id, - order_id=values.get('reference'), - currency=values.get('currency').name, - amount=values.get('amount'), - redirect_url=urls.url_join(base_url, CCAvenueController._return_url), - cancel_url=urls.url_join(base_url, CCAvenueController._cancel_url), - language='EN', - customer_identifier=values.get('partner_email'), - delivery_name=values.get('partner_name'), - delivery_address=values.get('partner_address'), - delivery_city=values.get('partner_city'), - delivery_state=values.get('partner_state').name, - delivery_zip=values.get('partner_zip'), - delivery_country=values.get('partner_country').name, - delivery_tel=values.get('partner_phone'), - billing_name=values.get('billing_partner_name'), - billing_address=values.get('billing_partner_address'), - billing_city=values.get('billing_partner_city'), - billing_state=values.get('billing_partner_state').name, - billing_zip=values.get('billing_partner_zip'), - billing_country=values.get('billing_partner_country').name, - billing_tel=values.get('billing_partner_phone'), - billing_email=values.get('billing_partner_email'), - ) - ccavenue_values['encRequest'] = self._ccavenue_encrypt_text(urls.url_encode(ccavenue_values)) - return ccavenue_values - - @api.multi - def ccavenue_get_form_action_url(self): - self.ensure_one() - return self._get_ccavenue_urls(self.environment)['ccavenue_form_url'] - - -class PaymentTransaction(models.Model): - _inherit = 'payment.transaction' - - # Override form_feedback - @api.model - def form_feedback(self, data, acquirer_name): - if acquirer_name == 'ccavenue': - acquirer = self.env['payment.acquirer'].search([('provider', '=', 'ccavenue')], limit=1) - data = acquirer._ccavenue_decrypt_text(data.get('encResp')) - data = urls.url_decode(data) - return super(PaymentTransaction, self).form_feedback(data=data, acquirer_name=acquirer_name) - - @api.model - def _ccavenue_form_get_tx_from_data(self, data): - """ Given a data dict coming from ccavenue, verify it and find the related - transaction record. """ - reference = data.get('order_id') - if not reference: - raise ValidationError(_('CCAvenue: received data with missing reference (%s)') % (reference)) - - transaction = self.search([('reference', '=', reference)]) - if not transaction or len(transaction) > 1: - error_msg = _('CCAvenue: received data for reference %s') % (reference) - if not transaction: - error_msg += _('; no order found') - else: - error_msg += _('; multiple order found') - raise ValidationError(error_msg) - return transaction - - @api.model - def _ccavenue_form_get_invalid_parameters(self, data): - invalid_parameters = [] - if self.acquirer_reference and data.get('order_id') != self.acquirer_reference: - invalid_parameters.append( - ('Transaction Id', data.get('order_id'), self.acquirer_reference)) - # check what is buyed - if float_compare(float(data.get('amount', '0.0')), self.amount, precision_digits=2) != 0: - invalid_parameters.append(('Amount', data.get('amount'), '%.2f' % self.amount)) - return invalid_parameters - - @api.model - def _ccavenue_form_validate(self, data): - if self.state == 'done': - _logger.warning('CCAvenue: trying to validate an already validated tx (ref %s)' % self.reference) - return True - status_code = data.get('order_status') - if status_code == "Success": - _logger.info('Validated CCAvenue payment for tx %s: set as done' % (self.reference)) - self.write({'acquirer_reference': data.get('tracking_id')}) - self._set_transaction_done() - return True - elif status_code == "Aborted": - _logger.info('Aborted CCAvenue payment for tx %s: set as cancel' % (self.reference)) - self.write({'acquirer_reference': data.get('tracking_id'), 'state_message': data.get('status_message')}) - self._set_transaction_cancel() - return False - else: - error = data.get('failure_message') or data.get('status_message') - _logger.info(error) - self.write({'acquirer_reference': data.get('tracking_id')}) - self._set_transaction_error(error) - return False diff --git a/addons/payment_ccavenue/static/description/icon.png b/addons/payment_ccavenue/static/description/icon.png Binary files differdeleted file mode 100644 index 368376d3538..00000000000 --- a/addons/payment_ccavenue/static/description/icon.png +++ /dev/null diff --git a/addons/payment_ccavenue/static/src/img/ccavenue_icon.png b/addons/payment_ccavenue/static/src/img/ccavenue_icon.png Binary files differdeleted file mode 100644 index 202f99e923f..00000000000 --- a/addons/payment_ccavenue/static/src/img/ccavenue_icon.png +++ /dev/null diff --git a/addons/payment_ccavenue/views/payment_acquirer.xml b/addons/payment_ccavenue/views/payment_acquirer.xml deleted file mode 100644 index dc659aff802..00000000000 --- a/addons/payment_ccavenue/views/payment_acquirer.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <record id="payment_acquirer_form_ccavenue" model="ir.ui.view"> - <field name="name">payment.acquirer.form.inherit</field> - <field name="model">payment.acquirer</field> - <field name="inherit_id" ref="payment.acquirer_form"/> - <field name="arch" type="xml"> - <xpath expr='//group[@name="acquirer"]' position='after'> - <group attrs="{'invisible': [('provider', '!=', 'ccavenue')]}"> - <field name="ccavenue_merchant_id"/> - <field name="ccavenue_access_code" password="True"/> - <field name="ccavenue_working_key" password="True"/> - </group> - </xpath> - </field> - </record> -</odoo> diff --git a/addons/payment_ccavenue/views/payment_ccavenue_templates.xml b/addons/payment_ccavenue/views/payment_ccavenue_templates.xml deleted file mode 100644 index 9785b43123b..00000000000 --- a/addons/payment_ccavenue/views/payment_ccavenue_templates.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <template id="ccavenue_form"> - <div> - <input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/> - <input type="hidden" name="encRequest" t-att-value="encRequest"/> - <input type="hidden" name="access_code" t-att-value="access_code"/> - </div> - </template> -</odoo> diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index e7a78a7d8c0..1101c9137c1 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -503,13 +503,11 @@ class PosOrder(models.Model): aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) - # Reconcile returns first - # to avoid mixing up the credit of a payment and the credit of a return - # in the receivable account - aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit)) try: - aml_returns.reconcile() - (aml - aml_returns).reconcile() + # Cash returns will be well reconciled + # Whereas freight returns won't be + # "c'est la vie..." + aml.reconcile() except Exception: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index f6bcb05d656..f5d1dc707d4 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -184,6 +184,8 @@ class PosSession(models.Model): journals = Journal.with_context(ctx).search([('type', '=', 'cash'), ('company_id', '=', pos_config.company_id.id)]) if not journals: journals = Journal.with_context(ctx).search([('journal_user', '=', True), ('company_id', '=', pos_config.company_id.id)]) + if not journals: + raise ValidationError(_("No payment method configured! \nEither no Chart of Account is installed or no payment method is configured for this POS.")) journals.sudo().write({'journal_user': True}) pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index a0626326b62..b1d99e2e199 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -383,7 +383,7 @@ var Gui = core.Class.extend({ numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; - var newbuf_float = field_utils.parse.float(newbuf); + var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf); var decimal_point = _t.database.parameters.decimal_point; if (input === decimal_point) { @@ -403,7 +403,9 @@ var Gui = core.Class.extend({ newbuf = newbuf.substring(1,newbuf.length); } } else if (input === '-') { - if ( newbuf[0] === '-' ) { + if (options.firstinput) { + newbuf = '-'; + } else if ( newbuf[0] === '-' ) { newbuf = newbuf.substring(1,newbuf.length); } else { newbuf = '-' + newbuf; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index b853df13549..b0f5ac10dfa 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -815,8 +815,8 @@ var ProductListWidget = PosBaseWidget.extend({ }; this.keypress_product_handler = function(ev){ - if (ev.which != 13 && ev.which != 32) { - // Key is not space or enter + // React only to SPACE to avoid interfering with warcode scanner which sends ENTER + if (ev.which != 32) { return; } ev.preventDefault(); diff --git a/addons/point_of_sale/views/pos_templates.xml b/addons/point_of_sale/views/pos_templates.xml index c2721c17e55..dfadf1af349 100644 --- a/addons/point_of_sale/views/pos_templates.xml +++ b/addons/point_of_sale/views/pos_templates.xml @@ -1,5 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> +<template id="pos_assets_backend" name="POS UI Backend Assets" inherit_id="web.assets_backend" primary="True"> + <xpath expr="//script[contains(@src, '/mail/static/src/js/services/mail_service.js')]" position="replace"></xpath> +</template> + <template id="index" name="POS Index"><!DOCTYPE html> <html> <head> @@ -31,7 +35,7 @@ </script> <t t-call-assets="web.assets_common" t-css="false"/> - <t t-call-assets="web.assets_backend" t-css="false"/> + <t t-call-assets="point_of_sale.pos_assets_backend" t-css="false"/> <t t-call-assets="point_of_sale.assets"/> <script type="text/javascript" id="loading-script" t-raw="init"> diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py index 71d8f970ac2..343c2b67809 100644 --- a/addons/portal/models/portal_mixin.py +++ b/addons/portal/models/portal_mixin.py @@ -33,6 +33,16 @@ class PortalMixin(models.AbstractModel): self.sudo().write({'access_token': str(uuid.uuid4())}) return self.access_token + @api.multi + def get_base_url(self): + """Get the base URL for the current model. + + Defined here to be overriden by website specific models. + The method has to be public because it is called from mail templates. + """ + self.ensure_one() + return self.env['ir.config_parameter'].sudo().get_param('web.base.url') + def _get_share_url(self, redirect=False, signup_partner=False, pid=None): """ Build the url of the record that will be sent by mail and adds additional parameters such as diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py index 5905df4eed4..8474f2c810c 100644 --- a/addons/portal/wizard/portal_share.py +++ b/addons/portal/wizard/portal_share.py @@ -10,9 +10,10 @@ class PortalShare(models.TransientModel): @api.model def default_get(self, fields): result = super(PortalShare, self).default_get(fields) + record = self.env[result['res_model']].browse(result['res_id']) result['res_model'] = self._context.get('active_model') result['res_id'] = self._context.get('active_id') - result['share_link'] = self.env[result['res_model']].browse(result['res_id'])._get_share_url(redirect=True) + result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True) return result res_model = fields.Char('Related Document Model', required=True) @@ -24,12 +25,11 @@ class PortalShare(models.TransientModel): @api.depends('res_model', 'res_id') def _compute_share_link(self): - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for rec in self: res_model = self.env[rec.res_model] if isinstance(res_model, self.pool['portal.mixin']): record = res_model.browse(rec.res_id) - rec.share_link = base_url + record._get_share_url(redirect=True) + rec.share_link = record.get_base_url() + record._get_share_url(redirect=True) @api.depends('res_model', 'res_id') def _compute_access_warning(self): @@ -52,7 +52,7 @@ class PortalShare(models.TransientModel): partner_ids = self.partner_ids.filtered(lambda x: x.user_ids) # if partner already user or record has access token send common link in batch to all user for partner in self.partner_ids: - share_link = active_record._get_share_url(redirect=True, pid=partner.id) + share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id) active_record.with_context(mail_post_autofollow=True).message_post_with_view(template, values={'partner': partner, 'note': self.note, 'record': active_record, 'share_link': share_link}, @@ -67,7 +67,7 @@ class PortalShare(models.TransientModel): share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id] active_record.with_context(mail_post_autofollow=True).message_post_with_view(template, values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link }, + 'share_link': share_link}, subject=_("You are invited to access %s" % active_record.display_name), subtype_id=note.id, notif_layout='mail.mail_notification_light', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index f2943b533a2..e2b9510c1d8 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -268,11 +268,18 @@ class Project(models.Model): """ copy and map tasks from old to new project """ tasks = self.env['project.task'] # We want to copy archived task, but do not propagate an active_test context key - task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)]).ids + task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)], order='parent_id').ids + old_to_new_tasks = {} for task in self.env['project.task'].browse(task_ids): # preserve task name and stage, normally altered during copy defaults = self._map_tasks_default_valeus(task) - tasks += task.copy(defaults) + if task.parent_id: + # set the parent to the duplicated task + defaults['parent_id'] = old_to_new_tasks[task.parent_id.id] + new_task = task.copy(defaults) + old_to_new_tasks[task.id] = new_task.id + tasks += new_task + return self.browse(new_project_id).write({'tasks': [(6, 0, tasks.ids)]}) @api.multi @@ -283,6 +290,8 @@ class Project(models.Model): if not default.get('name'): default['name'] = _("%s (copy)") % (self.name) project = super(Project, self).copy(default) + if self.subtask_project_id == self: + project.subtask_project_id = project for follower in self.message_follower_ids: project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) if 'tasks' not in default: @@ -655,6 +664,7 @@ class Task(models.Model): # force some parent values, if needed if 'parent_id' in vals and vals['parent_id']: vals.update(self._subtask_values_from_parent(vals['parent_id'])) + context.pop('default_parent_id', None) # for default stage if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') diff --git a/addons/project/models/res_partner.py b/addons/project/models/res_partner.py index a21370e52ed..64f7b685083 100644 --- a/addons/project/models/res_partner.py +++ b/addons/project/models/res_partner.py @@ -15,4 +15,4 @@ class ResPartner(models.Model): fetch_data = self.env['project.task'].read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) result = dict((data['partner_id'][0], data['partner_id_count']) for data in fetch_data) for partner in self: - partner.task_count = result.get(partner.id, 0) + partner.task_count = result.get(partner.id, 0) + sum(c.task_count for c in partner.child_ids) diff --git a/addons/project/static/src/js/tour.js b/addons/project/static/src/js/tour.js index 82ed66a15b9..951390455b9 100644 --- a/addons/project/static/src/js/tour.js +++ b/addons/project/static/src/js/tour.js @@ -73,11 +73,6 @@ tour.register('project_tour', { }, { trigger: ".o_kanban_record .oe_kanban_content", extra_trigger: '.o_kanban_project_tasks', - content: _t("Click on a card to get the details of the task."), - position: "bottom", -}, { - trigger: ".o_kanban_record .oe_kanban_content", - extra_trigger: '.o_kanban_project_tasks', content: _t("Click on the card to write more information about it and collaborate with your coworkers."), position: "bottom", }, { diff --git a/addons/purchase_requisition/data/purchase_requisition_data.xml b/addons/purchase_requisition/data/purchase_requisition_data.xml index ceafd213f6e..0eeee2a5b8b 100644 --- a/addons/purchase_requisition/data/purchase_requisition_data.xml +++ b/addons/purchase_requisition/data/purchase_requisition_data.xml @@ -17,6 +17,7 @@ <field name="code">purchase.requisition.purchase.tender</field> <field name="prefix">TE</field> <field name="padding">5</field> + <field name="company_id" eval="False"></field> </record> <record id="seq_blanket_order" model="ir.sequence"> @@ -24,6 +25,7 @@ <field name="code">purchase.requisition.blanket.order</field> <field name="prefix">BO</field> <field name="padding">5</field> + <field name="company_id" eval="False"></field> </record> </data> </odoo> diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 6972025ae41..2c1db4a5bd3 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -11,6 +11,7 @@ from pytz import timezone, utc from odoo import api, fields, models, _ from odoo.addons.base.models.res_partner import _tz_get from odoo.exceptions import ValidationError +from odoo.osv import expression from odoo.tools.float_utils import float_round # Default hour per day value. The one should @@ -211,10 +212,10 @@ class ResourceCalendar(models.Model): resource_ids = [resource.id, False] if resource else [False] domain = domain if domain is not None else [] - domain = domain + [ + domain = expression.AND([domain, [ ('calendar_id', '=', self.id), ('resource_id', 'in', resource_ids), - ] + ]]) # express all dates and times in the resource's timezone tz = timezone((resource or self).tz) diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 4108146149d..f1dc84564da 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -12,6 +12,12 @@ from odoo.tools import float_utils ROUNDING_FACTOR = 16 +def timezone_datetime(time): + if not time.tzinfo: + time = time.replace(tzinfo=utc) + return time + + class ResourceMixin(models.AbstractModel): _name = "resource.mixin" _description = 'Resource Mixin' @@ -75,7 +81,7 @@ class ResourceMixin(models.AbstractModel): def _get_day_total(self, from_datetime, to_datetime, calendar, resource): """ - @return dict with hours of attendance in each day between `from_datetime` and `to_datetime` + :return: dict with hours of attendance in each day between `from_datetime` and `to_datetime` """ # total hours per day: retrieve attendances with one extra day margin, @@ -88,13 +94,7 @@ class ResourceMixin(models.AbstractModel): day_total[start.date()] += (stop - start).total_seconds() / 3600 return day_total - def _timezone_datetime(self, time): - if not time.tzinfo: - time = time.replace(tzinfo=utc) - return time - - - def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): + def _get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. @@ -109,8 +109,8 @@ class ResourceMixin(models.AbstractModel): calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC - from_datetime = self._timezone_datetime(from_datetime) - to_datetime = self._timezone_datetime(to_datetime) + from_datetime = timezone_datetime(from_datetime) + to_datetime = timezone_datetime(to_datetime) day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) @@ -122,7 +122,7 @@ class ResourceMixin(models.AbstractModel): return self._get_days_data(intervals, day_total) - def get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): + def _get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. @@ -137,8 +137,8 @@ class ResourceMixin(models.AbstractModel): calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC - from_datetime = self._timezone_datetime(from_datetime) - to_datetime = self._timezone_datetime(to_datetime) + from_datetime = timezone_datetime(from_datetime) + to_datetime = timezone_datetime(to_datetime) day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index 0b7f5feeb3f..3f0c9a48106 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -329,7 +329,7 @@ class TestResMixin(TestResourceCommon): # Looking at Jean's calendar # Viewing it as Jean - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.jean.tz), ) @@ -337,7 +337,7 @@ class TestResMixin(TestResourceCommon): # Viewing it as Patel # Views from 2018/04/01 20:00:00 to 2018/04/06 12:00:00 - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz), datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.patel.tz), ) @@ -345,7 +345,7 @@ class TestResMixin(TestResourceCommon): # Viewing it as John # Views from 2018/04/02 09:00:00 to 2018/04/07 02:00:00 - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz), datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.john.tz), ) @@ -356,7 +356,7 @@ class TestResMixin(TestResourceCommon): # Viewing it as Jean # Views from 2018/04/01 15:00:00 to 2018/04/06 14:00:00 - data = self.john.get_work_days_data( + data = self.john._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -364,21 +364,21 @@ class TestResMixin(TestResourceCommon): # Viewing it as Patel # Views from 2018/04/01 11:00:00 to 2018/04/06 10:00:00 - data = self.john.get_work_days_data( + data = self.john._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.patel.tz), ) self.assertEqual(data, {'days': 1.1875, 'hours': 10}) # Viewing it as John - data = self.john.get_work_days_data( + data = self.john._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz), ) self.assertEqual(data, {'days': 2, 'hours': 20}) # using Jean as a timezone reference - data = self.john.get_work_days_data( + data = self.john._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz), calendar=self.calendar_jean, @@ -394,14 +394,14 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz), }) - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) self.assertEqual(data, {'days': 4.5, 'hours': 36}) # using John as a timezone reference, leaves are outside attendances - data = self.john.get_work_days_data( + data = self.john._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz), calendar=self.calendar_jean, @@ -419,7 +419,7 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz), }) - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -436,7 +436,7 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz), }) - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -463,7 +463,7 @@ class TestResMixin(TestResourceCommon): }) # Jean asks to see how much leave he has taken - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.jean.tz), ) @@ -473,14 +473,14 @@ class TestResMixin(TestResourceCommon): # Patel Asks to see when Jean has taken some leaves # Patel should see the same - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.patel.tz), datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.patel.tz), ) self.assertEqual(data, {'days': 1, 'hours': 8}) # use Patel as a resource, jean's leaves are not visible - datas = self.patel.get_leave_days_data( + datas = self.patel._get_leave_days_data( datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.patel.tz), datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.patel.tz), calendar=self.calendar_jean, @@ -510,7 +510,7 @@ class TestResMixin(TestResourceCommon): # John asks how much leaves he has # He sees that he has only 15 hours of leave in his attendances - data = self.john.get_leave_days_data( + data = self.john._get_leave_days_data( datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.john.tz), datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.john.tz), ) @@ -525,7 +525,7 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz), }) - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -542,7 +542,7 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz), }) - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -559,7 +559,7 @@ class TestResMixin(TestResourceCommon): 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz), }) - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz), datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz), ) @@ -783,21 +783,21 @@ class TestTimezones(TestResourceCommon): def test_work_data(self): # 09-04-2018 10:00:00 - 13-04-2018 18:00:00 - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 9, 8, 0, 0), datetime_tz(2018, 4, 13, 16, 0, 0), ) self.assertEqual(data, {'days': 4.75, 'hours': 38}) # 09-04-2018 00:00:00 - 13-04-2018 08:00:00 - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3), datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3), ) self.assertEqual(data, {'days': 4, 'hours': 32}) # 09-04-2018 08:00:00 - 14-04-2018 12:00:00 - data = self.jean.get_work_days_data( + data = self.jean._get_work_days_data( datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2), datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4), ) @@ -813,21 +813,21 @@ class TestTimezones(TestResourceCommon): }) # 09-04-2018 10:00:00 - 13-04-2018 18:00:00 - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 9, 8, 0, 0), datetime_tz(2018, 4, 13, 16, 0, 0), ) self.assertEqual(data, {'days': 0.5, 'hours': 4}) # 09-04-2018 00:00:00 - 13-04-2018 08:00:00 - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3), datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3), ) self.assertEqual(data, {'days': 0.75, 'hours': 6}) # 09-04-2018 08:00:00 - 14-04-2018 12:00:00 - data = self.jean.get_leave_days_data( + data = self.jean._get_leave_days_data( datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2), datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4), ) diff --git a/addons/sale/models/account_reconciliation_widget.py b/addons/sale/models/account_reconciliation_widget.py index ff6612a0891..517d4e76e5d 100644 --- a/addons/sale/models/account_reconciliation_widget.py +++ b/addons/sale/models/account_reconciliation_widget.py @@ -16,7 +16,7 @@ class AccountReconciliation(models.AbstractModel): FROM sale_order o, account_bank_statement_line stl WHERE - lower(concat(o.name,o.reference)) ~ lower(stl.name) + POSITION(lower(stl.name) IN lower(concat(o.name,o.reference))) != 0 AND stl.id IN %s AND (stl.partner_id is null OR stl.partner_id = o.partner_id) AND (o.invoice_status = 'to invoice' OR o.state = 'sent') diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index ab645e7b476..cbcf2c79f23 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -869,7 +869,14 @@ class SaleOrder(models.Model): self.ensure_one() return '%s %s' % (self.type_name, self.name) + @api.multi def _get_share_url(self, redirect=False, signup_partner=False, pid=None): + """Override for sales order. + + If the SO is in a state where an action is required from the partner, + return the URL with a login token. Otherwise, return the URL with a + generic access token (no login). + """ self.ensure_one() if self.state not in ['sale', 'done']: auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id]) @@ -1432,7 +1439,6 @@ class SaleOrderLine(models.Model): result = {'warning': warning} if product.sale_line_warn == 'block': self.product_id = False - return result name = self.get_sale_order_line_multiline_description_sale(product) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index ff069e8ad1a..cd115e5bbab 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -238,3 +238,58 @@ class TestSaleOrder(TestCommonSaleNoChart): self.assertEquals(self.sale_order.amount_total, self.sale_order.amount_untaxed + self.sale_order.amount_tax, 'Taxes should be applied') + + def test_reconciliation_with_so(self): + # create SO + so = self.env['sale.order'].create({ + 'name': 'SO/01/01', + 'reference': 'Petit suisse', + 'partner_id': self.partner_customer_usd.id, + 'partner_invoice_id': self.partner_customer_usd.id, + 'partner_shipping_id': self.partner_customer_usd.id, + 'pricelist_id': self.pricelist_usd.id, + }) + self.env['sale.order.line'].create({ + 'name': self.product_order.name, + 'product_id': self.product_order.id, + 'product_uom_qty': 2, + 'product_uom': self.product_order.uom_id.id, + 'price_unit': self.product_order.list_price, + 'order_id': so.id, + 'tax_id': False, + }) + # Mark SO as sent otherwise we won't find any match + so.write({'state': 'sent'}) + # Create bank statement + statement = self.env['account.bank.statement'].create({ + 'name': 'Test', + 'journal_id': self.journal_purchase.id, + 'user_id': self.user_employee.id, + }) + st_line1 = self.env['account.bank.statement.line'].create({ + 'name': 'should not find anything', + 'amount': 15, + 'statement_id': statement.id + }) + st_line2 = self.env['account.bank.statement.line'].create({ + 'name': 'SO/01', + 'amount': 15, + 'statement_id': statement.id + }) + st_line3 = self.env['account.bank.statement.line'].create({ + 'name': 'suisse', + 'amount': 15, + 'statement_id': statement.id + }) + # Call get_bank_statement_line_data for st_line_1, should not find any sale order + res = self.env['account.reconciliation.widget'].get_bank_statement_line_data([st_line1.id]) + line = res.get('lines', [{}])[0] + self.assertFalse(line.get('sale_order_ids', False)) + # Call again for st_line_2, it should find sale_order + res = self.env['account.reconciliation.widget'].get_bank_statement_line_data([st_line2.id]) + line = res.get('lines', [{}])[0] + self.assertEquals(line.get('sale_order_ids', []), [so.id]) + # Call again for st_line_3, it should find sale_order based on reference + res = self.env['account.reconciliation.widget'].get_bank_statement_line_data([st_line3.id]) + line = res.get('lines', [{}])[0] + self.assertEquals(line.get('sale_order_ids', []), [so.id]) diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index c86f25ea73a..85ada359385 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -46,7 +46,7 @@ class SaleOrderLine(models.Model): # Calculation of the margin for programmatic creation of a SO line. It is therefore not # necessary to call product_id_change_margin manually - if 'purchase_price' not in vals: + if 'purchase_price' not in vals and ('display_type' not in vals or not vals['display_type']): order_id = self.env['sale.order'].browse(vals['order_id']) product_id = self.env['product.product'].browse(vals['product_id']) product_uom_id = self.env['uom.uom'].browse(vals['product_uom']) diff --git a/addons/sale_quotation_builder/views/sale_portal_templates.xml b/addons/sale_quotation_builder/views/sale_portal_templates.xml index 93b41b50a76..a205c274781 100644 --- a/addons/sale_quotation_builder/views/sale_portal_templates.xml +++ b/addons/sale_quotation_builder/views/sale_portal_templates.xml @@ -9,6 +9,12 @@ <t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids"> <t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/> <a t-att-id="line.id"/> + <div class="alert alert-info alert-dismissable mt16 css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button> + Product: <strong t-esc="line.product_id.name"/>: + this content will appear on the quotation only if this + product is not removed. + </div> <div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/> </t> </t> diff --git a/addons/sale_stock/views/sale_stock_portal_template.xml b/addons/sale_stock/views/sale_stock_portal_template.xml index 9069b6c1e27..43dfdb779cf 100644 --- a/addons/sale_stock/views/sale_stock_portal_template.xml +++ b/addons/sale_stock/views/sale_stock_portal_template.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <template id="sale_order_portal_content_inherit_sale_stock" name="Orders Shipping Followup" inherit_id="sale.sale_order_portal_content"> - <xpath expr="//div[@id='shipping_address']" position="inside"> + <xpath expr="//div[@id='informations']" position="inside"> <t t-if="sale_order.picking_ids"> <div> <strong>Delivery Orders</strong> diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 1dbedd1b778..42b0f28dc0f 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -273,7 +273,9 @@ class Product(models.Model): # TODO: Still optimization possible when searching virtual quantities ids = [] - for product in self.with_context(prefetch_fields=False).search([]): + # Order the search on `id` to prevent the default order on the product name which slows + # down the search because of the join on the translation table to get the translated names. + for product in self.with_context(prefetch_fields=False).search([], order='id'): if OPERATORS[operator](product[field], value): ids.append(product.id) return [('id', 'in', ids)] diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 0b7fa6ed4ba..20a252b8307 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -280,9 +280,9 @@ class Warehouse(models.Model): warehouse_data[picking_type] = PickingType.create(values).id if 'out_type_id' in warehouse_data: - PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']}) + PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data.get('in_type_id', False)}) if 'in_type_id' in warehouse_data: - PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']}) + PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data.get('out_type_id', False)}) return warehouse_data def _create_or_update_global_routes_rules(self): diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index a91a860dee9..a6958d6d14d 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -5,18 +5,14 @@ <t t-call="web.html_container"> <t t-call="web.external_layout"> <t t-set="o" t-value="o.with_context({'lang':o.partner_id.lang})" /> - <t t-if="o.move_lines and o.move_lines[0].partner_id" name="partner_header"> + <t t-set="partner" t-value="o.partner_id or (o.move_lines and o.move_lines[0].partner_id) or False"/> + <t t-if="partner" name="partner_header"> <t t-set="address"> - <div t-field="o.move_lines[0].partner_id" + <div t-esc="partner" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> </t> </t> - <t t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header"> - <t t-set="address"> - <div t-field="o.partner_id" - t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> - </t> - </t> + <div class="page"> <h2> <span t-field="o.name"/> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index eadcd993ffd..269964ccef1 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -227,7 +227,7 @@ <field name="show_operations" invisible="1" readonly="1"/> <field name="move_line_exist" invisible="1"/> <field name="has_packages" invisible="1"/> - <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', '|', ('picking_type_code', '=', 'incoming'), ('state', 'in', ('draft', 'cancel', 'waiting', 'done')), ('is_locked', '=', False)]}"/> + <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', '|', '&', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done'), '&', ('picking_type_code', '=', 'outgoing'), ('state', '=', 'done'), ('is_locked', '=', False)]}"/> <button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('state', 'in', ('draft','cancel')), ('is_locked', '=', False)]}" string="Unlock" groups="stock.group_stock_manager" type="object" help="If the picking is unlocked you can edit initial demand (for a draft picking) or done quantities (for a done picking)."/> <button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="stock.group_stock_manager" type="object"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done" /> <field name="picking_type_entire_packs" invisible="1"/> @@ -250,12 +250,9 @@ <group> <group> <field name="partner_id"/> - <button name="action_assign_partner" string="Assign Partner" type="object" help="Assign the partner to the Operations. Use only if the partner is manually changed!" - attrs="{'invisible': ['|', '|', ('move_line_exist', '=', False), ('state', 'not in', ('draft', 'assigned', 'confirmed')), ('picking_type_code', '=', 'internal')]}" - class="oe_link oe_edit_only"/> + <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> + <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> <field name="picking_type_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/> - <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/> <field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/> </group> <group> diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index a3706c96354..8a60f15b388 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -265,7 +265,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=51, emp=71): # com runbot: 51 - 71 // test_mail only: 48 - 68 + with self.assertQueryCount(__system__=52, emp=72): # com runbot: 52 - 72 // test_mail only: 48 - 68 record.message_post( body='<p>Test Post Performances with an email ping</p>', partner_ids=self.customer.ids, diff --git a/addons/test_website/__init__.py b/addons/test_website/__init__.py new file mode 100644 index 00000000000..273ede25ea3 --- /dev/null +++ b/addons/test_website/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py new file mode 100644 index 00000000000..557722971d4 --- /dev/null +++ b/addons/test_website/__manifest__.py @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Website Test', + 'version': '1.0', + 'category': 'Hidden', + 'sequence': 9876, + 'summary': 'Website Test, mainly for module install/uninstall tests', + 'description': """This module contains tests related to website. Those are +contained in a separate module as we are testing module install/uninstall/upgrade +and we don't want to reload the website module every time, including it's possible +dependencies. Neither we want to add in website module some routes, views and +models which only purpose is to run tests.""", + 'depends': [ + 'website', + ], + 'data': [ + 'views/templates.xml', + 'data/test_website_data.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/addons/payment_ccavenue/controllers/__init__.py b/addons/test_website/controllers/__init__.py index 5d4b25db9c0..5d4b25db9c0 100644 --- a/addons/payment_ccavenue/controllers/__init__.py +++ b/addons/test_website/controllers/__init__.py diff --git a/addons/test_website/controllers/main.py b/addons/test_website/controllers/main.py new file mode 100644 index 00000000000..fad797d6984 --- /dev/null +++ b/addons/test_website/controllers/main.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request +from odoo.addons.portal.controllers.web import Home + + +class WebsiteTest(Home): + + @http.route('/test_view', type='http', auth="public", website=True) + def test_view(self, **kw): + return request.render('test_website.test_view') diff --git a/addons/test_website/data/test_website_data.xml b/addons/test_website/data/test_website_data.xml new file mode 100644 index 00000000000..df827d542a3 --- /dev/null +++ b/addons/test_website/data/test_website_data.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <record id="test_view" model="ir.ui.view"> + <field name="name">Test View</field> + <field name="type">qweb</field> + <field name="key">test_website.test_view</field> + <field name="arch" type="xml"> + <t name="Test View" priority="29" t-name="test_website.test_view"> + <t t-call="website.layout"> + <p>Test View</p> + <p>placeholder</p> + </t> + </t> + </field> + </record> + <record id="test_page_view" model="ir.ui.view"> + <field name="name">Test Page View</field> + <field name="type">qweb</field> + <field name="key">test_website.test_page_view</field> + <field name="arch" type="xml"> + <t name="Test Page View" priority="29" t-name="test_website.test_page_view"> + <t t-call="website.layout"> + <div id="oe_structure_test_website_page" class="oe_structure oe_empty"/> + <p>Test Page View</p> + <p>placeholder</p> + </t> + </t> + </field> + </record> + <record id="test_page" model="website.page"> + <field name="website_published">True</field> + <field name="url">/test_page_view</field> + <field name="view_id" ref="test_page_view"/> + </record> + <record id="test_view_to_be_t_called" model="ir.ui.view"> + <field name="name">Test View To Be t-called</field> + <field name="type">qweb</field> + <field name="key">test_website.test_view_to_be_t_called</field> + <field name="arch" type="xml"> + <t name="Test View To Be t-called" priority="29" t-name="test_website.test_view_to_be_t_called"> + <p>Test View To Be t-called</p> + <p>placeholder</p> + </t> + </field> + </record> + <template id="test_view_child_broken" inherit_id="test_website.test_view" active="False"> + <xpath expr="//p[last()]" position="replace"> + <p>Test View Child Broken</p> + <p>placeholder</p> + </xpath> + </template> + </data> +</odoo> diff --git a/addons/test_website/static/src/js/reset_views.test.js b/addons/test_website/static/src/js/reset_views.test.js new file mode 100644 index 00000000000..957cda6d594 --- /dev/null +++ b/addons/test_website/static/src/js/reset_views.test.js @@ -0,0 +1,103 @@ +odoo.define('test_website.reset_views', function (require) { +'use strict'; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); + +tour.register('test_reset_page_view_complete_flow_part1', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + // 1. Edit the page through Edit Mode, it will COW the view + { + content: "enter edit mode", + trigger: "a[data-action=edit]" + }, + { + content: "drop a snippet", + trigger: "#oe_snippets .oe_snippet:has(.s_cover) .oe_snippet_thumbnail", + // id starting by 'oe_structure..' will actually create an inherited view + run: "drag_and_drop #oe_structure_test_website_page", + }, + { + content: "save the page", + extra_trigger: '#oe_structure_test_website_page .s_cover', + trigger: "#web_editor-top-edit button[data-action=save]", + }, + // 2. Edit that COW'd view in the HTML editor to break it. + { + content: "open customize menu", + extra_trigger: "body:not(.editor_enable)", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("placeholder")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n'); + }, + }, + { + content: "save the html editor", + extra_trigger: '.ace_content:contains("not.exist")', + trigger: ".o_ace_view_editor button[data-action=save]", + } + + // 3. Reset the broken view + ] +); + +tour.register('test_reset_page_view_complete_flow_part2', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + { + content: "check that the view got fixed", + trigger: 'p:containsExact("Test Page View")', + run: function () {}, // it's a check + }, + { + content: "check that the inherited COW view is still there (created during edit mode)", + trigger: '#oe_structure_test_website_page .s_cover', + run: function () {}, // it's a check + }, + //4. Now break the inherited view created when dropping a snippet + { + content: "open customize menu", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "select oe_structure view", + trigger: '#ace-view-list', + run: function () { + var viewId = $('#ace-view-list option:contains("oe_structure")').val(); + $('#ace-view-list').val(viewId).trigger('change'); + }, + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n'); + }, + }, + { + content: "save the html editor", + trigger: ".o_ace_view_editor button[data-action=save]", + } + ] +); + +}); diff --git a/addons/payment_ccavenue/models/__init__.py b/addons/test_website/tests/__init__.py index 2ec5b9cd2f6..4c32d5f1959 100644 --- a/addons/payment_ccavenue/models/__init__.py +++ b/addons/test_website/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import payment +from . import test_reset_views diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py new file mode 100644 index 00000000000..930d3b00a42 --- /dev/null +++ b/addons/test_website/tests/test_reset_views.py @@ -0,0 +1,104 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import re +import unittest + +import odoo.tests +from odoo.tools import mute_logger + + +def break_view(view, fr='<p>placeholder</p>', to='<p t-field="not.exist"/>'): + view.arch = view.arch.replace(fr, to) + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestWebsiteResetViews(odoo.tests.HttpCase): + def do_test(self, name): + self.browser_js( + "/", + "odoo.__DEBUG__.services['web_tour.tour'].run('%s')" % name, + "odoo.__DEBUG__.services['web_tour.tour'].tours.%s.ready" % name, + login="admin" + ) + + def fix_it(self, page): + self.authenticate("admin", "admin") + resp = self.url_open(page) + self.assertEqual(resp.status_code, 500, "Waiting 500") + self.assertTrue('<button id="reset_templates_button"' in resp.text) + data = {'templates': [self.find_template(resp)], 'redirect': page} + resp = self.url_open('/website/reset_templates', data) + self.assertEqual(resp.status_code, 200, "Waiting 200") + + def find_template(self, response): + find = re.search(r'<input.*type="checkbox".*name="templates".*value="([0-9]+)?"', response.text) + return find and find.group(1) + + def setUp(self): + super(TestWebsiteResetViews, self).setUp() + self.Website = self.env['website'] + self.View = self.env['ir.ui.view'] + self.test_view = self.Website.viewref('test_website.test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_01_reset_specific_page_view(self): + self.test_page_view = self.Website.viewref('test_website.test_page_view') + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_page_view.with_context(website_id=1)) + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_page_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_02_reset_specific_view_controller(self): + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view.with_context(website_id=1)) + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_03_reset_specific_view_controller_t_called(self): + self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called') + + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view_to_be_t_called.with_context(website_id=1)) + break_view(self.test_view, to='<t t-call="test_website.test_view_to_be_t_called"/>') + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_04_reset_specific_view_controller_inherit(self): + self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken') + + # Activate and break the inherited view + self.test_view_child_broken.active = True + break_view(self.test_view_child_broken.with_context(website_id=1, load_all_views=True)) + + self.fix_it('/test_view') + + # This test work in real life, but not in test mode since we cannot rollback savepoint. + # @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + # def test_05_reset_specific_view_controller_broken_request(self): + # total_views = self.View.search_count([('type', '=', 'qweb')]) + # # Trigger COW then break the QWEB XML on it + # break_view(self.test_view.with_context(website_id=1), to='<t t-esc="request.env[\'website\'].browse(\'a\').name" />') + # self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (1)") + # self.fix_it('/test_view') + + # also mute ir.ui.view as `get_view_id()` will raise "Could not find view object with xml_id 'not.exist'"" + @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + def test_06_reset_specific_view_controller_inexisting_template(self): + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view.with_context(website_id=1), to='<t t-call="not.exist"/>') + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)") + self.fix_it('/test_view') + + @unittest.skip("broken test. Temporary deactivated") + @mute_logger('odoo.addons.website.models.ir_http') + def test_07_reset_page_view_complete_flow(self): + self.do_test('test_reset_page_view_complete_flow_part1') + self.fix_it('/test_page_view') + self.do_test('test_reset_page_view_complete_flow_part2') + self.fix_it('/test_page_view') diff --git a/addons/test_website/views/templates.xml b/addons/test_website/views/templates.xml new file mode 100644 index 00000000000..2ce171bac30 --- /dev/null +++ b/addons/test_website/views/templates.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_frontend" inherit_id="website.assets_frontend"> + <xpath expr="script[last()]" position="after"> + <script type="text/javascript" src="/test_website/static/src/js/reset_views.test.js"></script> + </xpath> + </template> +</odoo> diff --git a/addons/web/static/lib/moment/locale/es.js b/addons/web/static/lib/moment/locale/es.js index eb9e0809dfe..6d1f5907968 100644 --- a/addons/web/static/lib/moment/locale/es.js +++ b/addons/web/static/lib/moment/locale/es.js @@ -8,21 +8,30 @@ typeof define === 'function' && define.amd ? define(['../moment'], factory) : factory(global.moment) }(this, (function (moment) { 'use strict'; +var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'), + monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); - -var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'); -var monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); +var monthsParse = [/^ene/i, /^feb/i, /^mar/i, /^abr/i, /^may/i, /^jun/i, /^jul/i, /^ago/i, /^sep/i, /^oct/i, /^nov/i, /^dic/i]; +var monthsRegex = /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i; var es = moment.defineLocale('es', { months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'), monthsShort : function (m, format) { - if (/-MMM-/.test(format)) { + if (!m) { + return monthsShortDot; + } else if (/-MMM-/.test(format)) { return monthsShort[m.month()]; } else { return monthsShortDot[m.month()]; } }, - monthsParseExact : true, + monthsRegex : monthsRegex, + monthsShortRegex : monthsRegex, + monthsStrictRegex : /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i, + monthsShortStrictRegex : /^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i, + monthsParse : monthsParse, + longMonthsParse : monthsParse, + shortMonthsParse : monthsParse, weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'), weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'), weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'), @@ -57,6 +66,7 @@ var es = moment.defineLocale('es', { future : 'en %s', past : 'hace %s', s : 'unos segundos', + ss : '%d segundos', m : 'un minuto', mm : '%d minutos', h : 'una hora', diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index bd794323995..60b80a7f439 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -273,8 +273,7 @@ var py = {}; var Special = '[:;.,`@]'; var Funny = group(Operator, Bracket, Special); - var ContStr = group("([uU])?'([^']*)'", '([uU])?"([^"]*)"'); - + var ContStr = group("([uU])?'([^\n'\\\\]*(?:\\\\.[^\n'\\\\]*)*)'", '([uU])?"([^\n"\\\\]*(?:\\\\.[^\n"\\\\]*)*)"'); var PseudoToken = Whitespace + group(Number, Funny, ContStr, Name); var number_pattern = new RegExp('^' + Number + '$'); @@ -282,9 +281,6 @@ var py = {}; var name_pattern = new RegExp('^' + Name + '$'); var strip = new RegExp('^' + Whitespace); return function tokenize(s) { - s = s - .replace(/\\\"/g, '\\u0022') // for double quote - .replace(/\\\'/g, '\\u0027'); // for single quote var max=s.length, tokens = [], start, end; // /g flag makes repeated exec() have memory var pseudoprog = new RegExp(PseudoToken, 'g'); @@ -313,9 +309,6 @@ var py = {}; } else if (string_pattern.test(token)) { var m = string_pattern.exec(token); var value = (m[3] !== undefined ? m[3] : m[5]); - value - .replace(/\\u0022/g, '"') - .replace(/\\u0027/g, "'"); tokens.push(create(symbols['(string)'], { unicode: !!(m[2] || m[4]), value: value diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js index 34dd24b2be6..ef828b5dd5c 100644 --- a/addons/web/static/src/js/core/abstract_storage_service.js +++ b/addons/web/static/src/js/core/abstract_storage_service.js @@ -13,6 +13,17 @@ var AbstractStorageService = AbstractService.extend({ // this abstraction storage: null, + /** + * @override + */ + destroy: function () { + // storage can be permanent or transient, destroy transient ones + if ((this.storage || {}).destroy) { + this.storage.destroy(); + } + this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index 9bcd66864e5..296ab5c3089 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -415,7 +415,15 @@ function _formatAST(ast, lbp) { case "(number)": return String(ast.value); case "(string)": - return "'" + ast.value + "'"; + // ast.value is a string that may contain a mix of single and double quotes. + // But we need to return a string that represents the string ast.value. + // We use for that JSON.stringify. It will return a string with quotes correctly + // escaped. For instance, for x = `""` JSON.stringify(x) is `"\"\""`. + // But this means that if we pass several times here, escapes will be + // done again and again, leading to a profusion of backslashes that we don't want. + // For fun consider y = `\\` and y = JSON.stringify(y). + // This is why we first deescape particular characters in ast.value. + return JSON.stringify(ast.value.replace(/(\\(['"\\]))/g,"\$2")); case "(constant)": return ast.value; case "(name)": diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index cf64de72eef..44e09933a25 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -303,7 +303,17 @@ var utils = { var d2 = Math.pow(10, decimals); var val = _t('kMGTPE'); var symbol = ''; - for (var i = val.length - 1 ; i > 0 ; i--) { + var numberMagnitude = number.toExponential().split('e')[1]; + // the case numberMagnitude >= 21 corresponds to a number + // better expressed in the scientific format. + if (numberMagnitude >= 21) { + // we do not use number.toExponential(decimals) because we want to + // avoid the possible useless O decimals: 1e.+24 prefered to 1.0e+24 + number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2; + // formatterCallback seems useless here. + return number + 'e' + numberMagnitude; + } + for (var i = val.length; i > 0 ; i--) { var s = Math.pow(10, i * 3); if (s <= number / Math.pow(10, minDigits - 1)) { number = Math.round(number * d2 / s) / d2; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index dcfdc982b7d..c6df3885a86 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2914,6 +2914,7 @@ var AceEditor = DebouncedField.extend({ if (this.aceEditor) { this.aceEditor.destroy(); } + this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js index f58fbc93c4f..e59dbfb1036 100644 --- a/addons/web/static/src/js/views/control_panel/control_panel_model.js +++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js @@ -927,10 +927,21 @@ var ControlPanelModel = mvc.Model.extend({ } var sort = JSON.parse(favorite.sort); var orderedBy = sort.map(function (order) { - var orderTerms = order.split(' '); + var fieldName; + var asc; + var sqlNotation = order.split(' '); + if (sqlNotation.length > 1) { + // regex: \fieldName (asc|desc)?\ + fieldName = sqlNotation[0]; + asc = sqlNotation[1] === 'asc'; + } else { + // legacy notation -- regex: \-?fieldName\ + fieldName = order[0] === '-' ? order.slice(1) : order; + asc = order[0] === '-' ? false : true; + } return { - name: orderTerms[0], - asc: orderTerms.length === 2 && orderTerms[1] === 'asc', + name: fieldName, + asc: asc, }; }); return { diff --git a/addons/web/static/src/js/views/control_panel/search/filter_menu.js b/addons/web/static/src/js/views/control_panel/search/filter_menu.js index 70a242dd0f8..04f333b37c4 100644 --- a/addons/web/static/src/js/views/control_panel/search/filter_menu.js +++ b/addons/web/static/src/js/views/control_panel/search/filter_menu.js @@ -89,8 +89,6 @@ var FilterMenu = DropdownMenu.extend({ domain: Domain.prototype.arrayToString(preFilter.attrs.domain), }; }); - // TO DO intercepts 'new_filters' and decide what to do whith filters - // rewrite web.search_filters? this.trigger_up('new_filters', {filters: filters}); _.invoke(this.propositions, 'destroy'); this.propositions = []; diff --git a/addons/web/static/src/js/views/control_panel/search/search_filters.js b/addons/web/static/src/js/views/control_panel/search/search_filters.js index 4703995ac7c..80a9019c02d 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_filters.js +++ b/addons/web/static/src/js/views/control_panel/search/search_filters.js @@ -171,15 +171,6 @@ var Char = Field.extend({ {value: "∃", text: _lt("is set")}, {value: "∄", text: _lt("is not set")} ], - get_domain: function (field, operator) { - switch (operator.value) { - case '∃': return [[field.name, '!=', false]]; - case '∄': return [[field.name, '=', false]]; - // does not work in all cases - // default: return "[('" + field.name + "', '" + operator.value + "', '" + this.get_value() + "')]"; - default: return [[field.name, operator.value, this.get_value()]]; - } - }, get_value: function () { return this.$el.val(); } @@ -251,7 +242,7 @@ var DateTime = Field.extend({ _create_new_widget: function (name) { this[name] = new (this._get_widget_class())(this); return this[name].appendTo(this.$el).then((function () { - this[name].setValue(moment(new Date())); + this[name].setValue(moment()); }).bind(this)); }, _get_widget_class: function () { diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 549fa56eefa..9353301ca09 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -21,6 +21,7 @@ var CHART_TYPES = ['pie', 'bar', 'line']; // hide top legend when too many items for device size var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class)); +var SPLIT_THRESHOLD = config.device.isMobile ? Infinity : 20; return AbstractRenderer.extend({ className: "o_graph_renderer", @@ -456,7 +457,11 @@ return AbstractRenderer.extend({ } } - var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'}); + var $svgContainer = $('<div/>', { class: 'o_graph_svg_container'}); + // Split the tooltip into columns for large data because some portion goes out off the screen. + if (data.length >= SPLIT_THRESHOLD) { + $svgContainer.addClass('o_tooltip_split_in_columns'); + } this.$el.append($svgContainer); var svg = d3.select($svgContainer[0]).append('svg'); svg.datum(data); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 4c37b823639..3f195c1ea24 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -237,7 +237,7 @@ var PivotModel = AbstractModel.extend({ [ this.data.timeRangeDescription.toString(), this.data.comparisonTimeRangeDescription.toString(), - 'Variation' + _t('Variation') ], makeMeasure ) @@ -443,8 +443,10 @@ var PivotModel = AbstractModel.extend({ self._updateMainGroupBys(old_row, self.data.main_row); } - self._updateTree(old_col.root, self.data.main_col.root); - self._updateMainGroupBys(old_col, self.data.main_col); + if (!('pivot_column_groupby' in (params.context || {}))) { + self._updateTree(old_col.root, self.data.main_col.root); + self._updateMainGroupBys(old_col, self.data.main_col); + } }); }, /** diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index 813dc5d38b3..1a3c451f064 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -7,6 +7,7 @@ var field_utils = require('web.field_utils'); var config = require('web.config'); var QWeb = core.qweb; +var _t = core._t; var PivotRenderer = AbstractRenderer.extend({ tagName: 'table', @@ -173,7 +174,7 @@ var PivotRenderer = AbstractRenderer.extend({ $thead.append($row); } if (this.compare) { - var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, 'Variation']; + var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, _t('Variation')]; var dataTypes = ['data', 'comparisonData', 'variation']; $row = $('<tr>'); for (i = 0; i < 3 * nbrCols; i++) { diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index 5642e656dea..0645e00544e 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -744,6 +744,10 @@ .o_field_widget { width: 50%; flex: 0 0 auto; + + &.o_field_many2manytags > .o_field_widget { + flex: 1 0 50px; + } } button.btn-link:first-child { padding-left: 0; diff --git a/addons/web/static/src/scss/graph_view.scss b/addons/web/static/src/scss/graph_view.scss index 71ec5fbca9c..e67edbd3cb5 100644 --- a/addons/web/static/src/scss/graph_view.scss +++ b/addons/web/static/src/scss/graph_view.scss @@ -24,6 +24,35 @@ // ligther area color fill-opacity: 0.1; } + .nv-lineChart .nv-x.nv-axis .nv-axisMaxMin-x { + &.nv-axisMin-x text { + text-anchor: start !important; + } + &.nv-axisMax-x text { + text-anchor: end !important; + } + } + } + + /* Use less than 50% of the screen area for tooltip and show legends in 3 column of 33% each */ + &.o_tooltip_split_in_columns .nvtooltip { + max-width: 48%; + tbody { + display: flex; + flex-flow: wrap; + tr { + width: 33%; + td { + display: inline-block; + &.key { + min-width: 160px; + max-width: 70%; + overflow-x: hidden; + text-overflow: ellipsis; + } + } + } + } } } } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 43d8f6900e8..8126fcf11c4 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -347,7 +347,7 @@ <t t-raw="item.label"/> </a> </t> - <div class="dropdown-menu" role="menu"> + <div class="dropdown-menu o_dropdown_menu" role="menu"> <t t-foreach="widget.items[section.name]" t-as="item"> <t t-if="section.name == 'files'"> <t t-set="item.title"> diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 291084402a9..4b8a3a79751 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -3133,7 +3133,7 @@ QUnit.module('ActionManager', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, "[('bar', '=', 1)]", + assert.deepEqual(filter.domain, `[("bar", "=", 1)]`, "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/chrome/user_menu_tests.js b/addons/web/static/tests/chrome/user_menu_tests.js index 600ac125dd4..0151afdf6d6 100644 --- a/addons/web/static/tests/chrome/user_menu_tests.js +++ b/addons/web/static/tests/chrome/user_menu_tests.js @@ -25,6 +25,7 @@ QUnit.module('chrome', {}, function () { "should have a 'Shortcuts' item"); userMenu.destroy(); + parent.destroy(); }); }); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index be5760fc6c7..fba8017cf01 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -1149,7 +1149,7 @@ QUnit.module('core', function () { assert.checkAST("1.4", "float value"); assert.checkAST("-12", "negative integer value"); assert.checkAST("True", "boolean"); - assert.checkAST("'some string'", "a string"); + assert.checkAST(`"some string"`, "a string"); assert.checkAST("None", "None"); }); @@ -1157,8 +1157,8 @@ QUnit.module('core', function () { assert.expect(3); assert.checkAST("{}", "empty dictionary"); - assert.checkAST("{'a': 1}", "dictionary with a single key"); - assert.checkAST("d['a']", "get a value in a dictionary"); + assert.checkAST(`{"a": 1}`, "dictionary with a single key"); + assert.checkAST(`d["a"]`, "get a value in a dictionary"); }); QUnit.test("list", function (assert) { @@ -1225,14 +1225,14 @@ QUnit.module('core', function () { QUnit.test("strftime", function (assert) { assert.expect(3); - assert.checkAST("time.strftime('%Y')", "strftime with year"); - assert.checkAST("time.strftime('%Y') + '-01-30'", "strftime with year"); - assert.checkAST("time.strftime('%Y-%m-%d %H:%M:%S')", "strftime with year"); + assert.checkAST(`time.strftime("%Y")`, "strftime with year"); + assert.checkAST(`time.strftime("%Y") + "-01-30"`, "strftime with year"); + assert.checkAST(`time.strftime("%Y-%m-%d %H:%M:%S")`, "strftime with year"); }); QUnit.test("context_today", function (assert) { assert.expect(1); - assert.checkAST("context_today().strftime('%Y-%m-%d')", "context today call"); + assert.checkAST(`context_today().strftime("%Y-%m-%d")`, "context today call"); }); @@ -1250,17 +1250,10 @@ QUnit.module('core', function () { assert.checkAST('(a - b).days', "substraction and .days"); assert.checkAST('a + day == date(2002, 3, 3)'); - var expr = "[('type', '=', 'in'), ('day', '<=', time.strftime('%Y-%m-%d')), ('day', '>', (context_today() - datetime.timedelta(days = 15)).strftime('%Y-%m-%d'))]"; + var expr = `[("type", "=", "in"), ("day", "<=", time.strftime("%Y-%m-%d")), ("day", ">", (context_today() - datetime.timedelta(days = 15)).strftime("%Y-%m-%d"))]`; assert.checkAST(expr); }); - QUnit.test("parse escaped quoted strings", function (assert) { - assert.expect(2); - - assert.checkAST(`'\"'`, "a string containing \""); - assert.checkAST(`'\'`, "a string containing \'"); - }); - QUnit.module('pyutils (_normalizeDomain)'); QUnit.assert.checkNormalization = function (domain, normalizedDomain) { @@ -1278,23 +1271,23 @@ QUnit.module('core', function () { assert.expect(3); assert.checkNormalization("[]"); - assert.checkNormalization("[('a', '=', 1)]"); - assert.checkNormalization("['!', ('a', '=', 1)]"); + assert.checkNormalization(`[("a", "=", 1)]`); + assert.checkNormalization(`["!", ("a", "=", 1)]`); }); QUnit.test("properly add the & in a non normalized domain", function (assert) { assert.expect(1); assert.checkNormalization( - "[('a', '=', 1), ('b', '=', 2)]", - "['&', ('a', '=', 1), ('b', '=', 2)]" + `[("a", "=", 1), ("b", "=", 2)]`, + `["&", ("a", "=", 1), ("b", "=", 2)]` ); }); QUnit.test("normalize domain with ! operator", function (assert) { assert.expect(1); assert.checkNormalization( - "['!', ('a', '=', 1), ('b', '=', 2)]", - "['&', '!', ('a', '=', 1), ('b', '=', 2)]" + `["!", ("a", "=", 1), ("b", "=", 2)]`, + `["&", "!", ("a", "=", 1), ("b", "=", 2)]` ); }); diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js index 3dd0a54b5e3..368f36c2fc6 100644 --- a/addons/web/static/tests/core/util_tests.js +++ b/addons/web/static/tests/core/util_tests.js @@ -67,6 +67,26 @@ QUnit.module('core', {}, function () { assert.strictEqual("MzGzHz", caseSensetiveCharacters); }); + QUnit.test('human_number', function (assert) { + assert.expect(13); + + var human_number = utils.human_number; + + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1020000, 2, 2), '1020k'); + assert.strictEqual(human_number(10200000, 2, 2), '10.2M'); + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1002, 2, 1), '1k'); + assert.strictEqual(human_number(101, 2, 1), '101'); + assert.strictEqual(human_number(64.2, 2, 1), '64'); + assert.strictEqual(human_number(1e+18), '1E'); + assert.strictEqual(human_number(1e+21, 2, 1), '1e+21'); + assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22'); + assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22'); + assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43'); + assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43'); + }); + }); }); diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js index 834033a1769..0eb4192c4a1 100644 --- a/addons/web/static/tests/core/widget_tests.js +++ b/addons/web/static/tests/core/widget_tests.js @@ -24,6 +24,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy('exec'); fn(); assert.ok(w.executed, 'should execute the named method in the right context'); + w.destroy(); }); QUnit.test('proxy (String)(*args)', function (assert) { @@ -39,6 +40,7 @@ QUnit.module('core', {}, function () { fn(42); assert.ok(w.executed, "should execute the named method in the right context"); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); QUnit.test('proxy (String), include', function (assert) { @@ -59,6 +61,7 @@ QUnit.module('core', {}, function () { fn(); assert.strictEqual(w.executed, 2, "should be lazily resolved"); + w.destroy(); }); QUnit.test('proxy (Function)', function (assert) { @@ -69,6 +72,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function () { this.executed = true; }); fn(); assert.ok(w.executed, "should set the function's context (like Function#bind)"); + w.destroy(); }); QUnit.test('proxy (Function)(*args)', function (assert) { @@ -79,6 +83,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); @@ -99,6 +104,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element"); assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute"); assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content")); + widget.destroy(); }); QUnit.test('no template, custom tag', function (assert) { @@ -111,6 +117,7 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual(widget.el.nodeName, 'UL', "should have generated the custom element tag"); + widget.destroy(); }); QUnit.test('no template, @id', function (assert) { @@ -124,6 +131,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.attributes.length, 1, "should have one attribute"); assert.hasAttrValue(widget.$el, 'id', 'foo', "should have generated the id attribute"); assert.strictEqual(widget.el.id, 'foo', "should also be available via property"); + widget.destroy(); }); QUnit.test('no template, @className', function (assert) { @@ -136,6 +144,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.className, 'oe_some_class', "should have the right property"); assert.hasAttrValue(widget.$el, 'class', 'oe_some_class', "should have the right attribute"); + widget.destroy(); }); QUnit.test('no template, bunch of attributes', function (assert) { @@ -166,6 +175,7 @@ QUnit.module('core', {}, function () { assert.hasAttrValue(widget.$el, 'clark', 'gable'); assert.hasAttrValue(widget.$el, 'spoiler', 'snape kills dumbledore'); + widget.destroy(); }); QUnit.test('template', function (assert) { @@ -193,6 +203,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'OL'); assert.strictEqual(widget.$el.children().length, 5); assert.strictEqual(widget.el.textContent, '01234'); + widget.destroy(); }); QUnit.test('repeated', function (assert) { @@ -219,6 +230,8 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual($fix.find('p').text(), '36', "DOM fixture should use new value"); assert.strictEqual(widget.$el.text(), '36', "should set new value"); + }).always(function () { + widget.destroy(); }); }); @@ -257,6 +270,7 @@ QUnit.module('core', {}, function () { assert.ok(widget.$('li:eq(3)').is(widget.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); + widget.destroy(); }); @@ -285,6 +299,7 @@ QUnit.module('core', {}, function () { for(var i=0; i<3; ++i) { assert.ok(a[i], "should pass test " + i); } + widget.destroy(); }); QUnit.test('undelegate', function (assert) { @@ -310,6 +325,7 @@ QUnit.module('core', {}, function () { widget.$('li').click(); assert.ok(!clicked, "undelegate should unbind events delegated"); assert.ok(newclicked, "undelegate should only unbind events it created"); + widget.destroy(); }); QUnit.module('Widget, and async stuff'); @@ -321,7 +337,8 @@ QUnit.module('core', {}, function () { return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen()); }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead)", function (assert) { @@ -355,7 +372,8 @@ QUnit.module('core', {}, function () { var widget = new (Widget.extend({})); return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen(), true) }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead, true)", function (assert) { @@ -445,4 +463,4 @@ QUnit.module('core', {}, function () { }); -});
\ No newline at end of file +}); diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 27dbaeeaa44..2ecc655db7e 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -199,6 +199,11 @@ function addMockEnvironment(widget, params) { // widget is destroyed, at the end of each test to avoid collisions core.bus.trigger('clear_cache'); + _(services).chain() + .compact() // services can be defined but null (e.g. ajax) + .reject(function (s) { return s.isDestroyed(); }) + .invoke('destroy'); + DebouncedField.prototype.DEBOUNCE = initialDebounceValue; dom.DEBOUNCE = initialDOMDebounceValue; if (params.debounce === false) { diff --git a/addons/web/static/tests/views/abstract_controller_tests.js b/addons/web/static/tests/views/abstract_controller_tests.js index 73bc41d650b..f9045645bc6 100644 --- a/addons/web/static/tests/views/abstract_controller_tests.js +++ b/addons/web/static/tests/views/abstract_controller_tests.js @@ -80,6 +80,8 @@ odoo.define("base.abstract_controller_tests", function(require) { testUtils.dom.click(view.$(".descr")); testUtils.dom.click(view.$(".descr2")); assert.verifySteps(["a1", "a2", "method", "method", "descr", "descr2"]); + + view.destroy(); } ); } diff --git a/addons/web/static/tests/views/control_panel_tests.js b/addons/web/static/tests/views/control_panel_tests.js index ee191e4d3a9..f2654e55e1b 100644 --- a/addons/web/static/tests/views/control_panel_tests.js +++ b/addons/web/static/tests/views/control_panel_tests.js @@ -415,5 +415,86 @@ QUnit.module('Views', { controlPanel.destroy(); }); + + QUnit.test('load filter', async function (assert) { + assert.expect(1); + + var controlPanel = await createControlPanel({ + model: 'partner', + arch: "<search></search>", + data: this.data, + searchMenuTypes: ['filter'], + intercepts: { + load_filters: function (ev) { + ev.data.on_success([ + { + user_id: [2,"Mitchell Admin"], + name: 'sorted filter', + id: 5, + context: {}, + sort: "[\"foo\", \"-bar\"]", + domain: "[('user_id', '=', uid)]", + } + ]); + } + } + }); + + _.each(controlPanel.exportState().filters, function (filter) { + if (filter.type === 'favorite') { + assert.deepEqual(filter.orderedBy, + [{ + name: 'foo', + asc: true, + }, { + name: 'bar', + asc: false, + }], + 'the filter should have the right orderedBy values'); + } + }); + + controlPanel.destroy(); + }); + + QUnit.test('save filter', async function (assert) { + assert.expect(1); + + var controlPanel = await createControlPanel({ + model: 'partner', + arch: "<search></search>", + data: this.data, + searchMenuTypes: ['filter'], + intercepts: { + create_filter: function (ev) { + assert.strictEqual(ev.data.filter.sort, "[\"foo\",\"bar desc\"]", + 'The right format for the string "sort" should be sent to the server'); + }, + get_controller_query_params: function (ev) { + ev.data.callback({ + orderedBy: [ + { + name: 'foo', + asc: true, + }, { + name: 'bar', + asc: false, + } + ] + }); + } + } + }); + + controlPanel._onNewFavorite({ + data: { + description: 'Morbier', + type: 'favorite', + }, + stopPropagation: function () {return;} + }); + + controlPanel.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 7af2892ac33..0e7e7ff0a2d 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -1007,6 +1007,95 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('Unload Filter, reset display, load another filter', function (assert) { + assert.expect(18); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + // Equivalent to unload the filter + var reloadParams = { + context: {}, + }; + pivot.reload(reloadParams); + // collapse all headers + pivot.$('.o_pivot_header_cell_opened').click(); + pivot.$('.o_pivot_header_cell_opened').click(); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed').length, 1, + 'The column should not be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 0, + 'There should not be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 0, + 'There should not be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed').length, 1, + 'The row should not be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 0, + 'There should not be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 0, + 'There should not be a row "xpad"'); + + // Equivalent to load another filter + reloadParams = { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }; + pivot.reload(reloadParams); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); @@ -1441,7 +1530,7 @@ QUnit.module('Views', { '<field name="foo" type="measure"/>' + '</pivot>', 'partner,false,search': '<search></search>', - }, + } }); actionManager.doAction({ @@ -1512,6 +1601,12 @@ QUnit.module('Views', { ]; checkCellValues(results); + $('.o_search_options button:contains("Favorites")').click(); + var $favorites = $('.dropdown-menu.o_favorites_menu'); + $favorites.find('a.o_save_search').click(); + $favorites.find('.o_input').val('Fav').trigger('input'); + $favorites.find('button').click(); + unpatchDate(); actionManager.destroy(); }); diff --git a/addons/web/static/tests/views/search_filter_menu_tests.js b/addons/web/static/tests/views/search_filter_menu_tests.js index e907d70111e..92d49cc23ae 100644 --- a/addons/web/static/tests/views/search_filter_menu_tests.js +++ b/addons/web/static/tests/views/search_filter_menu_tests.js @@ -1,9 +1,10 @@ odoo.define('web.search_filters_menu_tests', function (require) { "use strict"; +var Domain = require('web.Domain'); var FilterMenu = require('web.FilterMenu'); +var pyUtils = require('web.py_utils'); var testUtils = require('web.test_utils'); -var Domain = require('web.Domain'); function createFilterMenu(filters, fields, params) { params = params || {}; @@ -134,33 +135,70 @@ QUnit.module('FilterMenu', { filterMenu.destroy(); }); - QUnit.skip('commit search with an extended proposition with field char does not cause a crash', function (assert) { - assert.expect(0); + QUnit.test('commit search with an extended proposition with field char does not cause a crash', function (assert) { + assert.expect(6); this.fields = {many2one_field: {string: "Trululu", type: "many2one", relation: 'partner', selectable: true, searchable: true}}; + var expectedDomains = [ + [['many2one_field','ilike', `a`]], + [['many2one_field','ilike', `"a"`]], + [['many2one_field','ilike', `'a'`]], + [['many2one_field','ilike', `'`]], + [['many2one_field','ilike', `"`]], + [['many2one_field','ilike', `\\`]], + ]; + var filterMenu = createFilterMenu([], this.fields, { intercepts: { new_filters: function (ev) { var filter = ev.data.filters[0]; - Domain.prototype.stringToArray(filter.domain); + // this step combine a tokenization/parsing followed by a string formatting + var domain = pyUtils.assembleDomains([filter.domain]); + domain = Domain.prototype.stringToArray(domain); + assert.deepEqual(domain, expectedDomains.shift()); }, } }); - // open menu dropdown and custom filter submenu, select trululu field and enter string "a", then click apply + // open menu dropdown and custom filter submenu, select trululu field and enter string `a`, then click apply testUtils.dom.click(filterMenu.$('span.fa-filter')); testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); - testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), "a"); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `a`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + + // open custom filter submenu, select trululu field and enter string `"a"`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `"a"`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + // open custom filter submenu, select trululu field and enter string `'a'`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `'a'`); testUtils.dom.click(filterMenu.$('.o_apply_filter')); + // open custom filter submenu, select trululu field and enter string `'`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `'`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); - // open custom filter submenu, select trululu field and enter string "'a'", then click apply + // open custom filter submenu, select trululu field and enter string `"`, then click apply testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); - testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), '"a"'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `"`); testUtils.dom.click(filterMenu.$('.o_apply_filter')); + + // open custom filter submenu, select trululu field and enter string `\`, then click apply + testUtils.dom.click(filterMenu.$('.o_add_custom_filter')); + testUtils.fields.editSelect(filterMenu.$('.o_filter_condition select.o_input.o_searchview_extended_prop_field'), 'many2one_field'); + testUtils.fields.editInput(filterMenu.$('.o_filter_condition .o_searchview_extended_prop_value input'), `\\`); + testUtils.dom.click(filterMenu.$('.o_apply_filter')); + filterMenu.destroy(); }); }); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index 87484bf855d..4916834e1bc 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -750,10 +750,10 @@ QUnit.module('Search View', { create_filter: function (ev) { assert.equal( ev.data.filter.domain, - "['&', " + - "('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d')), " + - "('date_field', '<', (context_today() + relativedelta(days = 1)).strftime('%Y-%m-%d'))"+ - "]"); + `["&", ` + + `("date_field", ">=", (context_today() + relativedelta()).strftime("%Y-%m-%d")), ` + + `("date_field", "<", (context_today() + relativedelta(days = 1)).strftime("%Y-%m-%d"))`+ + `]`); }, }, }); @@ -841,7 +841,7 @@ QUnit.module('Search View', { data: this.data, intercepts: { create_filter: function (ev) { - assert.ok(ev.data.filter.domain === "[['foo', 'ilike', 'a']]"); + assert.ok(ev.data.filter.domain === `[["foo", "ilike", "a"]]`); }, }, }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 1df70b03acc..27275e7fd80 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -443,7 +443,7 @@ QUnit.module('Views', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, "[('bar', '=', True)]", + assert.deepEqual(filter.domain, `[("bar", "=", True)]`, "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index e281d9f9ecc..41de79b3c24 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -159,6 +159,10 @@ class IrUiView(models.Model): out.tail = el.tail return out + @api.model + def _set_noupdate(self): + self.sudo().mapped('model_data_id').write({'noupdate': True}) + @api.multi def save(self, value, xpath=None): """ Update a view section. The view section may embed fields to write @@ -195,16 +199,12 @@ class IrUiView(models.Model): new_arch = self.replace_arch_section(xpath, arch_section) old_arch = etree.fromstring(self.arch.encode('utf-8')) if not self._are_archs_equal(old_arch, new_arch): - self.sudo().model_data_id.write({'noupdate': True}) # TODO check if we remove this + self._set_noupdate() self.write({'arch': self._pretty_arch(new_arch)}) @api.model def _view_get_inherited_children(self, view, options): - extensions = view.inherit_children_ids - if not options: - # only active children - extensions = extensions.filtered(lambda view: view.active) - return extensions + return view.inherit_children_ids @api.model def _view_obj(self, view_id): @@ -252,6 +252,9 @@ class IrUiView(models.Model): views_to_return += self._views_get(called_view, options=options, bundles=bundles) extensions = self._view_get_inherited_children(view, options) + if not options: + # only active children + extensions = extensions.filtered(lambda view: view.active) # Keep options in a deterministic order regardless of their applicability for extension in extensions.sorted(key=lambda v: v.id): diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js index 3db30752277..0cb451c100d 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js @@ -42,7 +42,6 @@ var KeyboardPlugin = AbstractPlugin.extend({ range = direction === 'prev' ? this._insertInvisibleCharAfterSingleBR(range) : range; range = this._rerangeOutOfBR(range, direction); range = this._cleanRangeAfterDeletion(range); - range = this._replaceEmptyParentWithEmptyP(range); return range; }, /** @@ -219,8 +218,15 @@ var KeyboardPlugin = AbstractPlugin.extend({ * @returns {Boolean} true if case handled */ _handleDeletion: function (direction) { - var didDeleteNodes = this.context.invoke('HelperPlugin.deleteSelection') var range = this.context.invoke('editor.createRange'); + range = direction === 'prev' && this._replaceEmptyParentWithEmptyP(range); + if (range) { + range = this._afterDeletion(range, direction); + range.select(); + return true; + } + var didDeleteNodes = this.context.invoke('HelperPlugin.deleteSelection'); + range = this.context.invoke('editor.createRange'); var wasOnStartOfBR = direction === 'prev' && !range.so && range.sc.tagName === 'BR'; this._removeNextEmptyUnbreakable(range.sc); @@ -837,23 +843,27 @@ var KeyboardPlugin = AbstractPlugin.extend({ * * @private * @param {Object} range - * @returns {Object} range + * @returns {Object|undefined} range */ _replaceEmptyParentWithEmptyP: function (range) { - if (range.sc === this.editable || range.sc.parentNode === this.editable) { - return range; + var node = range.sc.childElementCount === 1 && range.sc.firstChild.tagName === 'BR' ? range.sc.firstChild : range.sc; + if (node === this.editable || !node.parentNode || node.parentNode === this.editable) { + return; } - var node = dom.isVoid(range.sc) && range.sc.parentNode ? range.sc.parentNode : range.sc; - var parentOnlyHasNode = node.parentNode && this.context.invoke('HelperPlugin.onlyContains', node.parentNode, node); - if (dom.isEmpty(node) && node.tagName !== 'LI' && parentOnlyHasNode) { + if ( + dom.isEmpty(node) && + this.context.invoke('HelperPlugin.onlyContains', node.parentNode, node) && + ['LI', 'P'].indexOf(node.parentNode.tagName) === -1 + ) { var emptyP = this.document.createElement('p'); var br = this.document.createElement('br'); $(emptyP).append(br); - $(node).before(emptyP).remove(); + $(node.parentNode).before(emptyP).remove(); range.sc = range.ec = br; range.so = range.eo = 0; + return range.collapse(true); } - return range; + return; }, /** * Replace all leading space from a text node with one non-breakable space. diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js index b4d484af805..e60cfbe8039 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js @@ -151,6 +151,9 @@ var MediaPlugin = AbstractPlugin.extend({ * @param {Object} data contains the media to insert */ insertMedia: function (previous, data) { + if (!data.media) { + return; + } var newMedia = data.media; this._wrapCommand(function () { this.$editable.focus(); diff --git a/addons/web_editor/static/src/scss/web_editor.variables.scss b/addons/web_editor/static/src/scss/web_editor.variables.scss index a66e3516c77..e317fe7616b 100644 --- a/addons/web_editor/static/src/scss/web_editor.variables.scss +++ b/addons/web_editor/static/src/scss/web_editor.variables.scss @@ -117,10 +117,17 @@ $o-checklist-before-size: 13px; color: #555; background-color: #fff; border-radius: 0; + &.is-invalid { border-color: $danger; } } + .input-group .form-control { + height: auto; + } + .input-group-text { + background-color: #e9ecef; + } .was-validated { .form-control:invalid { @@ -143,10 +150,14 @@ $o-checklist-before-size: 13px; border-radius: 0; background-color: $o-we-color-paper; + .modal-header { + border-bottom-color: #e9ecef; + } .modal-body { background-color: white; } .modal-footer { + border-top-color: #e9ecef; text-align: left; } } @@ -154,23 +165,23 @@ $o-checklist-before-size: 13px; @mixin o-w-preserve-tabs { .nav-tabs { - border-bottom: 1px solid gray('200'); + border-bottom: 1px solid #e9ecef; > li { > a { line-height: 1.5; color: $o-we-color-light; + &:hover { - border-color: gray('200'); + border-color: #dee2e6; } - } - &.active > a { - &, &:hover, &:focus { - color: $o-we-color-normal; - background-color: $o-we-color-paper; - border-color: gray('200'); - border-bottom-color: transparent; + &.active { + &, &:hover, &:focus { + color: $o-we-color-normal; + background-color: $o-we-color-paper; + border-color: #dee2e6 #dee2e6 #FFFFFF; + } } } } @@ -182,7 +193,7 @@ $o-checklist-before-size: 13px; border-radius: 0; font-weight: normal; text-transform: none; - @include button-size(.375rem, .75rem, 1rem, 1.5, 0); + @include button-size(.375rem, .75rem, 0.875rem, 1.5, 0); &.btn-primary { @include button-variant($o-brand-primary, $o-brand-primary); @@ -207,13 +218,6 @@ $o-checklist-before-size: 13px; &.btn-danger { @include button-variant($o-we-color-danger, $o-we-color-danger); } - - &.btn-lg { - @include button-size(.5rem, 1rem, 1.25rem, 1.5, 0); - } - &.btn-sm { - @include button-size(.25rem, .5rem, .875rem, 1.5, 0); - } } } diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 51c3d5412ff..aa8b5a2f094 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -45,7 +45,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_size_x">Background width</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -56,7 +56,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_size_y">Background height</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -71,7 +71,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_pos_x">Horizontal</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control " id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -82,7 +82,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_pos_y">Vertical</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> diff --git a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js index 14f06ec2e9d..56ceb27a025 100644 --- a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js +++ b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js @@ -3093,7 +3093,7 @@ var keyboardTestsBackspace = [{ }, }, { - name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE", + name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE -> 'a'", content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>', steps: [{ start: 'p:eq(1)->3', @@ -3102,19 +3102,35 @@ var keyboardTestsBackspace = [{ key: 'BACKSPACE', }, { key: 'BACKSPACE', + }, { + key: 'a', }], test: { - content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br></p></td></tr></tbody></table>', - start: "p:eq(1):contents()[0]->0", + content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>a</p></td></tr></tbody></table>', + start: "p:eq(1):contents()[0]->1", }, }, { - name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE -> 'a'", - content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>', + name: "in h1: BACKSPACE on full selection -> 'a'", + content: '<h1>dom to delete</h1>', steps: [{ - start: 'p:eq(1)->3', + start: 'h1:contents()[0]->0', + end: 'h1:contents()[0]->13', key: 'BACKSPACE', }, { + key: 'a', + }], + test: { + content: '<h1>a</h1>', + start: "h1:contents()[0]->1", + }, + }, + { + name: "in h1: BACKSPACE on full selection -> BACKSPACE -> 'a'", + content: '<h1>dom to delete</h1>', + steps: [{ + start: 'h1:contents()[0]->0', + end: 'h1:contents()[0]->13', key: 'BACKSPACE', }, { key: 'BACKSPACE', @@ -3122,8 +3138,25 @@ var keyboardTestsBackspace = [{ key: 'a', }], test: { - content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>a</p></td></tr></tbody></table>', - start: "p:eq(1):contents()[0]->1", + content: '<p>a</p>', + start: "p:contents()[0]->1", + }, + }, + { + name: "in h1: BACKSPACE on full selection -> DELETE -> 'a'", + content: '<h1>dom to delete</h1>', + steps: [{ + start: 'h1:contents()[0]->0', + end: 'h1:contents()[0]->13', + key: 'BACKSPACE', + }, { + key: 'DELETE', + }, { + key: 'a', + }], + test: { + content: '<h1>a</h1>', + start: "h1:contents()[0]->1", }, }, diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index a2d1d332f80..13e82dc5c12 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -283,24 +283,36 @@ class Website(Home): views = views.sorted(key=lambda v: (v.inherit_id.id, v.name)) return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) - @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True) + @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True, csrf=False) def reset_template(self, templates, redirect='/', **kwargs): + """ This method will try to reset a list of broken views ids. + It will read the original `arch` from the view's XML file (arch_fs). + Views without an `arch_fs` can't be reset, except views created when + dropping a snippet in specific oe_structure that create an inherited + view doing an xpath. + Note: The `arch_fs` field is automatically erased when there is a + write on the `arch` field. + + This method is typically useful to reset specific views. In that case we + read the XML file from the generic view. + """ templates = request.httprequest.form.getlist('templates') - modules_to_update = [] for temp_id in templates: view = request.env['ir.ui.view'].browse(int(temp_id)) - if view.page: + if 'oe_structure' in view.key: + # Particular xpathing view created in edit mode + view.unlink() continue - view.model_data_id.write({ - 'noupdate': False - }) - if view.model_data_id.module not in modules_to_update: - modules_to_update.append(view.model_data_id.module) + xml_view = view._get_original_view() # view might already be the xml_view + if xml_view.arch_fs: + view_file_arch = xml_view.with_context(read_arch_from_file=True).arch + # Deactivate COW to not fix a generic view by creating a specific + view.with_context(website_id=None).arch_db = view_file_arch + if view == xml_view: + view.model_data_id.write({ + 'noupdate': False + }) - if modules_to_update: - modules = request.env['ir.module.module'].sudo().search([('name', 'in', modules_to_update)]) - if modules: - modules.button_immediate_upgrade() return request.redirect(redirect) @http.route(['/website/publish'], type='json', auth="public", website=True) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index be3085eb835..2be6c571a37 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +from lxml import etree import traceback import os import unittest @@ -12,14 +13,14 @@ import werkzeug.routing import werkzeug.utils import odoo -from odoo import api, models +from odoo import api, models, registry from odoo import SUPERUSER_ID from odoo.http import request from odoo.tools import config -from odoo.exceptions import QWebException from odoo.tools.safe_eval import safe_eval from odoo.osv.expression import FALSE_DOMAIN, OR +from odoo.addons.base.models.qweb import QWebException from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype from odoo.addons.portal.controllers.portal import _build_url_w_params @@ -185,7 +186,7 @@ class Http(models.AbstractModel): # if parent excplicitely returns a plain response, then we don't touch it return response except Exception as e: - if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')): + if 'werkzeug' in config['dev_mode']: raise e exception = e @@ -207,19 +208,9 @@ class Http(models.AbstractModel): if isinstance(exception, QWebException): values.update(qweb_exception=exception) - if isinstance(exception.qweb.get('cause'), odoo.exceptions.AccessError): + if type(exception.error) == odoo.exceptions.AccessError: code = 403 - if code == 500: - logger.error("500 Internal Server Error:\n\n%s", values['traceback']) - if 'qweb_exception' in values: - view = request.env["ir.ui.view"] - views = view._views_get(exception.qweb['template']) - to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and view.arch_fs) - values['views'] = to_reset - elif code == 403: - logger.warn("403 Forbidden:\n\n%s", values['traceback']) - values.update( status_message=werkzeug.http.HTTP_STATUS_CODES[code], status_code=code, @@ -233,10 +224,51 @@ class Http(models.AbstractModel): if not request.uid: cls._auth_method_public() - try: - html = request.env['ir.ui.view'].render_template('website.%s' % view_id, values) - except Exception: - html = request.env['ir.ui.view'].render_template('website.http_error', values) + with registry(request.env.cr.dbname).cursor() as cr: + env = api.Environment(cr, request.uid, request.env.context) + if code == 500: + logger.error("500 Internal Server Error:\n\n%s", values['traceback']) + View = env["ir.ui.view"] + if 'qweb_exception' in values: + if 'load could not load template' in exception.args: + # When t-calling an inexisting template, we don't have reference to + # the view that did the t-call. We need to find it. + values['views'] = View.search([ + ('type', '=', 'qweb'), + '|', + ('arch_db', 'ilike', 't-call="%s"' % exception.name), + ('arch_db', 'ilike', "t-call='%s'" % exception.name) + ], order='write_date desc', limit=1) + else: + try: + # exception.name might be int, string + exception_template = int(exception.name) + except: + exception_template = exception.name + view = View._view_obj(exception_template) + et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch']) + node = et.xpath(exception.path) + line = node is not None and etree.tostring(node[0], encoding='unicode') + # line = exception.html # FALSE -> contains branding <div t-att-data="request.browse('ok')"/> + if line: + # If QWebException occurs in a child view, the parent view is raised + values['editable'] = request.uid and request.website.is_publisher() + values['views'] = View._views_get(exception_template).filtered( + lambda v: line in v.arch + ) + else: + values['views'] = view + # Keep only views that we can reset + values['views'] = values['views'].filtered( + lambda view: view._get_original_view().arch_fs or 'oe_structure' in view.key + ) + elif code == 403: + logger.warn("403 Forbidden:\n\n%s", values['traceback']) + try: + html = env['ir.ui.view'].render_template('website.%s' % view_id, values) + except Exception: + html = env['ir.ui.view'].render_template('website.http_error', values) + return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8') def binary_content(self, xmlid=None, model='ir.attachment', id=None, field='datas', @@ -250,7 +282,7 @@ class Http(models.AbstractModel): obj = self.env[model].browse(int(id)) if obj and 'website_published' in obj._fields: if self.env[obj._name].sudo().search([('id', '=', obj.id), ('website_published', '=', True)]): - self.sudo() + self = self.sudo() return super(Http, self).binary_content( xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index dc3ebb286c3..27fe7a66437 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -278,6 +278,16 @@ class View(models.Model): return super(View, self).get_view_id(xml_id) @api.multi + def _get_original_view(self): + """Given a view, retrieve the original view it was COW'd from. + The given view might already be the original one. In that case it will + (and should) return itself. + """ + self.ensure_one() + domain = [('key', '=', self.key), ('model_data_id', '!=', None)] + return self.search(domain, limit=1) # Useless limit has multiple xmlid should not be possible + + @api.multi def render(self, values=None, engine='ir.qweb', minimal_qcontext=False): """ Render the template. If website is enabled on request, then extend rendering context with website values. """ new_context = dict(self._context) @@ -371,6 +381,15 @@ class View(models.Model): res['website_id'] = self.env['website'].get_current_website().id return res + @api.model + def _set_noupdate(self): + '''If website is installed, any call to `save` from the frontend will + actually write on the specific view (or create it if not exist yet). + In that case, we don't want to flag the generic view as noupdate. + ''' + if not self._context.get('website_id'): + super(View, self)._set_noupdate() + @api.multi def save(self, value, xpath=None): self.ensure_one() diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py index 9256f80293d..d62ee637675 100644 --- a/addons/website/models/res_partner.py +++ b/addons/website/models/res_partner.py @@ -51,3 +51,10 @@ class Partner(models.Model): # onchange uses the cache to retrieve value, we need to copy computed_value into the initial env for record, record2 in zip(self, self2): record.display_name = record2.display_name + + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(Partner, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 4eeb73c5423..0261b1fc616 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -454,7 +454,8 @@ class Website(models.Model): if website_id: return self.browse(website_id) - domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None + # The format of `httprequest.host` is `domain:port` + domain_name = request and request.httprequest.host or '' country = request.session.geoip.get('country_code') if request and request.session.geoip else False country_id = False @@ -465,9 +466,64 @@ class Website(models.Model): return self.browse(website_id) @tools.cache('domain_name', 'country_id', 'fallback') + @api.model def _get_current_website_id(self, domain_name, country_id, fallback=True): - # sort on country_group_ids so that we fall back on a generic website (empty country_group_ids) - websites = self.search([('domain', '=', domain_name)]).sorted('country_group_ids') + """Get the current website id. + + First find all the websites for which the configured `domain` (after + ignoring a potential scheme) is equal to the given + `domain_name`. If there is only one result, return it immediately. + + If there are no website found for the given `domain_name`, either + fallback to the first found website (no matter its `domain`) or return + False depending on the `fallback` parameter. + + If there are multiple websites for the same `domain_name`, we need to + filter them out by country. We return the first found website matching + the given `country_id`. If no found website matching `domain_name` + corresponds to the given `country_id`, the first found website for + `domain_name` will be returned (no matter its country). + + :param domain_name: the domain for which we want the website. + In regard to the `url_parse` method, only the `netloc` part should + be given here, no `scheme`. + :type domain_name: string + + :param country_id: id of the country for which we want the website + :type country_id: int + + :param fallback: if True and no website is found for the specificed + `domain_name`, return the first website (without filtering them) + :type fallback: bool + + :return: id of the found website, or False if no website is found and + `fallback` is False + :rtype: int or False + + :raises: if `fallback` is True but no website at all is found + """ + def _remove_port(domain_name): + return (domain_name or '').split(':')[0] + + def _filter_domain(website, domain_name, ignore_port=False): + """Ignore `scheme` from the `domain`, just match the `netloc` which + is host:port in the version of `url_parse` we use.""" + # Here we add http:// to the domain if it's not set because + # `url_parse` expects it to be set to correctly return the `netloc`. + website_domain = urls.url_parse(website._get_http_domain()).netloc + if ignore_port: + website_domain = _remove_port(website_domain) + domain_name = _remove_port(domain_name) + return website_domain.lower() == (domain_name or '').lower() + + # Sort on country_group_ids so that we fall back on a generic website: + # websites with empty country_group_ids will be first. + found_websites = self.search([('domain', 'ilike', _remove_port(domain_name))]).sorted('country_group_ids') + # Filter for the exact domain (to filter out potential subdomains) due + # to the use of ilike. + websites = found_websites.filtered(lambda w: _filter_domain(w, domain_name)) + # If there is no domain matching for the given port, ignore the port. + websites = websites or found_websites.filtered(lambda w: _filter_domain(w, domain_name, ignore_port=True)) if not websites: if not fallback: @@ -728,3 +784,16 @@ class Website(models.Model): 'url': '/', 'target': 'self', } + + @api.multi + def _get_http_domain(self): + """Get the domain of the current website, prefixed by http if no + scheme is specified. + + Empty string if no domain is specified on the website. + """ + self.ensure_one() + if not self.domain: + return '' + res = urls.url_parse(self.domain) + return 'http://' + self.domain if not res.scheme else self.domain diff --git a/addons/website/static/src/js/tours/website_tour_reset_password.js b/addons/website/static/src/js/tours/website_tour_reset_password.js new file mode 100644 index 00000000000..307b741ceef --- /dev/null +++ b/addons/website/static/src/js/tours/website_tour_reset_password.js @@ -0,0 +1,172 @@ +odoo.define('website.tour_reset_password', function (require) { +'use strict'; + +var localStorage = require('web.local_storage'); +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); +require('web.dom_ready'); + +var currentDomain = window.location.protocol + '//' + window.location.hostname; +var emailsUrl = '/web#action=mail.action_view_mail_mail&view_type=list'; +var usersUrl = '/web#action=base.action_res_users&view_type=list'; +var resetLinkKey = 'website.tour_reset_password.resetLink'; + +tour.register('website_reset_password', { + test: true, + url: '/web', +}, +[ + { + content: "Change the domain of the websites and go to users page", + trigger: '.oe_topbar_name:contains("Admin")', + run: function () { + // We could do it with the UI but this is not the goal of this test, + // so we just make an RPC to be faster. + + // We change the domain of the website to test that the email that + // will be sent uses the correct domain for its links. + var def1 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1], { + 'domain': "my-test-domain.com", + }], + }); + // We need to change the domain of all the websites otherwise the + // website selector will return the website 2 since the domain we + // set on website 1 doesn't actually match our test server. + var def2 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[2], { + 'domain': "https://domain-not-used.fr", + }], + }); + // Set a website on "Admin" partner to test the URL of the website. + var def3 = rpc.query({ + model: 'res.partner', + method: 'name_search', + kwargs: {'name': 'Mitchell Admin'}, + }).then(function (res) { + return rpc.query({ + 'model': 'res.partner', + 'method': 'write', + 'args': [[res[0][0]], { + 'website_id': 1, + }], + }); + }); + + return $.when(def1, def2, def3).then(function () { + window.location.href = usersUrl; + }); + }, + }, + { + content: "click on Admin", + trigger: '.o_data_cell:contains("Admin")', + }, + { + content: "click on reset password", + trigger: '.btn[name="action_reset_password"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: 'a[name="signup_url"][href^="http://my-test-domain.com"]', + run: function () { + window.location.href = emailsUrl; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("Password reset"):eq(0)', + }, + { + content: "check email has the button", + trigger: 'div.oe_form_field_html[name="body_html"] a:contains("Change password")', + run: function () {}, + }, + { + content: "check the URL is correct too", + trigger: 'div.oe_form_field_html[name="body_html"] a:contains("Change password")[href^="http://my-test-domain.com"]', + run: function () { + // reset the domain of the websites, go to users page + return rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1, 2], { + 'domain': "", + }], + }).then(function () { + window.location.href = usersUrl; + }); + }, + }, + { + content: "click on Admin", + trigger: '.o_data_cell:contains("Admin")', + }, + { + content: "click on reset password", + trigger: '.btn[name="action_reset_password"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: 'a[name="signup_url"][href^="' + currentDomain + '"]', + run: function () { + window.location.href = emailsUrl; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("Password reset"):eq(0)', + }, + { + content: "check the link has the current host, save the link, logout", + trigger: 'div.oe_form_field_html[name="body_html"] a:contains("Change password")[href^="' + currentDomain + '"]', + run: function () { + var link = $('div.oe_form_field_html[name="body_html"] a:contains("Change password")').attr('href'); + localStorage.setItem(resetLinkKey, link); + window.location.href = "/web/session/logout?redirect=/"; + }, + }, + { + content: "go to the reset link", + trigger: 'a[href="/web/login"]', + run: function () { + window.location.href = localStorage.getItem(resetLinkKey); + }, + }, + { + content: "fill new password and submit", + trigger: '.oe_reset_password_form', + run: function () { + var $form = $('.oe_reset_password_form'); + $form.find('input[name="password"]').val('adminadmin'); + // password must be at least 8 if the module `auth_password_policy` + // is installed which is the case on runbot + $form.find('input[name="confirm_password"]').val('adminadmin'); + $form.find('button[type="submit"]').click(); + }, + }, + { + content: "check logged in, and reset admin website", + trigger: '.oe_topbar_name:contains("Admin")', + run: function () { + return rpc.query({ + model: 'res.partner', + method: 'name_search', + kwargs: {'name': 'Admin'}, + }).then(function (res) { + return rpc.query({ + 'model': 'res.partner', + 'method': 'write', + 'args': [[res[0][0]], { + 'website_id': false, + }], + }); + }); + }, + }, +]); +}); diff --git a/addons/website/static/src/scss/website.editor.ui.scss b/addons/website/static/src/scss/website.editor.ui.scss index aa5cba39e00..353be1efed1 100644 --- a/addons/website/static/src/scss/website.editor.ui.scss +++ b/addons/website/static/src/scss/website.editor.ui.scss @@ -34,7 +34,7 @@ &:checked+span { box-shadow: none; - background-image: linear-gradient(0deg, $o-we-color-success, darken($o-we-color-success, 10%)); + background: $o-we-color-success; &:after { margin-left: ($o-we-switch-size*1.8 - $o-we-switch-size) + 0.1; @@ -46,7 +46,7 @@ >input { &:not(:checked)+span { box-shadow: none; - background-image: linear-gradient(0deg, lighten(theme-color('danger'), 5%), darken(theme-color('danger'), 5%)); + background: $o-we-color-danger; } } } diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss index 6865a8b7e79..cb6fbb94fcb 100644 --- a/addons/website/static/src/scss/website.ui.scss +++ b/addons/website/static/src/scss/website.ui.scss @@ -134,10 +134,14 @@ body .modal { cursor: pointer; font-family: $o-we-font-family; + &, .close { + color: white; + } + h4 { - color: lightgray; - font-family:inherit; + font-family: inherit; font-weight: normal; + color: inherit; .fa { margin-left: $grid-gutter-width/2; diff --git a/addons/website/static/src/xml/website.pageProperties.xml b/addons/website/static/src/xml/website.pageProperties.xml index 8ad1d995ce5..348da5e9c17 100644 --- a/addons/website/static/src/xml/website.pageProperties.xml +++ b/addons/website/static/src/xml/website.pageProperties.xml @@ -49,7 +49,7 @@ <div class="form-group row"> <label class="col-form-label col-md-3" for="page_name">Page Name</label> <div class="col-md-9"> - <input type="text" class="form-control form-control-sm" id="page_name" t-att-value="widget.page.name" /> + <input type="text" class="form-control" id="page_name" t-att-value="widget.page.name" /> </div> </div> <div class="form-group warn_about_call"></div> @@ -60,7 +60,7 @@ <div class="input-group-prepend"> <span class="input-group-text" t-att-title="widget.serverUrl"><small><t t-esc="widget.serverUrlTrunc"/></small></span> </div> - <input type="text" class="form-control form-control-sm" id="page_url" t-att-value="widget.page.url" /> + <input type="text" class="form-control" id="page_url" t-att-value="widget.page.url" /> </div> </div> </div> diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 1c57eeab793..85ecad1cba8 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -2,9 +2,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_converter +from . import test_get_current_website from . import test_qweb from . import test_crawl from . import test_ui from . import test_views from . import test_menu from . import test_page +from . import test_website_reset_password diff --git a/addons/website/tests/test_get_current_website.py b/addons/website/tests/test_get_current_website.py new file mode 100644 index 00000000000..a71126cb484 --- /dev/null +++ b/addons/website/tests/test_get_current_website.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged('post_install', '-at_install') +class TestGetCurrentWebsite(TransactionCase): + + def test_01_get_current_website_id(self): + """Make sure `_get_current_website_id works`.""" + + Website = self.env['website'] + + # clean initial state + website1 = Website.browse(1) + website1.domain = '' + website1.country_group_ids = False + + website2 = Website.browse(2) + website2.domain = '' + website2.country_group_ids = False + + country1 = self.env['res.country'].create({'name': "My Country 1"}) + country2 = self.env['res.country'].create({'name': "My Country 2"}) + country3 = self.env['res.country'].create({'name': "My Country 3"}) + country4 = self.env['res.country'].create({'name': "My Country 4"}) + country5 = self.env['res.country'].create({'name': "My Country 5"}) + + country_group_1_2 = self.env['res.country.group'].create({ + 'name': "My Country Group 1-2", + 'country_ids': [(6, 0, (country1 + country2 + country5).ids)], + }) + country_group_3 = self.env['res.country.group'].create({ + 'name': "My Country Group 3", + 'country_ids': [(6, 0, (country3 + country5).ids)], + }) + + # CASE: no domain, no country: get first + self.assertEqual(Website._get_current_website_id('', False), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website1.id) + + # CASE: no domain, given country: get by country + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('', country1.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country2.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website2.id) + + # CASE: no domain, wrong country: get first + self.assertEqual(Website._get_current_website_id('', country4.id), website1.id) + + # CASE: no domain, multiple country: get first + self.assertEqual(Website._get_current_website_id('', country5.id), website1.id) + + # setup domain + website1.domain = 'my-site-1.fr' + website2.domain = 'https://my2ndsite.com:80' + + website1.country_group_ids = False + website2.country_group_ids = False + + # CASE: domain set: get matching domain + self.assertEqual(Website._get_current_website_id('my-site-1.fr', False), website1.id) + + # CASE: domain set: get matching domain (scheme and port supported) + self.assertEqual(Website._get_current_website_id('my-site-1.fr:8069', False), website1.id) + + self.assertEqual(Website._get_current_website_id('my2ndsite.com:80', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com:8069', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', False), website2.id) + + # CASE: domain set, wrong domain: get first + self.assertEqual(Website._get_current_website_id('test.com', False), website1.id) + + # CASE: subdomain: not supported + self.assertEqual(Website._get_current_website_id('www.my2ndsite.com', False), website1.id) + + # CASE: domain set, given country: get by domain in priority + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country1.id), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country2.id), website2.id) + self.assertEqual(Website._get_current_website_id('my-site-1.fr', country3.id), website1.id) + + # CASE: domain set, wrong country: get first for domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country4.id), website2.id) + + # CASE: domain set, multiple country: get first for domain + website1.domain = website2.domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country5.id), website1.id) + + # CASE: overlapping domain: get exact match + website1.domain = 'site-1.com' + website2.domain = 'even-better-site-1.com' + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-better-site-1.com', False), website2.id) + + # CASE: case insensitive + website1.domain = 'Site-1.com' + website2.domain = 'Even-Better-site-1.com' + self.assertEqual(Website._get_current_website_id('sitE-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-beTTer-site-1.com', False), website2.id) + + # CASE: same domain, different port + website1.domain = 'site-1.com:80' + website2.domain = 'site-1.com:81' + self.assertEqual(Website._get_current_website_id('site-1.com:80', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com:81', False), website2.id) + self.assertEqual(Website._get_current_website_id('site-1.com:82', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 9ece6cf4c3d..0416bc72bf7 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -159,6 +159,12 @@ class TestViewSaving(common.TransactionCase): h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund", type='char'))), ) ), encoding='unicode') + + self.view_id.with_context(website_id=1).save(value=replacement, xpath='/div/div[2]') + self.assertFalse(imd.noupdate, "view's xml_id shouldn't be set to 'noupdate' in a website context as `save` method will COW") + # remove newly created COW view so next `save()`` wont be redirected to COW view + self.env['website'].with_context(website_id=1).viewref(self.view_id.key).unlink() + self.view_id.save(value=replacement, xpath='/div/div[2]') # the xml_id of the view should be flagged as 'noupdate' @@ -622,6 +628,76 @@ class TestCowViewSaving(common.TransactionCase): views = View.with_context(website_id=1).get_related_views('B') self.assertEqual(views.mapped('key'), ['B', 'I', 'II'], "Should only return the specific tree") + def test_get_related_views_tree_recursive_t_call_and_inherit_inactive(self): + """ If a view A was doing a t-call on a view B and view B had view C as child. + And view A had view D as child. + And view D also t-call view B (that as mentionned above has view C as child). + And view D was inactive (`d` in bellow schema). + + Then COWing C to set it as inactive would make `get_related_views()` on A to return + both generic active C and COW inactive C. + (Typically the case for Customize show on /shop for Wishlist, compare..) + See commit message for detailed explanation. + """ + # A -> B + # | ^ \ + # | | C + # d ___| + + View = self.env['ir.ui.view'] + Website = self.env['website'] + + products = View.create({ + 'name': 'Products', + 'type': 'qweb', + 'key': '_website_sale.products', + 'arch': ''' + <div id="products_grid"> + <t t-call="_website_sale.products_item"/> + </div> + ''', + }) + + products_item = View.create({ + 'name': 'Products item', + 'type': 'qweb', + 'key': '_website_sale.products_item', + 'arch': ''' + <div class="product_price"/> + ''', + }) + + add_to_wishlist = View.create({ + 'name': 'Wishlist', + 'active': True, + 'customize_show': True, + 'inherit_id': products_item.id, + 'key': '_website_sale_wishlist.add_to_wishlist', + 'arch': ''' + <xpath expr="//div[hasclass('product_price')]" position="inside"></xpath> + ''', + }) + + products_list_view = View.create({ + 'name': 'List View', + 'active': False, # <- That's the reason of why this behavior needed a fix + 'customize_show': True, + 'inherit_id': products.id, + 'key': '_website_sale.products_list_view', + 'arch': ''' + <div id="products_grid" position="replace"> + <t t-call="_website_sale.products_item"/> + </div> + ''', + }) + + views = View.with_context(website_id=1).get_related_views('_website_sale.products') + self.assertEqual(views, products + products_item + add_to_wishlist + products_list_view, "The four views should be returned.") + add_to_wishlist.with_context(website_id=1).write({'active': False}) # Trigger cow on hierarchy + add_to_wishlist_cow = Website.with_context(website_id=1).viewref(add_to_wishlist.key) + views = View.with_context(website_id=1).get_related_views('_website_sale.products') + self.assertEqual(views, products + products_item + add_to_wishlist_cow + products_list_view, "The generic wishlist view should have been replaced by the COW one.") + def test_cow_inherit_children_order(self): """ COW method should loop on inherit_children_ids in correct order when copying them on the new specific tree. diff --git a/addons/website/tests/test_website_reset_password.py b/addons/website/tests/test_website_reset_password.py new file mode 100644 index 00000000000..b9fe135d35f --- /dev/null +++ b/addons/website/tests/test_website_reset_password.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +import odoo +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteResetPassword(HttpCase): + + def test_01_website_reset_password_tour(self): + """The goal of this test is to make sure the reset password works.""" + + # We override unlink because we don't want the email to be auto deleted + # if the send works. + MailMail = odoo.addons.mail.models.mail_mail.MailMail + + # We override send_mail because in HttpCase on runbot we don't have an + # SMTP server, so if force_send is set, the test is going to fail. + MailTemplate = odoo.addons.mail.models.mail_template.MailTemplate + original_send_mail = MailTemplate.send_mail + + def my_send_mail(*args, **kwargs): + kwargs.update(force_send=False) + return original_send_mail(*args, **kwargs) + + with patch.object(MailMail, 'unlink', lambda self: None), patch.object(MailTemplate, 'send_mail', my_send_mail): + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('website_reset_password')", "odoo.__DEBUG__.services['web_tour.tour'].tours.website_reset_password.ready", login="admin") diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 8809cd292d4..3ec2fe607ef 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -17,6 +17,12 @@ </xpath> </template> +<template id="assets_common" name="Website Assets Common" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website/static/src/js/tours/website_tour_reset_password.js"></script> + </xpath> +</template> + <template id="assets_backend" inherit_id="web.assets_backend" name="Website Backend Assets (used in backend interface)"> <xpath expr="//link[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.backend.scss"/> @@ -934,17 +940,14 @@ </h4> <div id="error_qweb" class="collapse"> <div class="card-body"> - <p t-if="exception.qweb.get('message')"> + <p t-if="exception.message"> <strong>Error message:</strong> - <pre t-esc="exception.qweb.get('message')"/> + <pre t-esc="exception.message"/> </p> <p> - The error occured while rendering the template <code t-esc="qweb_exception.qweb.get('template')"/> - <t t-if="'expression' in qweb_exception.qweb">and evaluating the following expression: <code t-esc="qweb_exception.qweb['expression']"/></t> + The error occured while rendering the template <code t-esc="qweb_exception.name"/> + <t t-if="qweb_exception.html">and evaluating the following expression: <code t-esc="qweb_exception.html"/></t> </p> - <t t-if="'node' in qweb_exception.qweb"> - <pre id="exception_node" t-esc="qweb_exception.pretty_xml()"/> - </t> </div> </div> </div> @@ -1008,62 +1011,74 @@ </template> <template id="500"> - <!-- This template should not use any variable except those provided by website.ir_http._handle_exception --> + <!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> + <!-- This template should not use any variable except those provided by website.ir_http._handle_exception --> + <!-- no request.crsf_token, no theme style, no assets, ... cursor can be broken during rendering ! --> + <!-- see test_05_reset_specific_view_controller_broken_request --> + <!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> <html> <head> <title t-esc="status_message">Internal Server Error</title> <t t-set="debug" t-value="True"/> - <t t-call-assets="web.assets_common" t-js="false"/> - <t t-call-assets="web.assets_frontend" t-js="false"/> - <t t-call-assets="web.assets_common" t-css="false"/> - <t t-call-assets="web.assets_frontend" t-css="false"/> - <script> - $(document).ready(function() { - var button = $('#reset_templates_button'); - button.click(function() { - var dialog = $('#reset_template_confirmation').modal('show'); - var input = dialog.find('input[type="text"]').val('').focus(); - var dialog_form = dialog.find('form'); - dialog_form.submit(function() { - if (input.val() == dialog.find('.confirm_word').text()) { - dialog.modal('hide'); - button.prop('disabled', true).text('Working...'); - $('#reset_templates_form').trigger('submit'); - } else { - input.val('').focus(); - } + + <link rel="stylesheet" href="/web/static/lib/bootstrap/css/bootstrap.css"/> + <script src="/web/static/lib/jquery/jquery.js" type="text/javascript"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/util.js"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/collapse.js"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/modal.js"/> + + <t t-if='views'> + <script> + $(document).ready(function() { + var button = $('#reset_templates_button'); + button.click(function() { + var dialog = $('#reset_template_confirmation').modal('show'); + var input = dialog.find('input[type="text"]').val('').focus(); + var dialog_form = dialog.find('form'); + dialog_form.submit(function() { + if (input.val() == dialog.find('.confirm_word').text()) { + dialog.modal('hide'); + button.prop('disabled', true).text('Working...'); + if ($("input[name='templates']:checked").length) { + $('#reset_templates_form').attr('action', '/website/reset_templates'); + $('#reset_templates_form').trigger('submit'); + } + } else { + input.val('').focus(); + } + return false; + }); return false; }); - return false; }); - }); - </script> + </script> + </t> </head> <body> - <div role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true"> + <div t-if="views" role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true"> <div class="modal-dialog"> <form role="form"> - <div class="modal-content"> - <header class="modal-header"> - <h3 class="modal-title">Reset templates</h3> - <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> - </header> - <main class="modal-body"> - <div class="form-group row mb0"> - <label for="page-name" class="col-md-9 col-form-label"> - <p>The selected templates will be reset to their factory settings.</p> - <p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p> - </label> - <div class="col-md-3 mt16"> - <input type="text" class="form-control" required="required" placeholder="yes"/> + <div class="modal-content"> + <header class="modal-header"> + <h3 class="modal-title">Reset templates</h3> + <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> + </header> + <main class="modal-body"> + <div class="form-group row mb0"> + <label for="page-name" class="col-md-9 col-form-label"> + <p>The selected templates will be reset to their factory settings.</p> + <p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p> + </label> + <div class="col-md-3 mt16"> + <input type="text" id="page-name" class="form-control" required="required" placeholder="yes"/> + </div> </div> - </div> - </main> - <footer class="modal-footer"> - <button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button> - <input type="submit" value="Confirm" class="btn btn-primary"/> - </footer> - </div> + </main> + <footer class="modal-footer"> + <button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button> + <input type="submit" value="Confirm" class="btn btn-primary"/> + </footer> + </div> </form> </div> </div> @@ -1085,18 +1100,20 @@ <div class="oe_structure"> <h1 class="container mt32"><t t-esc="status_code"/>: <t t-esc="status_message"/></h1> </div> - <div class="container" t-if="views"> - <div class="alert alert-danger" t-if="qweb_exception and editable" role="alert"> + <div class="container" t-if="views and editable"> + <div class="alert alert-danger" t-if="debug" role="alert"> <h4>Template fallback</h4> - <p>An error occured while rendering the template <code t-esc="qweb_exception.qweb['template']"/>.</p> + <p>An error occured while rendering the template <code t-esc="qweb_exception.name"/>.</p> <p>If this error is caused by a change of yours in the templates, you have the possibility to reset one or more templates to their <strong>factory settings</strong>.</p> - <form action="/website/reset_templates" method="post" id="reset_templates_form"> - <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> + <form action="#" method="post" id="reset_templates_form"> <ul class="oe_template_fallback"> <li t-foreach="views" t-as="view"> <label> <input type="checkbox" name="templates" t-att-value="view.id" t-att-checked="'checked' if view_first else None"/> <t t-esc="view.name"/> + <span t-if="not view._get_original_view()" class="text-danger"> + (!!! This view will be removed ) + </span> </label> </li> </ul> diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 67272576845..429e8f9d822 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -170,7 +170,7 @@ class WebsiteAccount(CustomerPortal): "website_crm_partner_assign.portal_my_opportunity", { 'opportunity': opp, 'user_activity': opp.sudo().activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1], - 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc'), + 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc, name desc, id desc'), 'activity_types': request.env['mail.activity.type'].sudo().search([]), 'states': request.env['res.country.state'].sudo().search([]), 'countries': request.env['res.country'].sudo().search([]), diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index e1466c5c4c5..819fc764962 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -510,12 +510,12 @@ </div> <div class="col-md text-md-right"> <div class="d-inline-block"> - <small class="mr-2"><b>Stage:</b></small> - <div t-foreach="stages" t-as="stage" class="float-right"> + <small class="mr-2 mt-1 float-left"><b>Stage:</b></small> + <div t-foreach="stages[::-1]" t-as="stage" class="float-left"> + <i t-if="not stage_first" class="fa fa-chevron-right ml-2 mr-1 small" style="opacity: 0.5"/> <button type="button" t-att-data="stage.id" t-att-opp="opportunity.id" t-attf-class="btn btn-sm px-2 opp-stage-button #{'btn-secondary' if opportunity.stage_id.name != stage.name else 'btn-primary disabled'}"> <span t-field="stage.name"/> </button> - <i t-if="not stage_first" class="fa fa-chevron-right ml-1 mr-2 small" style="opacity: 0.5"/> </div> </div> </div> diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py index 159272983e5..c95d7d29059 100644 --- a/addons/website_event_track/models/event.py +++ b/addons/website_event_track/models/event.py @@ -66,26 +66,26 @@ class Event(models.Model): for event in self: if 'website_track' in vals: if vals['website_track']: - for sequence, (name, url, xml_id, menu_type) in enumerate(self._get_track_menu_entries()): - menu = super(Event, self)._create_menu(sequence, name, url, xml_id) - self.env['website.event.menu'].create({ + for sequence, (name, url, xml_id, menu_type) in enumerate(event._get_track_menu_entries()): + menu = super(Event, event)._create_menu(sequence, name, url, xml_id) + event.env['website.event.menu'].create({ 'menu_id': menu.id, - 'event_id': self.id, + 'event_id': event.id, 'menu_type': menu_type, }) else: - self.track_menu_ids.mapped('menu_id').unlink() + event.track_menu_ids.mapped('menu_id').unlink() if 'website_track_proposal' in vals: if vals['website_track_proposal']: - for sequence, (name, url, xml_id, menu_type) in enumerate(self._get_track_proposal_menu_entries()): - menu = super(Event, self)._create_menu(sequence, name, url, xml_id) - self.env['website.event.menu'].create({ + for sequence, (name, url, xml_id, menu_type) in enumerate(event._get_track_proposal_menu_entries()): + menu = super(Event, event)._create_menu(sequence, name, url, xml_id) + event.env['website.event.menu'].create({ 'menu_id': menu.id, - 'event_id': self.id, + 'event_id': event.id, 'menu_type': menu_type, }) else: - self.track_proposal_menu_ids.mapped('menu_id').unlink() + event.track_proposal_menu_ids.mapped('menu_id').unlink() def _get_track_menu_entries(self): self.ensure_one() diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index abcf91f786b..482a7d9541b 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -706,9 +706,13 @@ <span t-field="question.content" class="oe_no_empty"/> <div> - <div class="btn-group"> + <div class="btn-group mb8"> <a t-attf-class="btn btn-outline-primary #{not question.can_answer and 'karma_required text-muted' or ''}" t-att-data-karma="question.forum_id.karma_answer" - href="#post_reply"><i class=" fa fa-reply"/> <b>Reply</b></a> + href="#post_reply"><i class="fa fa-reply"/> <b>Reply</b> + </a> + <a t-attf-class="btn btn-outline-primary #{not question.can_comment and 'karma_required text-muted' or ''}" t-att-data-karma="question.karma_comment" t-att-data-toggle="question.can_comment and 'collapse' or None" + t-attf-href="#comment#{ question._name.replace('.','') + '-' + str(question.id) }"><i class=" fa fa-comment-o"/> <b>Comment</b> + </a> <a href="javascript:void(0)" class="oe_social_share btn btn-outline-primary" t-attf-data-hashtags="#question"><i class="fa fa-share-alt"/> <b>Share</b> </a> @@ -756,15 +760,14 @@ <t t-set="icon" t-value="'fa-check'"/> <t t-set="karma" t-value="not question.can_moderate and question.forum_id.karma_moderate or 0"/> </t> - <a t-if="question.active" href="#" t-call="website_forum.link_button"> + <t t-if="question.active" href="#" t-call="website_forum.link_button"> <t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/flag'"/> <t t-set="title">Flag</t> <t t-set="inDropdown" t-value="False"/> <t t-set="icon" t-value="not question.can_flag and 'fa-flag-o' or 'fa-flag'"/> <t t-set="karma" t-value="not question.can_flag and question.forum_id.karma_flag or 0"/> <t t-set="classes" t-value="'flag'"/> - - </a> + </t> </div> </div> @@ -963,14 +966,14 @@ <t t-set="inDropdown" t-value="True"/> <t t-set="icon" t-value="'fa-trash-o text-primary'"/> <t t-set="classes" t-value="'comment_delete border-bottom'"/> - <t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/> + <t t-set="karma" t-value="not object.can_unlink and answer.karma_unlink or 0"/> </t> <t t-call="website_forum.link_button"> <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/> <t t-set="label">Convert as a answer</t> <t t-set="inDropdown" t-value="True"/> <t t-set="icon" t-value="'fa-magic text-primary'"/> - <t t-set="karma" t-value="not answer.can_comment_convert and answer.karma_comment_convert or 0"/> + <t t-set="karma" t-value="not object.can_comment_convert and answer.karma_comment_convert or 0"/> </t> </div> </div> diff --git a/addons/website_profile/controllers/main.py b/addons/website_profile/controllers/main.py index ec587f59eca..ea0ccd3f0cb 100644 --- a/addons/website_profile/controllers/main.py +++ b/addons/website_profile/controllers/main.py @@ -36,7 +36,6 @@ class WebsiteProfile(http.Controller): content = base64.b64encode(image) dictheaders = dict(headers) if headers else {} dictheaders['Content-Type'] = 'image/png' - headers = list(dictheaders.items()) if not (width or height): suffix = field.split('_')[-1] if '_' in field else 'large' if suffix in ('small', 'medium', 'large', 'big'): @@ -45,6 +44,9 @@ class WebsiteProfile(http.Controller): def _check_user_profile_access(self, user_id): user_sudo = request.env['res.users'].sudo().browse(user_id) + # User can access - no matter what - his own profile + if user_sudo.id == request.env.user.id: + return user_sudo if user_sudo.karma == 0 or not user_sudo.website_published or \ (user_sudo.id != request.session.uid and request.env.user.karma < request.website.karma_profile_min): return False diff --git a/addons/website_profile/static/src/scss/website_profile.scss b/addons/website_profile/static/src/scss/website_profile.scss index 1a00803b1b9..e9fd045da45 100644 --- a/addons/website_profile/static/src/scss/website_profile.scss +++ b/addons/website_profile/static/src/scss/website_profile.scss @@ -120,6 +120,73 @@ $bronze: #eea91e; } } +/// Progress Circle +.o_wprofile_progress_circle { + position: relative; + + $circle-size: 220px; + $circle-background: rgba(black, 0.1); + $circle-color: theme-color('primary'); + $inset-size: $circle-size - 20; + $transition-length: 1s; + + .o_progress_circle_container, .o_pc, .o_pc div { + @include size($circle-size); + } + + .o_pc_overlay, .o_pc, .o_pc div { + border-radius: 50%; + } + + .o_progress_circle_container { + position: relative; + margin: 0 auto; + background-color: inherit; + } + + .o_pc_overlay { + $tmp-gap: ($circle-size - $inset-size)/2; + @include o-position-absolute($tmp-gap, $left: $tmp-gap); + @include size($inset-size); + + background-color: inherit; + } + + .o_pc { + background-color: $circle-background; + + div { + position: absolute; + -webkit-backface-visibility: hidden; + transition: transform $transition-length; + + &.o_pc_slice { + clip: rect(0px, $circle-size, $circle-size, $circle-size/2); + + .o_pc_fill { + clip: rect(0px, $circle-size/2, $circle-size, 0px); + background-color: $circle-color; + } + } + } + + $i: 0; + $increment: 180deg / 100; + @while $i <= 100 { + &[data-progress='#{$i}'] { + .o_pc_slice.full, .o_pc_fill { + transform: rotate($increment * $i); + } + .o_pc_fill.o_pc_bar { + transform: rotate($increment * $i * 2); + } + $i: $i + 1; + } + } + } +} + + .o_wprofile_next_rank_card { div { text-align: center; diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml index 2a29a3a446b..b7ab4704115 100644 --- a/addons/website_profile/views/website_profile.xml +++ b/addons/website_profile/views/website_profile.xml @@ -252,19 +252,29 @@ </template> <template id="profile_next_rank_card" name="Profile Next Rank Card"> - <div class="card-body o_wprofile_next_rank_card mt-2 pt-2"> - <div><img class="o_wprofile_img_medium" t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id}/image_medium"/></div> - <div><span class="text-center font-weight-bold">"<span t-field="user.next_rank_id.name"/>"</span></div> - <div><span class="text-center"><span class="font-weight-bold text-primary" t-field="user.karma"/>/<span class="font-weight-bold" t-field="user.next_rank_id.karma_min"/> xp</span></div> - <div class="progress o_wprofile_next_rank_progress"> - <t t-if="user.next_rank_id.karma_min - user.rank_id.karma_min > 0"> - <div class="progress-bar o_wprofile_next_rank_progress_bar" style="background-color: #00A09D !important" - t-att-style="'width: '+ str(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))+'%'"></div> - </t> - <t t-else=""> - <div class="progress-bar o_wprofile_next_rank_progress_bar" style="background-color: #00A09D !important" - t-att-style="'width: 0%'"></div> - </t> + <div t-attf-class="o_wprofile_progress_circle #{bg_class if bg_class else ''}"> + <div class="o_progress_circle_container"> + <div class="o_pc" t-att-data-progress="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))" > + <div class="o_pc_circle"> + <div class="o_pc_slice full"> + <div class="o_pc_fill"/> + </div> + <div class="o_pc_slice"> + <div class="o_pc_fill full"/> + <div class="o_pc_fill o_pc_bar"/> + </div> + </div> + </div> + <div class="o_pc_overlay d-flex flex-column align-items-center justify-content-center"> + <img class="img-fluid" + t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id}/image_medium" + t-att-alt="user.next_rank_id.name + ' badge'" + t-att-style="'max-width: ' + (img_max_width if img_max_width else '60%;')"/> + <h4 class="mt-2 mb-0">"<span t-field="user.next_rank_id.name"/>"</h4> + <small> + <span class="font-weight-bold text-primary" t-field="user.karma"/>/<span class="font-weight-bold" t-field="user.next_rank_id.karma_min"/> xp + </small> + </div> </div> </div> </template> diff --git a/addons/website_rating/static/src/js/website_mail.js b/addons/website_rating/static/src/js/website_mail.js index 8f21f740c98..6fd9a305e95 100644 --- a/addons/website_rating/static/src/js/website_mail.js +++ b/addons/website_rating/static/src/js/website_mail.js @@ -33,6 +33,11 @@ odoo.define('website_rating.thread', function(require) { init: function(parent, options){ this._super.apply(this, arguments); + // apply ratio to default rating value + if (options.default_rating_value) { + options.default_rating_value = parseFloat(options.default_rating_value) / STAR_RATING_RATIO; + } + // default options this.options = _.defaults(this.options, { 'default_message': false, @@ -50,7 +55,7 @@ odoo.define('website_rating.thread', function(require) { '5': _t("I love it"), }; this.user_click = false; // user has click or not - this.set("star_value", this.options.rating_default_value); + this.set("star_value", this.options.default_rating_value); this.on("change:star_value", this, this._onChangeStarValue); }, start: function(){ @@ -59,7 +64,7 @@ odoo.define('website_rating.thread', function(require) { // rating stars self.$input = self.$('input[name="rating_value"]'); self.$star_list = self.$('.stars').find('i'); - self.set("star_value", self.options.rating_default_value); // set the default value to trigger the display of star widget + self.set("star_value", self.options.default_rating_value); // set the default value to trigger the display of star widget }); }, @@ -265,7 +270,7 @@ odoo.define('website_rating.thread', function(require) { _onChangeRatingDomain: function(){ var domain = []; if(this.get('rating_value')){ - domain = [['rating_value', '=', this.get('rating_value')]]; + domain = [['rating_value', '=', this.get('rating_value') * STAR_RATING_RATIO]]; } this._changeCurrentPage(1, domain); }, diff --git a/addons/website_rating/static/src/xml/website_mail.xml b/addons/website_rating/static/src/xml/website_mail.xml index 11534a768c2..d4e8dd83218 100644 --- a/addons/website_rating/static/src/xml/website_mail.xml +++ b/addons/website_rating/static/src/xml/website_mail.xml @@ -79,7 +79,7 @@ <div class="rate_text"> <span class="badge badge-info"></span> </div> - <input type="hidden" readonly="readonly" name="rating_value" t-att-value="default_rating"/> + <input type="hidden" readonly="readonly" name="rating_value" t-att-value="default_rating || ''"/> </div> </t> diff --git a/addons/website_rating/views/website_rating_templates.xml b/addons/website_rating/views/website_rating_templates.xml index 32dc3d3c573..8ce20495b32 100644 --- a/addons/website_rating/views/website_rating_templates.xml +++ b/addons/website_rating/views/website_rating_templates.xml @@ -18,7 +18,7 @@ Rating widget static: show 5 stars (full or empty) regarding the given rating_avg and rating_count --> <template id="rating_widget_stars_static" name="Rating: static star widget"> - <t t-set="rating_avg" t-value="round(rating_avg * 100) / 100"/> + <t t-set="rating_avg" t-value="round(rating_avg * 100) / 100 / 2"/> <t t-set="val_decimal" t-value="round(rating_avg % 1, 1)"/> <t t-set="val_integer" t-value="int(rating_avg)"/> <t t-set="empty_star" t-value="5 - (val_integer+1) if val_decimal else 5 - val_integer"/> diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml index 71f7a2d7db7..fd94cb52c82 100644 --- a/addons/website_sale/data/mail_template_data.xml +++ b/addons/website_sale/data/mail_template_data.xml @@ -57,7 +57,7 @@ <hr/> % endif <div style="text-align: center; margin: 16px 0px 16px 0px; font-size: 14px;"> - <a href="/shop/cart?access_token=${object.access_token}" + <a href="${object.get_base_url()}/shop/cart?access_token=${object.access_token}" target="_blank" style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"> Resume order diff --git a/addons/website_sale/models/account_invoice.py b/addons/website_sale/models/account_invoice.py index 5696f4ace36..db5e9822889 100644 --- a/addons/website_sale/models/account_invoice.py +++ b/addons/website_sale/models/account_invoice.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import fields, models, api class AccountInvoice(models.Model): @@ -10,3 +10,10 @@ class AccountInvoice(models.Model): website_id = fields.Many2one('website', related='partner_id.website_id', string='Website', help='Website through which this invoice was created.', store=True, readonly=True) + + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(AccountInvoice, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 5bbdeff7868..adf71d95eec 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -43,11 +43,17 @@ class SaleOrder(models.Model): @api.multi @api.depends('website_id', 'date_order', 'order_line', 'state', 'partner_id') def _compute_abandoned_cart(self): - abandoned_delay = self.website_id and self.website_id.cart_abandoned_delay or 1.0 - abandoned_datetime = datetime.utcnow() - relativedelta(hours=abandoned_delay) for order in self: - domain = order.date_order and order.date_order <= abandoned_datetime and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line - order.is_abandoned_cart = bool(domain) + # a quotation can be considered as an abandonned cart if it is linked to a website, + # is in the 'draft' state and has an expiration date + if order.website_id and order.state == 'draft' and order.date_order: + public_partner_id = order.website_id.user_id.partner_id + # by default the expiration date is 1 hour if not specified on the website configuration + abandoned_delay = order.website_id.cart_abandoned_delay or 1.0 + abandoned_datetime = datetime.utcnow() - relativedelta(hours=abandoned_delay) + order.is_abandoned_cart = bool(order.date_order <= abandoned_datetime and order.partner_id != public_partner_id and order.order_line) + else: + order.is_abandoned_cart = False def _search_abandoned_cart(self, operator, value): abandoned_delay = self.website_id and self.website_id.cart_abandoned_delay or 1.0 @@ -312,6 +318,7 @@ class SaleOrder(models.Model): @api.multi def action_recovery_email_send(self): + self._portal_ensure_token() composer_form_view_id = self.env.ref('mail.email_compose_message_wizard_form').id try: default_template = self.env.ref('website_sale.mail_template_sale_cart_recovery', raise_if_not_found=False) @@ -338,6 +345,13 @@ class SaleOrder(models.Model): }, } + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(SaleOrder, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res + class SaleOrderLine(models.Model): _inherit = "sale.order.line" diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js b/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js new file mode 100644 index 00000000000..319a470ab6d --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js @@ -0,0 +1,92 @@ +odoo.define('website_sale.tour_shop_cart_recovery', function (require) { +'use strict'; + +var localStorage = require('web.local_storage'); +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); +require('web.dom_ready'); + +var orderIdKey = 'website_sale.tour_shop_cart_recovery.orderId'; +var recoveryLinkKey = 'website_sale.tour_shop_cart_recovery.recoveryLink'; + +tour.register('shop_cart_recovery', { + test: true, + url: '/shop?search=Acoustic Bloc Screens', +}, +[ + { + content: "select Acoustic Bloc Screens", + trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")', + }, + { + content: "click add to cart", + trigger: '#product_details #add_to_cart', + }, + { + content: "check product is in cart, get cart id, logout, go to login", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () { + rpc.query({ + 'model': 'website', + 'method': 'sale_get_order', + 'args': [[$('html').data('website-id')]], + }).then(function (data) { + var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]); + localStorage.setItem(orderIdKey, orderId); + window.location.href = "/web/session/logout?redirect=/web/login"; + }); + }, + }, + { + content: "login as admin and go to the SO (backend)", + trigger: '.oe_login_form', + run: function () { + var orderId = localStorage.getItem(orderIdKey); + var url = "/web#action=sale.action_orders&view_type=form&id=" + orderId; + var $loginForm = $('.oe_login_form'); + $loginForm.find('input[name="login"]').val("admin"); + $loginForm.find('input[name="password"]').val("admin"); + $loginForm.find('input[name="redirect"]').val(url); + $loginForm.submit(); + }, + }, + { + content: "click action", + trigger: '.btn:containsExact("Action")', + }, + { + content: "click Send a Cart Recovery Email", + trigger: 'a:containsExact("Send a Cart Recovery Email")', + }, + { + content: "click Send email", + trigger: '.btn[name="action_send_mail"]', + }, + { + content: "check the mail is sent, grab the recovery link, and logout", + trigger: '.o_thread_message_content a:containsExact("Resume order")', + run: function () { + var link = $('.o_thread_message_content a:containsExact("Resume order")').attr('href'); + localStorage.setItem(recoveryLinkKey, link); + window.location.href = "/web/session/logout?redirect=/"; + } + }, + { + content: "go to the recovery link", + trigger: 'a[href="/web/login"]', + run: function () { + window.location.href = localStorage.getItem(recoveryLinkKey); + }, + }, + { + content: "check the page is working, click on restore", + extra_trigger: 'p:contains("This is your current cart")', + trigger: 'p:contains("restore") a:contains("Click here")', + }, + { + content: "check product is in restored cart", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () {}, + }, +]); +}); diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js new file mode 100644 index 00000000000..0d8fe0131f0 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js @@ -0,0 +1,94 @@ +odoo.define('website_sale.tour_shop_mail', function (require) { +'use strict'; + +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); + +require('web.dom_ready'); + +tour.register('shop_mail', { + test: true, + url: '/', +}, +[ + { + content: "Change the domain of the websites and go to shop", + trigger: 'body', + run: function () { + // We change the domain of the website to test that the email that + // will be sent uses the correct domain for its links. + var def1 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1], { + 'domain': "my-test-domain.com", + }], + }); + // We need to change the domain of all the websites otherwise the + // website selector will return the website 2 since the domain we + // set on website 1 doesn't actually match our test server. + var def2 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[2], { + 'domain': "https://domain-not-used.fr", + }], + }); + $.when(def1, def2).then(function (data) { + window.location.href = '/shop?search=Acoustic Bloc Screens'; + }); + }, + }, + { + content: "select Acoustic Bloc Screens", + trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")', + }, + { + content: "click add to cart", + trigger: '#product_details #add_to_cart', + }, + { + content: "check product is in cart, get cart id, go to backend", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () { + rpc.query({ + 'model': 'website', + 'method': 'sale_get_order', + 'args': [[$('html').data('website-id')]], + }).then(function (data) { + var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]); + window.location.href = "/web#action=sale.action_orders&view_type=form&id=" + orderId; + }); + }, + }, + { + content: "click confirm", + trigger: '.btn[name="action_confirm"]', + }, + { + content: "click send by email", + trigger: '.btn[name="action_quotation_send"]', + extra_trigger: '.o_statusbar_status .btn-primary:contains("Sales Order")', + }, + { + content: "click Send email", + trigger: '.btn[name="action_send_mail"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: '.o_thread_message_content:contains("Here is"):contains("the order")', + run: function () { + window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list"; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("(Ref SO")', + }, + { + content: "check it's the correct email, and the URL is correct too", + trigger: 'div.oe_form_field_html[name="body_html"] p:contains("Here is"):contains("the order")', + extra_trigger: 'div.oe_form_field_html[name="body_html"] a[href^="http://my-test-domain.com"]', + }, +]); +}); diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index a8ec3aea1bf..3689314f28b 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -589,7 +589,7 @@ td.noborder { } .border_primary { - border: 1px solid $o-brand-odoo; + border: 1px solid theme-color('primary'); } .js_change_shipping { diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index 483dd984704..33bc88ece4a 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -1,5 +1,7 @@ from . import test_customize from . import test_sale_process +from . import test_website_sale_cart_recovery +from . import test_website_sale_mail from . import test_website_sale_pricelist from . import test_website_sale_product_attribute_value_config from . import test_website_sale_image diff --git a/addons/website_sale/tests/test_website_sale_cart_recovery.py b/addons/website_sale/tests/test_website_sale_cart_recovery.py new file mode 100644 index 00000000000..4aaa0433886 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_cart_recovery.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleCartRecovery(HttpCase): + + def test_01_shop_cart_recovery_tour(self): + """The goal of this test is to make sure cart recovery works.""" + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_cart_recovery')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_cart_recovery.ready", login="portal") diff --git a/addons/website_sale/tests/test_website_sale_mail.py b/addons/website_sale/tests/test_website_sale_mail.py new file mode 100644 index 00000000000..8cb495b5db9 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_mail.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +import odoo +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleMail(HttpCase): + + def test_01_shop_mail_tour(self): + """The goal of this test is to make sure sending SO by email works.""" + + # we override unlink because we don't want the email to be auto deleted + MailMail = odoo.addons.mail.models.mail_mail.MailMail + + with patch.object(MailMail, 'unlink', lambda self: None): + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_mail')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_mail.ready", login="admin") diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 58064782b17..557b788e558 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -48,6 +48,8 @@ <template id="assets_common" name="tour" inherit_id="web.assets_common"> <xpath expr="." position="inside"> <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop.js"></script> + <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js"></script> + <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_mail.js"></script> </xpath> </template> @@ -1319,7 +1321,7 @@ <t t-set="partner_id" t-value="partner"/> <t t-set="back_button_icon_class" t-value="'fa-chevron-left'"/> - <t t-set="back_button_txt" t-value="'Return to Cart'"/> + <t t-set="back_button_txt">Return to Cart</t> <t t-set="back_button_class" t-value="'btn btn-secondary'"/> <t t-set="back_button_link" t-value="'/shop/cart'"/> </t> diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index 2eec680e069..88c240222d9 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -5,6 +5,8 @@ var core = require('web.core'); var sAnimations = require('website.content.snippets.animation'); var _t = core._t; +var concurrency = require('web.concurrency'); +var dp = new concurrency.DropPrevious(); sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({ selector: '.oe_website_sale', @@ -89,12 +91,12 @@ sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({ $payButton.prop('disabled', true); $payButton.data('disabled_reasons', $payButton.data('disabled_reasons') || {}); $payButton.data('disabled_reasons').carrier_selection = true; - this._rpc({ + dp.add(this._rpc({ route: '/shop/update_carrier', params: { carrier_id: $radio.val(), }, - }).then(this._handleCarrierUpdateResult.bind(this)); + })).then(this._handleCarrierUpdateResult.bind(this)); }, /** * @private diff --git a/addons/website_sale_slides/__manifest__.py b/addons/website_sale_slides/__manifest__.py index b44cab80345..074d76ce016 100644 --- a/addons/website_sale_slides/__manifest__.py +++ b/addons/website_sale_slides/__manifest__.py @@ -5,7 +5,7 @@ 'summary': 'Sell your courses online', 'description': """Sell your courses using the e-commerce features of the website.""", 'category': 'Hidden', - 'version': '0.1', + 'version': '1.0', 'depends': ['website_slides', 'website_sale'], 'installable': True, diff --git a/addons/website_sale_stock/views/website_sale_stock_templates.xml b/addons/website_sale_stock/views/website_sale_stock_templates.xml index 6e755321851..9581154c616 100644 --- a/addons/website_sale_stock/views/website_sale_stock_templates.xml +++ b/addons/website_sale_stock/views/website_sale_stock_templates.xml @@ -37,7 +37,7 @@ <template id="website_sale_stock_product" inherit_id="website_sale.product" priority="4"> <xpath expr="//a[@id='add_to_cart']" position="after"> - <div class='availability_messages'/> + <div class="availability_messages o_not_editable"/> </xpath> </template> diff --git a/addons/website_slides/__init__.py b/addons/website_slides/__init__.py index 7d34c7c054a..88710707227 100644 --- a/addons/website_slides/__init__.py +++ b/addons/website_slides/__init__.py @@ -3,3 +3,4 @@ from . import controllers from . import models +from . import wizard diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index 32b4f407e67..2e051152cb5 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -1,21 +1,23 @@ # -*- coding: utf-8 -*- { - 'name': 'Slides', - 'version': '1.0', + 'name': 'eLearning', + 'version': '2.0', 'sequence': 145, - 'summary': 'Publish videos, slides and documents', + 'summary': 'Create online Courses', 'website': 'https://www.odoo.com/page/slides', 'category': 'Website', 'description': """ -Share and Publish Videos, Presentations and Documents' -====================================================== +Create Online Courses +===================== - * Website Application - * Channel Management - * Filters and Tagging - * Statistics of Presentation - * Channel Subscription - * Supported document types : PDF, images, YouTube videos and Google Drive documents) +Featuring + + * Integrated course and lesson management + * Fullscreen navigation + * Support Youtube videos, Google documents, PDF, images, web pages + * Test knowledge with quizzes + * Filter and Tag + * Statistics """, 'depends': [ 'website', @@ -25,16 +27,18 @@ Share and Publish Videos, Presentations and Documents' 'data': [ 'views/assets.xml', 'views/res_config_settings_views.xml', - 'views/website_slides_templates_homepage.xml', - 'views/website_slides_templates.xml', - 'views/website_slides_embed_templates.xml', 'views/slide_slide_views.xml', 'views/slide_channel_partner_views.xml', 'views/slide_channel_views.xml', 'views/slide_channel_tag_views.xml', - 'views/slide_channel_invite_views.xml', 'views/website_slides_menu_views.xml', + 'views/website_slides_templates_homepage.xml', + 'views/website_slides_templates_course.xml', + 'views/website_slides_templates_lesson.xml', + 'views/website_slides_templates_lesson_embed.xml', 'views/website_slides_templates_profile.xml', + 'views/website_slides_templates_utils.xml', + 'wizard/slide_channel_invite_views.xml', 'data/ir_data.xml', 'data/gamification_data.xml', 'data/mail_data.xml', diff --git a/addons/website_slides/controllers/__init__.py b/addons/website_slides/controllers/__init__.py index acf000fd92c..5f151657861 100644 --- a/addons/website_slides/controllers/__init__.py +++ b/addons/website_slides/controllers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. from . import mail from . import main diff --git a/addons/website_slides/controllers/mail.py b/addons/website_slides/controllers/mail.py index 1273ebe1119..5ec41d9de88 100644 --- a/addons/website_slides/controllers/mail.py +++ b/addons/website_slides/controllers/mail.py @@ -33,6 +33,7 @@ class SlidesMail(http.Controller): ('res_id', '=', res_id), ('website_published', '=', True), ('author_id', '=', request.env.user.partner_id.id), + ('message_type', '=', 'comment'), ('id', '=', message_id) ] # restrict to the given message_id message = request.env['mail.message'].search(domain, limit=1) diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 50cea64c078..7ab6b6052d7 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -53,8 +53,8 @@ class WebsiteSlides(WebsiteProfile): return True def _get_slide_detail(self, slide): - most_viewed_slides = slide.get_most_viewed_slides(self._slides_per_list) - related_slides = slide.get_related_slides(self._slides_per_list) + most_viewed_slides = slide._get_most_viewed_slides(self._slides_per_list) + related_slides = slide._get_related_slides(self._slides_per_list) values = { 'slide': slide, 'most_viewed_slides': most_viewed_slides, @@ -100,9 +100,8 @@ class WebsiteSlides(WebsiteProfile): tags |= search_tag return tags - def _build_channel_domain(self, base_domain, slide_type=None, **post): + def _build_channel_domain(self, base_domain, slide_type=None, my=False, **post): search_term = post.get('search') - category_id = post.get('category_id') channel_tag_id = post.get('channel_tag_id') tags = self._extract_channel_tag_search(**post) @@ -111,15 +110,16 @@ class WebsiteSlides(WebsiteProfile): domain = expression.AND([ domain, ['|', ('name', 'ilike', search_term), ('description', 'ilike', search_term)]]) - if category_id: - domain = expression.AND([domain, [('category_ids', 'in', [category_id])]]) - elif channel_tag_id: + if channel_tag_id: domain = expression.AND([domain, [('tag_ids', 'in', [channel_tag_id])]]) elif tags: domain = expression.AND([domain, [('tag_ids', 'in', tags.ids)]]) - if slide_type and 'nbr_%ss' % slide_type in request.env['slide.channel']: - domain = expression.AND([domain, [('nbr_%ss' % slide_type, '>', 0)]]) + if slide_type and 'nbr_%s' % slide_type in request.env['slide.channel']: + domain = expression.AND([domain, [('nbr_%s' % slide_type, '>', 0)]]) + + if my: + domain = expression.AND([domain, [('partner_ids', '=', request.env.user.partner_id.id)]]) return domain def _prepare_channel_values(self, **kwargs): @@ -179,24 +179,29 @@ class WebsiteSlides(WebsiteProfile): }) @http.route('/slides/all', type='http', auth="public", website=True) - def slides_channel_all(self, slide_type=None, **post): + def slides_channel_all(self, slide_type=None, my=False, **post): """ Home page displaying a list of courses displayed according to some criterion and search terms. - : param string slide_type: if provided, filter the slide.channels - to contain at least one slide of type 'slide_type' - : param dict post: post parameters, including - * search_term: keywords entered in the search box, used to filter on slide content; - * category_id: id of a slide.category; - * channel_tag_id: id of a channel.tag; + :param string slide_type: if provided, filter the course to contain at + least one slide of type 'slide_type'. Used notably to display courses + with certifications; + :param bool my: if provided, filter the slide.channels for which the + current user is a member of + :param dict post: post parameters, including + + * ``search``: filter on course description / name; + * ``channel_tag_id``: filter on courses containing this tag; + * ``channel_tag_group_id_<id>``: filter on courses containing this tag + in the tag group given by <id> (used in navigation based on tag group); """ domain = request.website.website_domain() - domain = self._build_channel_domain(domain, slide_type=slide_type, **post) + domain = self._build_channel_domain(domain, slide_type=slide_type, my=my, **post) order = self._channel_order_by_criterion.get(post.get('sorting', 'date'), 'create_date desc') channels = request.env['slide.channel'].search(domain, order=order) - channels_layouted = list(itertools.zip_longest(*[iter(channels)] * 4, fillvalue=None)) + # channels_layouted = list(itertools.zip_longest(*[iter(channels)] * 4, fillvalue=None)) tag_groups = request.env['slide.channel.tag.group'].search(['&', ('tag_ids', '!=', False), ('website_published', '=', True)]) search_tags = self._extract_channel_tag_search(**post) @@ -204,10 +209,13 @@ class WebsiteSlides(WebsiteProfile): return request.render('website_slides.courses_all', { 'user': request.env.user, 'is_public_user': request.website.is_public_user(), - 'channels_layouted': channels_layouted, + 'channels': channels, 'tag_groups': tag_groups, 'search_term': post.get('search'), + 'search_slide_type': slide_type, + 'search_my': my, 'search_tags': search_tags, + 'search_channel_tag_id': post.get('channel_tag_id'), }) def _prepare_additional_channel_values(self, values, **kwargs): @@ -221,11 +229,12 @@ class WebsiteSlides(WebsiteProfile): '/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>', '/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>/page/<int:page>' ], type='http', auth="public", website=True, sitemap=sitemap_slide) - def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sorting=None, search=None, **kw): + def channel(self, channel, category=None, tag=None, page=1, slide_type=None, uncategorized=False, sorting=None, search=None, **kw): if not channel.can_access_from_current_website(): raise werkzeug.exceptions.NotFound() + domain = [('channel_id', '=', channel.id)] - if not request.env.user.has_group('website.group_website_publisher'): + if not channel.can_publish: domain = expression.AND([ domain, ['&', ('website_published', '=', True), ('channel_id.website_published', '=', True)] @@ -250,7 +259,10 @@ class WebsiteSlides(WebsiteProfile): elif tag: domain += [('tag_ids.id', '=', tag.id)] pager_url += "/tag/%s" % tag.id - if slide_type: + if uncategorized: + domain += [('category_id', '=', False)] + pager_url += "?uncategorized=1" + elif slide_type: domain += [('slide_type', '=', slide_type)] pager_url += "?slide_type=%s" % slide_type @@ -270,6 +282,7 @@ class WebsiteSlides(WebsiteProfile): 'search_category': category, 'search_tag': tag, 'search_slide_type': slide_type, + 'search_uncategorized': uncategorized, 'slide_types': slide_types, 'sorting': actual_sorting, 'search': search, @@ -287,6 +300,7 @@ class WebsiteSlides(WebsiteProfile): ('model', '=', channel._name), ('res_id', '=', channel.id), ('author_id', '=', request.env.user.partner_id.id), + ('message_type', '=', 'comment'), ('website_published', '=', True) ], order='write_date DESC', limit=1).read(['body', 'rating_value']) last_message_data = last_message_values[0] if last_message_values else {} @@ -301,24 +315,48 @@ class WebsiteSlides(WebsiteProfile): # Display uncategorized slides # fetch slides; done as sudo because we want to display all of them but # unreachable ones won't be clickable (+ slide controller will crash anyway) - if not category: + if not category and not uncategorized: category_data = [] for category in request.env['slide.slide'].sudo().read_group(domain, ['category_id'], ['category_id']): category_id, name = category.get('category_id') or (False, _('Uncategorized')) + slides_sudo = request.env['slide.slide'].sudo().search(category['__domain'], limit=4, offset=0, order=order) category_data.append({ 'id': category_id, 'name': name, + 'slug_name': slug((category_id, name)) if category_id else name, 'total_slides': category['category_id_count'], - 'slides': request.env['slide.slide'].sudo().search(category['__domain'], limit=4, offset=0, order=order) + 'slides': slides_sudo, }) + elif uncategorized: + slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order) + category_data = [{ + 'id': False, + 'name': _('Uncategorized'), + 'slug_name': _('Uncategorized'), + 'total_slides': len(slides_sudo), + 'slides': slides_sudo, + }] else: slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order) category_data = [{ 'id': category.id, 'name': category.name, + 'slug_name': slug(category), 'total_slides': len(slides_sudo), 'slides': slides_sudo, }] + + # post slide-fetch computation: promoted, user completion (separated because sudo-ed) + if not request.website.is_public_user() and channel.is_member: + displayed_slide_ids = list(set(sid for item in category_data for sid in item['slides'].ids)) + done_slide_ids = request.env['slide.slide.partner'].sudo().search([ + ('slide_id', 'in', displayed_slide_ids), + ('partner_id', '=', request.env.user.partner_id.id), + ('completed', '=', True) + ]).mapped('slide_id').ids + else: + done_slide_ids = [] + values['done_slide_ids'] = done_slide_ids values['slide_promoted'] = request.env['slide.slide'].sudo().search(domain, limit=1, order=order) values['category_data'] = category_data @@ -500,7 +538,7 @@ class WebsiteSlides(WebsiteProfile): } # JSONRPC - @http.route('/slides/slide/like', type='json', auth="user", website=True) + @http.route('/slides/slide/like', type='json', auth="public", website=True) def slide_like(self, slide_id, upvote): if request.website.is_public_user(): return {'error': 'public_user'} @@ -521,7 +559,7 @@ class WebsiteSlides(WebsiteProfile): @http.route(['/slides/slide/send_share_email'], type='json', auth='user', website=True) def slide_send_share_email(self, slide_id, email): slide = request.env['slide.slide'].browse(int(slide_id)) - result = slide.send_share_email(email) + result = slide._send_share_email(email) return result # -------------------------------------------------- diff --git a/addons/website_slides/data/slide_channel_demo.xml b/addons/website_slides/data/slide_channel_demo.xml index 61fc7337d4b..78e760228e0 100644 --- a/addons/website_slides/data/slide_channel_demo.xml +++ b/addons/website_slides/data/slide_channel_demo.xml @@ -4,14 +4,13 @@ <record id="website_slides.channel_public" model="slide.channel"> <field name="name">Basics of Gardening</field> <field name="channel_type">training</field> - <field name="promote_strategy">latest</field> + <field name="promote_strategy">most_voted</field> <field name="tag_ids" eval="[(5, 0), (4, ref('website_slides.slide_channel_tag_level_basic')), (4, ref('website_slides.slide_channel_tag_role_gardener')), (4, ref('website_slides.slide_channel_tag_other_0'))]"/> <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_gardening.jpg"/> - <field name="description" type="html"> -<div>Learn the basics of gardening !</div></field> + <field name="description">Learn the basics of gardening !</field> </record> <record id="slide_category_demo_0_0" model="slide.category"> <field name="name">Requirements</field> @@ -34,8 +33,7 @@ <field name="website_published" eval="True"/> <field name="enroll">public</field> <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_gardening_2.jpg"/> - <field name="description" type="html"> -<div>In this training you will learn how to take care of trees: when to plant, potted trees, ...</div></field> + <field name="description">Learn how to take care of your favorite trees. Learn when to plant, how to manage potted trees, ...</field> </record> <record id="slide_category_demo_1_0" model="slide.category"> <field name="name">Interesting Facts</field> @@ -60,8 +58,7 @@ <field name="website_published" eval="True"/> <field name="enroll">public</field> <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_flowers.jpg"/> - <field name="description" type="html"> -<div>A lot of nice documentation: trees, wood, gardens. After that you will know everything.</div></field> + <field name="description">A lot of nice documentation: trees, wood, gardens. A gold mine for references.</field> </record> <record id="slide_category_demo_2_0" model="slide.category"> <field name="name">Trees</field> @@ -86,10 +83,9 @@ <field name="promote_strategy">latest</field> <field name="website_published" eval="True"/> <field name="enroll">public</field> - <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_flowers.jpg"/> - <field name="description" type="html"> -<div>Knowing which kind of wood to use depending on your application is important. In this course you -will learn the basics of wood characteristics.</div></field> + <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_tree_1.jpg"/> + <field name="description">Knowing which kind of wood to use depending on your application is important. In this course you +will learn the basics of wood characteristics.</field> </record> <record id="slide_category_demo_3_0" model="slide.category"> <field name="name">Introduction</field> @@ -106,10 +102,8 @@ will learn the basics of wood characteristics.</div></field> <field name="promote_strategy">most_voted</field> <field name="website_published" eval="True"/> <field name="enroll">invite</field> - <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/> - <field name="description" type="html"> -<div>We're also Santa Claus! Belligerent and numerous. I am the man with no name, - Zapp Brannigan! </div></field> + <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture_2.jpg"/> + <field name="description">We're also Santa Claus! Belligerent and numerous. I am the man with no name, Zapp Brannigan!</field> </record> <record id="slide_category_demo_4_0" model="slide.category"> <field name="name">Introduction</field> @@ -129,7 +123,7 @@ will learn the basics of wood characteristics.</div></field> <field name="website_published" eval="True"/> <field name="enroll">invite</field> <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/> - <field name="description" type="html"><div>All you need to know about furniture creation.</div></field> + <field name="description">All you need to know about furniture creation.</field> </record> <record id="slide_category_demo_5_0" model="slide.category"> <field name="name">Introduction</field> @@ -137,21 +131,20 @@ will learn the basics of wood characteristics.</div></field> <field name="sequence">10</field> </record> - <record id="slide_tag_demo_0" model="slide.tag"> - <field name="name">Tag0</field> + <record id="slide_tag_demo_cheatsheet" model="slide.tag"> + <field name="name">CheatSheet</field> </record> - <record id="slide_tag_demo_1" model="slide.tag"> - <field name="name">Tag1</field> + <record id="slide_tag_demo_theory" model="slide.tag"> + <field name="name">Theory</field> </record> - <record id="slide_tag_demo_2" model="slide.tag"> - <field name="name">Tag2</field> + <record id="slide_tag_demo_exercises" model="slide.tag"> + <field name="name">Exercises</field> </record> - <record id="slide_tag_demo_tools" model="slide.tag"> <field name="name">Tools</field> </record> - <record id="slide_tag_demo_colorfull" model="slide.tag"> - <field name="name">Colorfull</field> + <record id="slide_tag_demo_colorful" model="slide.tag"> + <field name="name">Colorful</field> </record> <record id="slide_tag_demo_howto" model="slide.tag"> <field name="name">HowTo</field> diff --git a/addons/website_slides/data/slide_slide_demo.xml b/addons/website_slides/data/slide_slide_demo.xml index 8971ec2fa8f..80c45a5313f 100644 --- a/addons/website_slides/data/slide_slide_demo.xml +++ b/addons/website_slides/data/slide_slide_demo.xml @@ -1,6 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <odoo><data noupdate="0"> + <!-- CHANNEL 0: Basics of Gardening --> + <!-- ================================================== --> <record id="slide_slide_demo_0_0" model="slide.slide"> <field name="name">Know-How Summary</field> <field name="datas" type="base64" file="website_slides/static/src/img/presentation.pdf"/> @@ -10,11 +12,14 @@ <field name="category_id" ref="website_slides.slide_category_demo_0_0"/> <field name="website_published" eval="True"/> <field name="is_preview" eval="True"/> - <field name="public_views">40</field> + <field name="public_views">10</field> + <field name="completion_time">2.5</field> <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_tools')), (4, ref('website_slides.slide_tag_demo_howto'))]"/> <field name="description">A summary of know-how: how and what.</field> </record> + <!-- CHANNEL 1: Taking care of Trees --> + <!-- ================================================== --> <record id="slide_slide_demo_1_0" model="slide.slide"> <field name="name">Tree Infographic</field> <field name="image" type="base64" file="website_slides/static/src/img/slide_demo_tree_infographic_1.jpg"/> @@ -24,7 +29,8 @@ <field name="website_published" eval="True"/> <field name="is_preview" eval="True"/> <field name="public_views">5</field> - <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_colorfull'))]"/> + <field name="completion_time">0.5</field> + <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_colorful'))]"/> <field name="description">Just some basics Tree Infographic.</field> </record> <record id="slide_slide_demo_1_1" model="slide.slide"> @@ -34,9 +40,9 @@ <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/> <field name="category_id" ref="website_slides.slide_category_demo_1_0"/> <field name="website_published" eval="True"/> - <field name="is_preview" eval="True"/> - <field name="public_views">10</field> - <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_colorfull'))]"/> + <field name="is_preview" eval="False"/> + <field name="public_views">5</field> + <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_colorful'))]"/> <field name="description">Just some basics Interesting Tree Facts.</field> </record> <record id="slide_slide_demo_1_2" model="slide.slide"> @@ -47,8 +53,8 @@ <field name="category_id" ref="website_slides.slide_category_demo_1_0"/> <field name="website_published" eval="True"/> <field name="is_preview" eval="False"/> - <field name="public_views">15</field> - <field name="tag_ids" eval="[(5, 0)]"/> + <field name="public_views">10</field> + <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_colorful')), (4, ref('website_slides.slide_tag_demo_theory'))]"/> <field name="description">Just some basics Energy Efficiency Facts.</field> </record> <record id="slide_slide_demo_1_3" model="slide.slide"> @@ -126,6 +132,71 @@ <field name="description">A summary of know-how: how and what.</field> </record> + <!-- CHANNEL 2: Trees, Wood and Garden --> + <!-- ================================================== --> + <record id="slide_slide_demo_2_0" model="slide.slide"> + <field name="name">Main Trees Categories</field> + <field name="datas" type="base64" file="website_slides/static/src/img/presentation.pdf"/> + <field name="image" type="base64" file="website_slides/static/src/img/slide_demo_tree_img_1.jpg"/> + <field name="slide_type">presentation</field> + <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/> + <field name="category_id" ref="website_slides.slide_category_demo_2_0"/> + <field name="website_published" eval="True"/> + <field name="is_preview" eval="False"/> + <field name="public_views">0</field> + <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_howto'))]"/> + <field name="description">A summary of know-how: how and what.</field> + </record> + <record id="slide_slide_demo_2_1" model="slide.slide"> + <field name="name">A Mighty Forest from Ages</field> + <field name="image" type="base64" file="website_slides/static/src/img/slide_demo_tree_img_1.jpg"/> + <field name="slide_type">webpage</field> + <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/> + <field name="category_id" ref="website_slides.slide_category_demo_2_0"/> + <field name="html_content" type="html"> +<section class="s_cover parallax bg-black-50 pt16 pb16" data-scroll-background-ratio="0" style="background-image: none;"> + <span class="s_parallax_bg oe_img_bg oe_custom_bg" style="background-image: url('/website_slides/static/src/img/slide_demo_tree_img_1.jpg'); background-position: 50% 0;"></span> + <div class="container"> + <div class="row s_nb_column_fixed"> + <div class="col-lg-12 s_title"> + <h1 class="s_title_thin o_default_snippet_text" style="font-size: 62px; text-align: center;">Catchy Headline</h1> + </div> + <div class="col-lg-12 s_text pt16 pb16"> + <p class="lead o_default_snippet_text" style="text-align: center;">Write one or two paragraphs describing your product, services or a specific feature.<br/> To be successful your content needs to be useful to your readers.</p> + </div> + <div class="col-lg-12 s_btn text-center pt16 pb16"> + <a href="/aboutus" class="btn btn-delta rounded-circle o_default_snippet_text">About us</a> + <a href="/contactus" class="btn btn-primary rounded-circle o_default_snippet_text">Contact us</a> + </div> + </div> + </div> +</section> +<section class="s_text_image pt32 pb32"> + <div class="container"> + <div class="row align-items-center"> + <div class="col-lg-6 pt16 pb16"> + <img src="/website_slides/static/src/img/slide_demo_tree_img_2.jpg" class="img img-fluid mx-auto" alt="Odoo • Image and Text"/> + </div> + <div class="col-lg-6 pt16 pb16"> + <h2 class="o_default_snippet_text">Section Subtitle</h2> + <p class="o_default_snippet_text">Write one or two paragraphs describing your product or services. <br/>To be successful your content needs to be useful to your readers.</p> + <p class="o_default_snippet_text">Start with the customer – find out what they want and give it to them.</p> + <div class="s_btn text-left pt16 pb16"> + <a href="#" class="btn btn-outline-primary o_default_snippet_text">Discover more</a> + </div> + </div> + </div> + </div> +</section></field> + <field name="website_published" eval="True"/> + <field name="is_preview" eval="False"/> + <field name="public_views">0</field> + <field name="tag_ids" eval="[(4, ref('website_slides.slide_tag_demo_theory'))]"/> + <field name="description">Mighty forest just don't appear in a few weeks. Learn how time made our forests mighty and mysterious.</field> + </record> + + <!-- CHANNEL 2: Trees, Wood and Garden --> + <!-- ================================================== --> <record id="slide_slide_demo_5_0" model="slide.slide"> <field name="name">How to find quality wood</field> <field name="url">https://www.youtube.com/watch?v=5WMqwTnZ-qs</field> diff --git a/addons/website_slides/data/slide_user_demo.xml b/addons/website_slides/data/slide_user_demo.xml index 12700c81712..aa08a5b1755 100644 --- a/addons/website_slides/data/slide_user_demo.xml +++ b/addons/website_slides/data/slide_user_demo.xml @@ -1,6 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> <odoo><data noupdate="0"> + <!-- CHANNEL 0: Basics of Gardening --> + <!-- ================================================== --> + <record id="slide_channel_0_partner_admin" model="slide.channel.partner"> + <field name="channel_id" ref="website_slides.channel_public"/> + <field name="partner_id" ref="base.partner_admin"/> + </record> + <record id="slide_slide_0_0_partner_admin" model="slide.slide.partner"> + <field name="slide_id" ref="website_slides.slide_slide_demo_0_0"/> + <field name="partner_id" ref="base.partner_admin"/> + <field name="completed" eval="True"/> + </record> + <record id="slide_channel_0_partner_demo" model="slide.channel.partner"> + <field name="channel_id" ref="website_slides.channel_public"/> + <field name="partner_id" ref="base.partner_demo"/> + </record> + <record id="slide_slide_0_0_partner_demo" model="slide.slide.partner"> + <field name="slide_id" ref="website_slides.slide_slide_demo_0_0"/> + <field name="partner_id" ref="base.partner_demo"/> + </record> + + + <!-- CHANNEL 1: Taking care of Trees --> + <!-- ================================================== --> <record id="slide_channel_1_partner_admin" model="slide.channel.partner"> <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/> <field name="partner_id" ref="base.partner_admin"/> @@ -59,4 +82,17 @@ <field name="message_id" ref="website_slides.message_channel_1_demo"/> </record> + <!-- CHANNEL 2: Trees, Wood and Garden --> + <!-- ================================================== --> + <record id="slide_channel_2_partner_admin" model="slide.channel.partner"> + <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/> + <field name="partner_id" ref="base.partner_admin"/> + </record> + <record id="slide_slide_2_0_partner_admin" model="slide.slide.partner"> + <field name="slide_id" ref="website_slides.slide_slide_demo_2_0"/> + <field name="partner_id" ref="base.partner_admin"/> + <field name="vote">1</field> + <field name="completed" eval="True"/> + </record> + </data></odoo> diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 2768668e3e4..9a55188c7ae 100644 --- a/addons/website_slides/models/__init__.py +++ b/addons/website_slides/models/__init__.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. from . import gamification_challenge from . import slide_slide from . import slide_question from . import slide_channel from . import slide_channel_tag -from . import slide_channel_invite from . import res_config_settings from . import website from . import res_users diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 766fde193a0..3dfba4e7576 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -6,7 +6,6 @@ import uuid from odoo import api, fields, models, tools, _ from odoo.addons.http_routing.models.ir_http import slug -from odoo.tools.translate import html_translate from odoo.exceptions import UserError from odoo.osv import expression @@ -63,7 +62,10 @@ class Channel(models.Model): """ A channel is a container of slides. """ _name = 'slide.channel' _description = 'Slide Channel' - _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin', 'rating.mixin'] + _inherit = [ + 'mail.thread', 'rating.mixin', + 'image.mixin', + 'website.seo.metadata', 'website.published.multi.mixin'] _order = 'sequence, id' def _default_access_token(self): @@ -72,7 +74,8 @@ class Channel(models.Model): # description name = fields.Char('Name', translate=True, required=True) active = fields.Boolean(default=True) - description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) + description = fields.Text('Short Description', translate=True) + description_html = fields.Html('Description', translate=tools.html_translate, sanitize_attributes=False) channel_type = fields.Selection([ ('documentation', 'Documentation'), ('training', 'Training')], string="Course type", default="documentation", required=True) @@ -82,10 +85,6 @@ class Channel(models.Model): 'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id', string='Tags', help='Used to categorize and filter displayed channels/courses') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") - image = fields.Binary("Image", attachment=True) - image_large = fields.Binary("Large image", attachment=True) - image_medium = fields.Binary("Medium image", attachment=True) - image_small = fields.Binary("Small image", attachment=True) # slides: promote, statistics slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") slide_last_update = fields.Date('Last Update', compute='_compute_slide_last_update', store=True) @@ -96,12 +95,12 @@ class Channel(models.Model): ('most_viewed', 'Most Viewed')], string="Featuring Policy", default='latest', required=True) access_token = fields.Char("Security Token", copy=False, default=_default_access_token) - nbr_presentations = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True) - nbr_documents = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True) - nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True) - nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True) - nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) - nbr_quizs = fields.Integer("Number of quizzes", compute="_compute_slides_statistics", store=True) + nbr_presentation = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True) + nbr_document = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True) + nbr_video = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True) + nbr_infographic = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True) + nbr_webpage = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) + nbr_quiz = fields.Integer("Number of Quizs", compute='_compute_slides_statistics', store=True) total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total') total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True) total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True) @@ -126,7 +125,7 @@ class Channel(models.Model): help='Condition to enroll: everyone, on invite, on payment (sale bridge).') enroll_msg = fields.Html( 'Enroll Message', help="Message explaining the enroll process", - default=False, translate=html_translate, sanitize_attributes=False) + default=False, translate=tools.html_translate, sanitize_attributes=False) enroll_group_ids = fields.Many2many('res.groups', string='Auto Enroll Groups', help="Members of those groups are automatically added as members of the channel.") visibility = fields.Selection([ ('public', 'Public'), ('members', 'Members')], @@ -180,7 +179,7 @@ class Channel(models.Model): @api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.completion_time', 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views') def _compute_slides_statistics(self): - result = dict((cid, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) for cid in self.ids) + result = dict((cid, dict(total_views=0, total_votes=0, total_time=0)) for cid in self.ids) read_group_res = self.env['slide.slide'].read_group( [('is_published', '=', True),('channel_id', 'in', self.ids)], ['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'], @@ -188,7 +187,6 @@ class Channel(models.Model): lazy=False) for res_group in read_group_res: cid = res_group['channel_id'][0] - result[cid]['total_slides'] += res_group['__count'] result[cid]['total_views'] += res_group.get('total_views', 0) result[cid]['total_votes'] += res_group.get('likes', 0) result[cid]['total_votes'] -= res_group.get('dislikes', 0) @@ -202,16 +200,16 @@ class Channel(models.Model): record.update(result[record.id]) def _compute_slides_statistics_type(self, read_group_res): - """ Can be overridden to compute stats on added slide_types """ - result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, nbr_quizs=0)) for cid in self.ids) + """ Compute statistics based on all existing slide types """ + slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env) + keys = ['nbr_%s' % slide_type for slide_type in slide_types] + keys.append('total_slides') + result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids) for res_group in read_group_res: cid = res_group['channel_id'][0] - result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0 - result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0 - result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0 - result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0 - result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0 - result[cid]['nbr_quizs'] += res_group.get('slide_type', '') == 'quiz' and res_group['__count'] or 0 + for slide_type in slide_types: + result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0 + result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0 return result @api.depends('slide_partner_ids') @@ -259,41 +257,6 @@ class Channel(models.Model): if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange. channel.website_url = '%s/slides/%s' % (base_url, slug(channel)) - @api.multi - def action_redirect_to_members(self): - action = self.env.ref('website_slides.slide_channel_partner_action').read()[0] - action['view_mode'] = 'tree' - action['domain'] = [('channel_id', 'in', self.ids)] - if len(self) == 1: - action['context'] = {'default_channel_id': self.id} - - return action - - @api.multi - def action_channel_invite(self): - self.ensure_one() - - if self.enroll != 'invite': - raise UserError(_("You cannot send invitations for channels that are not set as 'invite'.")) - - template = self.env.ref('website_slides.mail_template_slide_channel_invite', raise_if_not_found=False) - - local_context = dict( - self.env.context, - default_channel_id=self.id, - default_use_template=bool(template), - default_template_id=template and template.id or False, - notif_layout='mail.mail_notification_light', - ) - return { - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'slide.channel.invite', - 'target': 'new', - 'context': local_context, - } - # --------------------------------------------------------- # ORM Overrides # --------------------------------------------------------- @@ -322,9 +285,8 @@ class Channel(models.Model): vals['channel_partner_ids'] = [(0, 0, { 'partner_id': self.env.user.partner_id.id })] - if 'image' in vals: - tools.image_resize_images(vals, return_large=True) channel = super(Channel, self.with_context(mail_create_nosubscribe=True)).create(vals) + if channel.user_id: channel._action_add_members(channel.user_id.partner_id) if 'enroll_group_ids' in vals: @@ -333,8 +295,6 @@ class Channel(models.Model): @api.multi def write(self, vals): - if 'image' in vals: - tools.image_resize_images(vals, return_large=True) res = super(Channel, self).write(vals) if vals.get('user_id'): self._action_add_members(self.env['res.users'].sudo().browse(vals['user_id']).partner_id) @@ -364,6 +324,41 @@ class Channel(models.Model): # Business / Actions # --------------------------------------------------------- + @api.multi + def action_redirect_to_members(self): + action = self.env.ref('website_slides.slide_channel_partner_action').read()[0] + action['view_mode'] = 'tree' + action['domain'] = [('channel_id', 'in', self.ids)] + if len(self) == 1: + action['context'] = {'default_channel_id': self.id} + + return action + + @api.multi + def action_channel_invite(self): + self.ensure_one() + + if self.visibility != 'invite': + raise UserError(_("You cannot send invitations for channels that are not set as 'invite'.")) + + template = self.env.ref('website_slides.mail_template_slide_channel_invite', raise_if_not_found=False) + + local_context = dict( + self.env.context, + default_channel_id=self.id, + default_use_template=bool(template), + default_template_id=template and template.id or False, + notif_layout='mail.mail_notification_light', + ) + return { + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'slide.channel.invite', + 'target': 'new', + 'context': local_context, + } + def action_add_member(self, **member_values): """ Adds the logged in user in the channel members. (see '_action_add_members' for more info) @@ -413,11 +408,6 @@ class Channel(models.Model): for channel in self: channel._action_add_members(channel.mapped('enroll_group_ids.users.partner_id')) - def list_all(self): - return { - 'channels': [{'id': channel.id, 'name': channel.name, 'website_url': channel.website_url} for channel in self.search([])] - } - # --------------------------------------------------------- # Rating Mixin API # --------------------------------------------------------- @@ -439,12 +429,12 @@ class Category(models.Model): channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade') sequence = fields.Integer(default=10, help='Display order') slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides") - nbr_presentations = fields.Integer("Number of Presentations", compute='_count_presentations', store=True) - nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True) - nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True) - nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True) - nbr_webpages = fields.Integer("Number of Webpages", compute='_count_presentations', store=True) - nbr_quizs = fields.Integer("Number of quizzes", compute="_count_presentations", store=True) + nbr_presentation = fields.Integer("Number of Presentations", compute='_count_presentations', store=True) + nbr_document = fields.Integer("Number of Documents", compute='_count_presentations', store=True) + nbr_video = fields.Integer("Number of Videos", compute='_count_presentations', store=True) + nbr_infographic = fields.Integer("Number of Infographics", compute='_count_presentations', store=True) + nbr_webpage = fields.Integer("Number of Webpages", compute='_count_presentations', store=True) + nbr_quiz = fields.Integer("Number of Quizs", compute="_count_presentations", store=True) total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total') @api.depends('slide_ids.slide_type', 'slide_ids.is_published') @@ -454,29 +444,23 @@ class Category(models.Model): [('is_published', '=', True), ('category_id', 'in', self.ids)], ['category_id', 'slide_type'], ['category_id', 'slide_type'], lazy=False) - for res_group in res: - result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count'] - for record in self: - record.update(self._extract_count_presentations_type(result, record.id)) - - def _extract_count_presentations_type(self, result, record_id): - """ Can be overridden to compute stats on added slide_types """ - statistics = { - 'nbr_presentations': result[record_id].get('presentation', 0), - 'nbr_documents': result[record_id].get('document', 0), - 'nbr_videos': result[record_id].get('video', 0), - 'nbr_infographics': result[record_id].get('infographic', 0), - 'nbr_webpages': result[record_id].get('webpage', 0), - 'nbr_quizs': result[record_id].get('quiz', 0), - 'total_slides': 0 - } + type_stats = self._compute_slides_statistics_type(res) + for cid, cdata in type_stats.items(): + result[cid].update(cdata) - statistics['total_slides'] += statistics['nbr_presentations'] - statistics['total_slides'] += statistics['nbr_documents'] - statistics['total_slides'] += statistics['nbr_videos'] - statistics['total_slides'] += statistics['nbr_infographics'] - statistics['total_slides'] += statistics['nbr_webpages'] - statistics['total_slides'] += statistics['nbr_quizs'] + for record in self: + record.update(result[record.id]) - return statistics + def _compute_slides_statistics_type(self, read_group_res): + """ Compute statistics based on all existing slide types """ + slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env) + keys = ['nbr_%s' % slide_type for slide_type in slide_types] + keys.append('total_slides') + result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids) + for res_group in read_group_res: + cid = res_group['category_id'][0] + for slide_type in slide_types: + result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0 + result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0 + return result diff --git a/addons/website_slides/models/slide_question.py b/addons/website_slides/models/slide_question.py index 0193558ba46..9d497190434 100644 --- a/addons/website_slides/models/slide_question.py +++ b/addons/website_slides/models/slide_question.py @@ -8,14 +8,11 @@ from odoo.exceptions import ValidationError class SlideQuestion(models.Model): _name = "slide.question" _rec_name = "question" - _description = """ - Question for a slide. A slide can have multiple questions and each question - must have at least 2 possible answers and only one good answer. - """ + _description = "Slide Quiz Question" sequence = fields.Integer("Sequence", default=10) - question = fields.Char("Question Name", required=True) - slide_id = fields.Many2one('slide.slide', string="Slide") + question = fields.Char("Question Name", required=True, translate=True) + slide_id = fields.Many2one('slide.slide', string="Slide", required=True) answer_ids = fields.One2many('slide.answer', 'question_id', string="Answer") @api.constrains('answer_ids') @@ -44,8 +41,9 @@ class SlideQuestion(models.Model): class SlideAnswer(models.Model): _name = "slide.answer" _rec_name = "text_value" - _description = """Answer for a slide question""" + _description = "Answer for a slide question" + _order = 'question_id, id' - question_id = fields.Many2one('slide.question', string="Question") - text_value = fields.Char("Answer", required=True,) - is_correct = fields.Boolean("Is correct answer", default=False) + question_id = fields.Many2one('slide.question', string="Question", required=True) + text_value = fields.Char("Answer", required=True, translate=True) + is_correct = fields.Boolean("Is correct answer") diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index fe022f83b31..0ab75feec59 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -11,7 +11,7 @@ import uuid from werkzeug import urls -from odoo import api, fields, models, tools, _ +from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.exceptions import Warning, UserError from odoo.http import request @@ -77,19 +77,11 @@ class SlideTag(models.Model): class Slide(models.Model): - """ This model represents actual presentations. Those must be one of four - types: - - - Presentation - - Document - - Infographic - - Video - - Webpage - - Slide has various statistics like view count, embed count, like, dislikes """ - _name = 'slide.slide' - _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin', 'rating.mixin'] + _inherit = [ + 'mail.thread', 'rating.mixin', + 'image.mixin', + 'website.seo.metadata', 'website.published.mixin'] _description = 'Slides' _mail_post_access = 'read' _order_by_strategy = { @@ -99,10 +91,6 @@ class Slide(models.Model): } _order = 'category_sequence asc, sequence asc' - _sql_constraints = [ - ('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.") - ] - def _default_access_token(self): return str(uuid.uuid4()) @@ -119,14 +107,10 @@ class Slide(models.Model): access_token = fields.Char("Security Token", copy=False, default=_default_access_token) is_preview = fields.Boolean('Always visible', default=False) completion_time = fields.Float('# Hours', default=1, digits=(10, 4)) - image = fields.Binary("Image", attachment=True) - image_large = fields.Binary("Large image", attachment=True) - image_medium = fields.Binary("Medium image", attachment=True) - image_small = fields.Binary("Small image", attachment=True) # subscribers partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', - string='Subscribers', groups='base.group_website_publisher') - slide_partner_ids = fields.One2many('slide.slide.partner', 'slide_id', string='Subscribers information', groups='base.group_website_publisher') + string='Subscribers', groups='website.group_website_publisher') + slide_partner_ids = fields.One2many('slide.slide.partner', 'slide_id', string='Subscribers information', groups='website.group_website_publisher') user_membership_id = fields.Many2one( 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', help="Subscriber information for the current logged in user") @@ -161,13 +145,17 @@ class Slide(models.Model): likes = fields.Integer('Likes', compute='_compute_user_info', store=True) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True) user_vote = fields.Integer('User vote', compute='_compute_user_info') - embed_code = fields.Text('Embed Code', readonly=True, compute='_get_embed_code') + embed_code = fields.Text('Embed Code', readonly=True, compute='_compute_embed_code') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views") public_views = fields.Integer('# of Public Views') total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True) + _sql_constraints = [ + ('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.") + ] + @api.depends('slide_views', 'public_views') def _compute_total(self): for record in self: @@ -216,7 +204,8 @@ class Slide(models.Model): self.env['slide.slide.partner'] ) - def _get_embed_code(self): + @api.depends('document_id', 'slide_type', 'mime_type') + def _compute_embed_code(self): base_url = request and request.httprequest.url_root or self.env['ir.config_parameter'].sudo().get_param('web.base.url') if base_url[-1] == '/': base_url = base_url[:-1] @@ -235,7 +224,7 @@ class Slide(models.Model): record.embed_code = False @api.onchange('url') - def on_change_url(self): + def _on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) @@ -250,6 +239,7 @@ class Slide(models.Model): @api.multi @api.depends('name') def _compute_website_url(self): + # TDE FIXME: clena this link.tracker strange stuff super(Slide, self)._compute_website_url() base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for slide in self: @@ -296,9 +286,6 @@ class Slide(models.Model): for key, value in doc_data.items(): values.setdefault(key, value) - if 'image' in values: - tools.image_resize_images(values, return_large=True) - slide = super(Slide, self).create(values) if slide.website_published: @@ -312,15 +299,16 @@ class Slide(models.Model): for key, value in doc_data.items(): values.setdefault(key, value) - if 'image' in values: - tools.image_resize_images(values, return_large=True) - res = super(Slide, self).write(values) if values.get('website_published'): self.date_published = datetime.datetime.now() self._post_publication() return res + # --------------------------------------------------------- + # Mail/Rating + # --------------------------------------------------------- + @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to website if it is published. """ @@ -335,10 +323,6 @@ class Slide(models.Model): } return super(Slide, self).get_access_action(access_uid) - # --------------------------------------------------------- - # Business Methods - # --------------------------------------------------------- - @api.multi def _notify_get_groups(self, message, groups): """ Add access button to everyone if the document is active. """ @@ -350,7 +334,12 @@ class Slide(models.Model): return groups - def get_related_slides(self, limit=20): + # --------------------------------------------------------- + # Business Methods + # --------------------------------------------------------- + + def _get_related_slides(self, limit=20): + # TDE FIXME: should build a smarter domain domain = request.website.website_domain() domain += [('website_published', '=', True), ('id', '!=', self.id)] if self.category_id: @@ -358,7 +347,8 @@ class Slide(models.Model): for record in self.search(domain, limit=limit): yield record - def get_most_viewed_slides(self, limit=20): + def _get_most_viewed_slides(self, limit=20): + # TDE FIXME: should build a smarter domain domain = request.website.website_domain() domain += [('website_published', '=', True), ('id', '!=', self.id)] for record in self.search(domain, limit=limit, order='total_views desc'): @@ -387,10 +377,13 @@ class Slide(models.Model): self.write({'access_token': self._default_access_token()}) return self._sign_token(partner_id) - @api.one - def send_share_email(self, email): + def _send_share_email(self, email): + # TDE FIXME: template to check + mail_ids = [] base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - return self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(self.id, notif_layout='mail.mail_notification_light') + for record in self: + mail_ids.append(self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(record.id, notif_layout='mail.mail_notification_light')) + return mail_ids def action_like(self): self.check_access_rights('read') diff --git a/addons/website_slides/static/src/img/banner-default.jpg b/addons/website_slides/static/src/img/banner-default.jpg Binary files differdeleted file mode 100644 index a594ca906e8..00000000000 --- a/addons/website_slides/static/src/img/banner-default.jpg +++ /dev/null diff --git a/addons/website_slides/static/src/img/banner_default.svg b/addons/website_slides/static/src/img/banner_default.svg new file mode 100644 index 00000000000..1d8cbdaf77a --- /dev/null +++ b/addons/website_slides/static/src/img/banner_default.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="515"><defs><path id="a" d="M0 0L1280 0 1280 515 0 515z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#845978" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(-64 -902)"><path fill="#FFF" opacity=".105" d="M2443 1200.003L1425.898 1227.592 2428.994 1053.537 2410.807 959.354 1413.838 1188.559 2367.991 814.755 2332.04 726.066 1402.709 1149.229 2264.239 596.223 2212.162 516.325 1376.929 1117.888 2118.458 399.473 2052.002 331.594 1351.903 1085.917 1940.74 238.14 1862.693 184.686 1316.365 1067.012 1732.352 113.368 1645.54 76.631 1281.269 1047.257 1507.742 33.798 1415.612 14.927 1241.359 1043.705 1268.482 0 1174.518 0 1201.53 1039.145 1031.165 14.307 938.981 32.888 1163.316 1051.484 797.46 76.631 710.65 113.368 1124.834 1062.875 583.567 182.638 505.363 235.852 1094.152 1089.198 390.988 331.587 324.544 399.471 1062.838 1114.751 233.088 513.158 180.759 592.892 1044.335 1151.069 110.958 726.063 75.002 814.755 1025.021 1186.927 33.079 955.542 14.605 1049.674 1021.586 1227.721 0 1200.003 0 1295.992 1017.114 1268.403 14.001 1442.465 32.191 1536.639 1029.174 1307.438 75.002 1681.24 110.958 1769.932 1040.3 1346.768 178.761 1899.772 230.838 1979.679 1066.076 1378.114 324.544 2096.524 390.983 2164.406 1091.094 1410.09 502.255 2257.855 580.3 2311.319 1126.649 1428.964 710.641 2382.632 797.453 2419.374 1161.731 1448.736 935.256 2462.199 1027.383 2481.071 1201.646 1452.3 1174.518 2496 1268.482 2496 1241.475 1456.834 1411.837 2481.691 1504.019 2463.109 1279.686 1444.516 1645.54 2419.374 1732.352 2382.632 1318.176 1433.141 1859.435 2313.36 1937.649 2260.148 1348.874 1406.818 2052.009 2164.406 2118.458 2096.524 1380.157 1381.246 2209.92 1982.84 2262.241 1903.103 1398.639 1344.912 2332.047 1769.932 2367.991 1681.24 1417.991 1309.061 2409.923 1540.453 2428.386 1446.317 1421.502 1268.286 2443 1295.992 2443 1200.003"/><path fill="#845978" d="M64 1417L1344 1417 1344 1248 64 1248z"/><g transform="translate(892 1246)" fill="#573F51"><path d="M354.867 14.066L362.3 28 86.043 91.596 84.19 74.089z"/><path d="M248.749 6.147L253.38 18.931 28.168 71.1 30.604 58.435z"/><path d="M294.541 7.005L311.054 0 324.033 8.712 58.485 79.222 52.043 70.679z"/><path d="M172.008 16.138L209.092 6.884 213.083 12.127 0.734 61.158 0.891 56.487z"/></g><path fill="#573F51" d="M1111.493 1204.155c-.021.535-.06 1.102-.117 1.69-.356 3.73-1.408 8.343-3.014 11.624-2.639 5.398-4.017 11.736-3.912 16.782.105 5.044.703 9.657 1.787 13.639 1.082 3.979 2.8 6.916.164 9.283-1.315 1.183-2.172 1.28-3.133 1.056-.962-.225-2.029-.772-3.763-.88-3.468-.212-3.412.795-6.057 2.963-2.643 2.166-7.56 1.829-11.07.81-3.514-1.02-2.025-2.47-1.05-3.375 1.15-1.077 8.05-5.494 9.373-6.577 1.324-1.084 2.84-1.774 4.922-4.717 1.52-2.144 1.396-9.167 1.696-9.991.3-.824.118-26.067.225-27.286.004-.05.008-.118.011-.197.087-1.89.015-11.867-.166-15.148-.189-3.424-1.874-21.513-1.859-24.342.016-2.49-.223-10.77-.078-12.715.449-5.982 1.684-12.607 2.079-14.625-4.06-5.473-20.5-23.557-17.062-29.655 1.181-2.097 4.16-6.867 10.993-14.678 1.111-1.27 5.622-8.67 6.564-10.051 3.358-4.921 6.404-7.743 9.178-8.714.107-.066.203-.118.287-.154.1-.043.197-.088.292-.135.95-.765 2.077-1.477 2.687-1.967.56-.45.956-1.005 1.227-1.616a27.455 27.455 0 0 1-.397-.353c-3.066-1.651-6.327-5.703-7.704-11.32-1.135-4.632-1.509-9.206-.696-12.845.076-.969.141-1.94.123-2.693-.153-.132-.3-.27-.44-.414a4.451 4.451 0 0 1-1.253-2.742c-.086-1.08.41-1.714-.018-2.698-.936-2.12-.973-2.25.426-4.104 3.676-4.872 11.968-6.568 17.706-5.853 7.174.893 14.118 3.489 20.54 6.725 3.652 1.84 7.251 3.79 10.968 5.497 3.375 1.55 6.992 3.113 10.735 3.438 4.154.362 8.352-.28 12.49.392 1.773.287 4.26.443 5.61 1.78.247.247 2.205 3.21 1.81 3.409.094-.23-.967-1.403-1.137-1.597-.788-.9-1.703-1.352-2.893-1.395-2.524-.103-5.085.247-7.543.8.274.097 5.012 2.04 4.89 2.328-.026-.35-4.149-.697-4.446-.721a26.884 26.884 0 0 0-7.93.544c2.858.066 5.61.68 8.367 1.354-1.572.144-3.148.282-4.71.535-2.477.392-4.769 1.304-7.147 2.055 1.694.207 3.297-.054 4.943-.397 2.87-.585 5.825-.892 8.754-.665 2.58.206 5.575.828 7.278 2.95.675.842 1.216 1.858 1.36 2.94.042.318-.558 2.82-.905 2.474.293.189.435-1.687.407-1.808-.198-.881-.796-1.653-1.454-2.248-1.619-1.46-3.95-1.475-6.002-1.233-3.993.473-7.308 2.489-11.08 3.576a17.61 17.61 0 0 1-1.43.348c.369.618.762 1.225 1.192 1.814 3.905 5.34 10.308 2.235 15.31.52 5.26-1.803 12.645-2.675 17.262.952 1.685 1.324 2.916 3.496 3.463 5.545.46 1.726.58 3.588.226 5.343-.363 1.792-1.099 2.206-.193 3.969 1.318 2.584 3.388 4.936 5.84 6.492 1.452.92 3.156 1.594 4.903 1.485.444-.025 4.764-1.718 4.345-2.093.185.127-2.114 2.112-2.303 2.225-1.719 1.036-3.787 1.18-5.717.757-3.102-.674-6.367-2.076-9.164-3.541 1.545 2.146 2.88 4.442 4.263 6.694-1.37-1.242-2.724-2.503-4.113-3.723-1.867-1.632-3.826-3.274-6.185-4.136-2.203.983-4.22 2.36-6.147 3.799 3.977-.252 8.627.185 11.438 3.366 1.346 1.499 2.244 3.439 2.053 5.485-.036.397-1.543 4.917-1.985 4.59 1.155.42.785-5.933.5-6.49-1.256-2.448-3.925-3.62-6.564-3.715-4.79-.176-9.434 1.916-13.8 3.646-6.352 2.513-12.744 3.538-18.602 1.868 3.06 6.546 5.709 13.409 11.322 18.436 8.568 7.246 20.031 9.339 30.543 10.576 13.703 1.516 26.903 2.15 37.383 11.108 7.138 5.962 19.081 8.482 28.586 7.96-12.672.697-17.935 12.337-26.638 19.17-8.704 6.835-20.646 4.315-31.13 3.53-13.225-1.088-15.62 13.118-20.58 22.017-4.578 7.515-14.337 11.684-22.559 10.776-7.242-1.04-15.38-7.999-23.157-10.374.444 2.193.875 4.066.977 4.355.174.5 2.238 4.612 4.258 9.212l.151-.157.121.274c.48 1.099.92 2.14 1.308 3.094 1.502 3.69 2.926 9.864 1.196 18.319a212.782 212.782 0 0 1-.93 4.289c-1.299 5.803-2.16 9.635-1.35 17.602.223 2.225.77 4.009 1.255 5.586.917 2.99 1.712 5.576-.135 9.723-2.092 4.689-2.836 6.772-5.12 6.898-.625.032-1.266-.082-1.962-.356-2.96-1.16-4.164-8.26-2.866-12.488.598-1.944 1.118-3.414 1.575-4.71.498-1.413.892-2.528 1.21-3.745.581-2.25-2.86-21.918-3.369-24.276-.162-.752-.338-2.483-.558-4.675-.236-2.334-.528-5.24-.895-7.723l-.104-.697.247.29c-.31-2.02-.674-3.757-1.11-4.73-1.467-3.28-2.739-3-6.226-12.887-.306-.868-.602-1.722-.89-2.562-5.065 1.12-9.121 2.356-12.317 3.4-1.002.327-2.797 1.098-4.632 1.72.236 1.513.475 2.763.686 3.499.238.828.321 2.097.262 3.61z"/><path fill="#573F51" d="M1257.956 1185.867l.35-.214.211.39c.262.487.546.89.8 1.245.206.29.399.563.555.844l.061.109c4.155 7.467 5.06 15.14 5.086 20.264.037 7.473-2.373 17.109-3.969 23.485l-.231.919c.202 1.712.469 4.139.776 7.794.258 3.046.818 5.549 1.314 7.759 1.109 4.94 1.986 8.84-1.051 15.342-3.33 7.135-5.332 10.32-8.382 10.32h-.002c-.76 0-1.582-.191-2.584-.607-4.362-1.797-6.106-12.685-4.048-18.994 1.303-3.988 1.936-6.68 2.496-9.053.288-1.222.561-2.376.895-3.579.397-1.433.012-5.182-1.146-11.138-2.149-6.859-4.144-17.743-5.215-23.593-.146-.795-.275-1.5-.386-2.09-.369-1.975-.591-3.935-.808-5.828-.284-2.498-.554-4.857-1.118-6.696l-.311-1.014.518.295a8.435 8.435 0 0 0-.539-1.199c-1.739-3.133-2.525-1.848-3.936-6.913-1.073-3.85-2.386-7.25-4.824-11.594-.45 1.492-.972 2.944-1.467 4.45-.956 2.897-.981 5.362.018 8.193.372 1.055.661 2.155.879 3.283l.396-.17.092.522c.666 3.78.638 8.161-.082 13.025-1.258 8.369-5.06 15.851-9.181 23.415-.456.834-.956 1.661-1.483 2.538-2.069 3.434-4.209 6.985-3.53 10.831.24 1.357.664 2.715 1.117 4.154.708 2.268 1.443 4.614 1.383 6.941-.068 2.557-2.373 5.529-5.246 5.529-.293 0-.586-.031-.875-.094a32.806 32.806 0 0 1-1.978-.539c-1.333-.391-2.711-.799-4.04-.799-.814 0-1.54.15-2.22.461-.771.349-1.437.971-2.081 1.573-.201.191-.401.376-.606.556-1.635 1.443-3.043 2.228-4.708 2.621a15.96 15.96 0 0 1-3.687.409c-2.217 0-4.606-.39-7.102-1.161-.991-.309-3.054-.945-3.027-2.516.017-1.011.875-1.777 1.501-2.338l.329-.292c1.471-1.282 3.194-2.342 4.865-3.369.629-.389 1.286-.792 1.901-1.193 1.976-1.282 4.056-2.706 6.548-4.474 2.504-1.779 4.154-3.616 5.045-5.621.362-.812.772-1.619 1.167-2.398.774-1.529 1.576-3.109 2.029-4.762 1.025-3.75 1.81-15.59 1.818-15.697.195-5.522.458-11.423 1.046-17.151l.019-.185.15-.11c.1-.075.202-.149.305-.223.154-1.448.33-2.892.533-4.327.564-3.995 1.788-8.564-.075-12.392-1.48-3.041-1.218-6.035-1.48-9.355-.567-7.134-3.397-13.732-4.369-20.789a129.162 129.162 0 0 1-1.115-21.947c.093-2.66.242-5.345.652-7.965-.542-.86-1.107-1.598-1.697-2.116-2.767-2.43-16.811-11.084-19.283-15.707-2.477-4.623-.7-13.566.509-18.364 1.178-4.672 3.27-4.536 3.799-9.789.528-5.253 6.764-14.781 6.764-14.781s3.495-9.884 9.583-12.833c2.455-1.957 4.892-2.262 6.785-2.144.402-.1 4.41-1.094 8.661-2.19l-.045-.326c-2.84-.43-1.969-1.46-1.969-1.46s-.765.524-1.376.311c-1.565-.542-2.594-1.837-2.938-2.946-.345-1.108-.325-9.416-.89-10.669-.565-1.253-2.065-3.684-1.708-4.71.36-1.027 1.268-2.701 1.075-3.081-.194-.379-1.532-3.146-.991-4.745.196-.577.913-1.815 1.77-3.183-.175.187-.275.3-.275.3s.021-1.98 1.6-5.357c1.581-3.377 4.995-6.182 4.995-6.182l-.223 2.941s.534-2.55 3.965-3.455c3.43-.905 5.793.203 5.793.203l-1.948 1.433s3.144-1.532 6.542-1.134c3.399.396 6.636 3.149 6.636 3.149l-1.811.264s1.814 1.754 4.033 3.082c2.22 1.327 2.577 2.319 2.577 2.319s-1.825.38-2.071 2.489c-.247 2.109-.764 8.577-1.637 10.968-.663 1.813-1.795 3.57-2.304 4.313l.286.762c-1.129 2.61.276 4.743-.122 7.572a2.276 2.276 0 0 1-.34.877c13.797-2.203 30.236-1.437 38.839 1.84 16.726 6.432 26.376 23.159 45.675 23.802 19.301.644 38.601-12.223 58.543-7.076 5.79 1.287 9.649 4.504 13.511 8.363 5.146 4.503 10.291 9.006 16.084 12.223-12.225 4.504-21.873 2.573-23.805 18.657-1.285 13.509-1.93 26.375-17.369 29.592-13.511 3.217-27.663-3.86-39.887 4.504-7.72 5.146-7.077 14.797-11.58 21.872-4.502 9.651-10.936 16.083-21.871 14.797-12.226-1.287-21.873-9.65-34.098-9.007-2.497 0-4.996.607-7.493 1.819.9 8.704 1.49 16.902 1.077 20.069-.535 4.103-.024 6.368.676 7.846zm-47.861-77.259c1.04-3.92-.19-10.75-.803-14.726-.442-2.847-1.741-5.627-2.935-8.179-2.549 5.476-6.527 10.322-5.576 13.589 1.11 3.814 5.099 5.725 8.48 11.07.24-.634.691-1.929.691-1.929l.143.175z"/><g transform="translate(873 1139)"><path fill="#418ECA" d="M25.659 35.361c-3.172-2.336-6.91-.743-11.387 5.818-.707 1.039-4.095 6.596-4.929 7.549-5.133 5.867-7.37 9.449-8.257 11.024-2.707 4.801 10.995 19.498 13.189 22.801 2.192 3.301 1.362-6.213 1.362-6.213S7.703 63.739 7.749 63.136c.139-1.771 8.088-8.693 8.088-8.693s14.449-15.673 9.822-19.082"/><path fill="#1A1919" d="M12.857 51.647c-.474-1.979-.629-3.155.447-5.129.995-1.787 1.971-3.936 3.437-5.476 1.325-1.397 3.519-3.25 3.992-5.325 2.244-1.478 3.091-1.706 4.926-.356 4.627 3.409-9.822 19.082-9.822 19.082s-.308.237-1.477 1.316c-.536-1.429-1.226-2.819-1.503-4.112"/><path fill="#F1DCBC" d="M7.677 67.01c.265.114.596.29.919.272 0-.835-.057-1.567.078-2.329 2.082 3.634 6.963 11.387 6.963 11.387s.83 9.514-1.362 6.213c-1.503-2.263-8.412-9.878-11.739-15.992.605-.009 1.199-.003 1.761-.01 1.161.001 2.266.133 3.38.459"/><path fill="#2D2118" d="M67.078 66.508c-.148-.429-.288-.86-.423-1.295.256.374.301.868.423 1.295-.095-.271-.065-.227 0 0zm4.192-7.726c-.312 1.108-.841 2.211-1.687 3.014.877-3.611 1.518-6.704-.744-9.975 2.497 1.554 3.199 4.218 2.431 6.961-.235.835.232-.83 0 0zm-4.727 5.777c-1.107-1.913-1.362-4.535-2.186-6.615-.729-1.845-1.721-3.524-3.028-5.019 2.897.461 5.47 2.197 5.433 5.41-.026 2.201-.433 4.31-.067 6.509a19.071 19.071 0 0 0-.152-.285zM19.642 7.837c-5.382 8.241-4.964 16.814.79 24.746 1.901 4.55 4.693 8.136 8.709 11.016 2.418 1.72 2.927 4.571 3.602 7.322 1.965 8.011 8.398 11.746 16.182 13.188 3.469.641 7.258 1.163 10.237 3.186 1.639 1.113 2.871 2.924 2.693 4.983-.04.467-2.34 4.658-2.906 3.93.15.384 2.912-1.891 3.093-2.13.938-1.226 1.142-2.818.883-4.309-.52-3.145-3.305-5.278-5.936-6.704 1.801-.147 3.633-.22 5.427.033 1.158 1.491 1.749 3.317 2.285 5.1.399 1.331.757 2.674 1.133 4.01.018-1.985.084-3.978-.043-5.96 1.195 2.048 2.714 4.243 4.419 5.909 1.059 1.039 2.433 1.773 3.94 1.799.165.003 2.42-.342 2.354-.498.116.407-3.308-.239-3.6-.401-1.156-.627-1.972-1.736-2.529-2.901-.938-1.968-1.317-4.29-1.126-6.46.127-1.484.762-1.453 1.707-2.45.924-.975 1.591-2.208 1.986-3.489.47-1.523.555-3.395.009-4.91-1.49-4.15-6.537-6.541-10.605-7.492-3.865-.904-9.179-1.482-9.533-6.438-.265-3.698.89-7.276-.058-10.944-1.556-6.014-6.959-7.692-12.174-9.532-1.925-.677-3.754-1.549-5.235-2.977-1.689-1.636-1.027-5.038-1.737-7.147-.761-2.268-2.502-3.616-4.715-4.386-.508-.177-6.552-1.424-6.553-.103-.284.896-1.934 2.851-2.699 4.009-.848 1.298.856-1.295 0 0z"/><path fill="#F1DCBC" d="M31.302 24.242s.144 2.631 1.084 4.209c.939 1.576 5.973 4.308 5.973 4.308s-8.682 7.089-9.748 7.458c-1.065.368-3.41.033-4.292-.462-.88-.494-1.908-3.696-1.85-4.009.062-.315 1.136-1.925 1.472-2.874.336-.95.238-6.918.238-6.918l7.123-1.712"/><path fill="#C8A67A" d="M27.466 32.44c-1.221.265-2.352-.03-3.377-.598.155-1.998.09-5.888.09-5.888l7.123-1.712s.144 2.619 1.078 4.198c-1.212 1.75-2.845 3.554-4.914 4"/><path fill="#418ECA" d="M52.384 76.226c-.717-1.746-2.198-21.275-2.647-24.105-.475-3-1.764-5.256-2.042-6.095-.279-.838-.416-1.917-.386-2.772.032-.854-.726-6.617-3.544-9.424-1.527-1.519-4.439-1.695-5.757-1.7-1.32-.005-3.031.637-4.788 1.815-2.395 1.606-8.073 6.813-8.966 6.086-.893-.727-.697-5.476-1.035-6-.341-.524-1.003.133-1.837.489-.834.356-3.258 2.817-3.902 3.783-.39.583-.845 1.989-1.181 3.305-.949 1.267-1.916 2.872-2.225 3.489-.559 1.117-1.23 3.015-.252 5.289.979 2.274 2.491 4.363 3.246 5.406.079.111.179.241.288.38.183 1.025.346 2.078.509 3.211.377 2.617-.365 4.675-.652 6.475-.29 1.8-1.27 6.198-1.752 8.706 0 0 23.626 3.676 22.547 2.343-1.521-1.87-2.272-4.221-3.175-5.982-1.638-3.2-.891-6.997-.824-8.552.07-1.556 1.284-3.484 2.005-4.455.723-.971 2.262-5.428 2.692-6.073.429-.645 1.076 1.212 1.697 2.651 2.231 5.159 5.655 20.019 5.667 24.018.002.836.199 2.199.814 4.919.598 2.649 2.132 11.967 2.466 13.809.334 1.844-.053 1.865-.637 3.915-.586 2.05-.368 3.201-.459 4.368-.09 1.169.142 2.555.222 4.022.059 1.07-.04 1.492-.077 2.029.374-.035.749-.063 1.123-.098.18-.714-.036-1.908-.036-1.908l.035-.777c.035-.778.771-1.516.771-1.516s.843 2.667.877 3.286a4.56 4.56 0 0 1-.051.757c.57-.054 1.14-.113 1.71-.17.051-.312.085-.632.105-.994.077-1.4-.681-9.843-.62-11.552.062-1.711.832-16.989.917-18.235.088-1.247-.131-2.398-.846-4.143z"/><path fill="#F1DCBC" d="M13.275 114.357c.248 2.93.465 5.505.518 6.485.142 2.57.195 10.608.117 11.522-.08.915 10.276-.407 10.454-3.326.08-1.332.027-2.455-.175-3.157-.551-1.919-1.355-8.497-1.409-11.782-3.166.125-6.334.209-9.505.258zm32.218-.586c-.014-.246-.036-.633-.067-1.125-3.666.317-7.336.602-11.008.84.807 2.436 1.666 5.042 2.654 7.846 2.619 7.427 3.574 7.218 4.677 9.679.352.787.643 2.24.886 3.907.143.979 1.293 3.566 3.45 7.762 3.916-6.327 5.657-10.023 5.223-11.089a95.252 95.252 0 0 0-.948-2.244c-1.548-3.546-3.162-6.761-3.297-7.145-.165-.464-1.457-6.384-1.57-8.431z"/><path fill="#252A1D" d="M46.24 162.453c.456-1.76-2.155-16.589-2.526-18.303-.254-1.175-.565-5.746-1.09-9.302 1.91 2.246 3.142 4.89 3.142 4.89s.696-3.069 1.62-5.936c.631-1.954 2.083-3.652 2.94-4.533.334.764.669 1.546.982 2.322 1.111 2.729 2.197 7.29.892 13.669-1.306 6.379-2.464 9.117-1.716 16.484.49 4.86 2.943 6.733.853 11.422-2.09 4.687-3.008 5.666-5.125 4.834-2.117-.828-2.998-6.143-2.061-9.191.936-3.048 1.636-4.597 2.089-6.356m-44.51 6.389c.866-.809 6.047-4.125 7.04-4.938.995-.815 2.133-1.332 3.698-3.543 1.14-1.611 1.047-6.884 1.273-7.503.224-.62.089-19.579.169-20.494.003-.037.006-.088.008-.147a16.36 16.36 0 0 1 3.131-1.887c2.284-1.059 4.805-1.744 7.337-1.736-.117 2.953-.955 7.146-2.35 9.998-1.983 4.055-3.017 8.815-2.938 12.605.077 3.788.527 7.253 1.341 10.242.813 2.989 2.103 5.196.124 6.974-1.976 1.775-2.577.294-5.18.132-2.605-.16-2.563.598-4.549 2.226-1.984 1.625-5.679 1.373-8.314.608-2.639-.767-1.521-1.855-.79-2.537"/><path fill="#1A1919" d="M38.727 49.838c.449.402.692 1.411.731 2.504-.284-.531-.544-.81-.752-.497-.309.463-1.191 2.895-1.929 4.589.11-.501.173-1.069.172-1.725-.054-1.02-1.073-7.101 1.778-4.871"/><path fill="#F1DCBC" d="M21.655 8.25c4.388-1.947 12.618 2.256 12.52 10.33-.1 8.073-5.219 10.99-6.67 11.38-1.454.391-7.342-5.104-8.449-9.697-1.105-4.594-1.795-10.064 2.599-12.013"/><path fill="#50341D" d="M23.135.769c-1.631.621-2.748 2.046-2.998 3.769-.12.797-.689 1.004-1.064 1.724a3.338 3.338 0 0 0-.298 2.244c.571 2.631 3.126 4.794 5.193 6.362 2.318 1.757 4.762 3.332 7.077 5.095 1.198.912 2.495 1.752 3.588 2.787 1.112 1.048 1.803 2.418 2.41 3.802 1.484 3.374 2.887 6.494 5.852 8.851 1.346 1.07 2.862 1.8 4.174 2.894a12.895 12.895 0 0 0 4.708 2.483c2.835.813 5.748.852 8.477 2.144 1.402.665 2.881 1.604 3.329 3.18.181.641.253 1.37.027 2.011-.029.087-.869 1.224-.98.987.083.36 1.463-.994 1.563-1.212.341-.746.402-1.609.307-2.415-.236-2.029-1.894-3.62-3.454-4.78-1.773-1.313-3.776-2.296-5.836-3.069-1.184-.438-2.308-.912-3.303-1.72 1.813.471 3.635.805 5.368 1.544 1.095.463 2.153 1.004 3.211 1.539-1.486-1.529-2.993-3.018-4.785-4.198a20.193 20.193 0 0 1 5.264 2.815c.179.135 2.663 1.999 2.54 2.233.194-.134-2.047-3.261-2.183-3.432 1.785.628 3.555 1.427 5.121 2.501.738.501 1.14 1.155 1.283 2.041.031.192.239 1.36.086 1.47.331.032.268-2.636.208-2.891-.325-1.39-1.844-2.481-2.858-3.371-2.366-2.078-5.292-3.345-7.792-5.233-2.252-1.699-3.93-4.136-5.459-6.469-1.685-2.569-3.198-5.247-4.788-7.874-2.795-4.622-6.177-9.043-10.386-12.473C33.37 1.364 27.417-.864 23.135.769c-.943.36.948-.361 0 0"/><path fill="#343534" d="M11.419 107.536c0 3.597-.359 12.226-.359 12.226s9.709 2.878 19.777 3.238c10.069.359 17.98-3.598 19.418-4.316 1.439-.72 6.114-1.079 6.114-1.079S41.625 82.724 37.31 75.172c-4.315-7.552-21.935-.719-21.935-.719s-3.956 29.486-3.956 33.083z"/><path fill="#CBCCCB" d="M35.787 113.862L65.21 113.862 65.21 91.636 35.787 91.636z"/><path fill="#FFFFFE" d="M53.356 102.432a3.386 3.386 0 1 1-6.775 0 3.386 3.386 0 0 1 6.775 0"/><path fill="#F1DCBC" d="M56.411 114.063c-.102-.647-1.022-4.939-1.601-6.029-.579-1.09-2.213-5.415-2.316-6.301a6.177 6.177 0 0 0-.081-.502c-.072-1.187-.117-2.162-.1-2.627.056-1.573.712-14.603.884-17.671-1.449.638-2.606 1.769-2.948 3.462-.025.119-.162.146-.251.081-1.181-.862-2.301-1.782-3.46-2.641.098.475.213 1.002.346 1.598.598 2.649 2.132 11.967 2.466 13.809.334 1.844-.053 1.865-.637 3.915a10.41 10.41 0 0 0-.38 2.055l-.028.02s-1.124 5.755-1.159 6.744c-.034.988.103 2.794-.102 3.44-.204.647-.953 2.248-.953 2.248s1.226.408 1.669-.102c.442-.511 1.635-2.419 1.635-2.589 0-.17.238 2.112.238 2.112s.238 1.123.851.987c.614-.135.818-.987 1.397-.987.579 0 .238.681.783.681s.817-.919 1.26-.988c.443-.068.409.545 1.124.477.716-.068 1.465-.546 1.363-1.192"/></g><g transform="translate(925 1113)"><path fill="#907E63" d="M43.628 20.934s2.299 6.159 2.791 7.309c.493 1.149 1.971 4.106 2.3 4.27.328.164.083-.411 2.956-.247 2.874.165 2.793 3.203 2.793 3.203s-17.247 12.424-22.502 12.26c-5.257-.165-14.432-9.027-14.432-9.027s2.408-1.697 5.036-2.026c2.627-.328 7.508-1.207 7.508-1.207l-.82-5.995 14.37-8.54"/><path fill="#605442" d="M44.112 22.228l-.484-1.294-14.37 8.54.82 5.995.024.655c.666.174 1.467 0 2.132-.092.548-.075 1.096-.154 1.647-.16 3.391-.034 3.815-2.246 5.641-3.714.738-.595 1.789-1.177 2.504-1.841.698-.646 1.863-1.468 1.995-2.402.298-2.125-.756-3.727.091-5.687"/><path fill="#907E63" d="M23.137 12.53c-.406 1.201.598 3.28.743 3.564.146.285-.536 1.543-.805 2.314-.269.77.857 2.596 1.281 3.537.425.941.411 7.181.669 8.013.259.834 1.031 1.806 2.206 2.213.46.16 1.035-.233 1.035-.233s-.779.92 2.134 1.171 4.239.024 4.958-.784a13.006 13.006 0 0 1 3.252-2.652c1.532-.891 3.177-1.235 4.235-3.428 1.059-2.195.244-5.143.316-5.759.072-.615.908-3.193 1.026-4.962.117-1.77.029-9.982-4.898-10.736-4.928-.754-8.752-2.182-11.997 1.184-.347.361-3.747 5.358-4.155 6.558z"/><path fill="#252A1D" d="M10.417 216.905a11.96 11.96 0 0 0 2.741-.509c1.224-.385 2.236-1.049 3.383-2.22.142-.147.282-.296.423-.448.45-.487.914-.99 1.472-1.293a3.933 3.933 0 0 1 1.637-.468c.996-.072 2.05.157 3.07.377.499.11 1.016.22 1.512.296.22.031.441.039.661.023 2.152-.156 3.716-2.509 3.626-4.429-.082-1.747-.762-3.463-1.416-5.124-.417-1.051-.81-2.047-1.064-3.048-.719-2.846.689-5.624 2.051-8.308.347-.687.676-1.331.972-1.982 2.673-5.892 5.112-11.703 5.595-18.042.273-3.683.054-6.965-.652-9.759l-.097-.387-.354.183c-1.25.648-2.542 1.271-3.791 1.876-2.015.974-4.099 1.981-6.081 3.12-1.893 1.087-3.476 2.185-4.841 3.359l-.106.09-.003.14c-.127 4.324-.002 8.758.154 12.904.001.081.061 8.992-.502 11.858-.249 1.263-.763 2.49-1.259 3.679a37.83 37.83 0 0 0-.742 1.858c-.558 1.549-1.695 3.019-3.473 4.488a140.341 140.341 0 0 1-4.659 3.709c-.438.336-.908.671-1.358.999-1.195.86-2.429 1.749-3.46 2.788l-.002.001-.228.237c-.438.454-1.041 1.074-.997 1.832.066 1.178 1.646 1.541 2.406 1.72 1.912.439 3.723.601 5.382.48m48.342-17.409c-.061-.729-.12-1.391-.177-1.998-2.779.008-5.558.027-8.333.166.123 1.295.118 2.214-.023 2.725a69.888 69.888 0 0 0-.673 2.688c-.419 1.782-.896 3.802-1.874 6.799-1.546 4.737-.235 12.915 3.039 14.265.754.312 1.371.455 1.942.455h.001c2.291 0 3.795-2.391 6.296-7.75 2.28-4.883 1.622-7.814.789-11.523-.372-1.659-.794-3.54-.987-5.827"/><path fill="#1A1919" d="M43.173 6.685C41.498 1.614 35.196-.77 29.067 1.234c-6.128 2.004-9.714 7.787-8.059 12.795 5.94-5.694 14.013-8.316 22.165-7.344"/><path fill="#1A1919" d="M42.825 26.504c5.014-1.74 7.194-8.587 4.866-15.295C45.364 4.504 39.413.477 34.398 2.216l8.427 24.288"/><path fill="#907E63" d="M10.651 78.683a45.745 45.745 0 0 1 1.863-3.532c1.434 1.934 3.564 3.845 5.364 7.127 2.666 4.863 2.646 2.235 6.408 2.956 3.761.723 2.648 2.609 4.168 4.851 1.519 2.242 2.614 3.583 2.169 5.555-.446 1.982-2.352 3.947-6.213 4.788-2.96.642-4.507.99-6.024-.878-1.517-1.866-3.068-8.237-5.338-10.474l-2.663-10.294c.093.056.223-.01.266-.099"/><path fill="#343534" d="M19.27 83.782s-2.21 36.436-1.916 49.688c.269 12.047-2.613 44.024-2.613 44.024l-3.945 27.935 21.639.606s3.332-31.497 3.661-37.41c.329-5.914 3.075-39.73 3.075-39.73s4.058 45.376 4.387 47.675c.329 2.299 3.09 30.951 3.09 30.951l7.213 3.394 12.941-1.697s-1.899-43.82-2.885-49.732c-.985-5.914-2.495-21.418-3.48-27.332-.985-5.912-7.228-41.395-7.228-41.395l-1.971-3.614-31.968-3.363"/><path fill="#D89A13" d="M52.236 79.356c.742-4.081 5.936-21.521 5.936-21.521l12.653 14.101-15.621 17.439 3.339 8.163s24.155-19.666 25.639-23.005c1.485-3.34-15.62-33.394-18.959-36.734-3.34-3.34-10.532-5.159-13.8-5.362-3.269-.204-20.337 12.783-20.337 12.783l-2.339-9.926s-9.352.124-13.183 3.244c-2.459 2.001-7.112 10.393-9.338 15.958-2.227 5.566-9.018 15.987-4.937 20.811 4.082 4.824 13.789 17.201 13.789 17.201A639.882 639.882 0 0 0 18.968 87l-.34-5.241c-2.716-2.696-6.805-7.234-6.805-10.195 0-4.452 5.33-12.244 5.33-12.244l1.984 30.162 32.376-.816s-.019-5.228.723-9.31z"/><g transform="rotate(24 -140.5 137.11)"><path fill="#CBCCCB" d="M0.909 25.869L33.909 25.869 33.909 0.869 0.909 0.869z"/><path fill="#FFFFFE" d="M21.663 12.71a4 4 0 1 1-8.001 0 4 4 0 0 1 8 0"/></g><path fill="#907E63" d="M63.861 83.712l-7.227 3.596c-2.666 4.862-2.646 2.234-6.407 2.955-3.762.722-2.648 2.609-4.168 4.851-1.52 2.241-2.615 3.582-2.17 5.555.447 1.982 2.352 3.947 6.213 4.788 2.961.643 4.508.991 6.024-.878 1.517-1.866 3.068-8.238 5.338-10.474l2.663-10.294c-.093.057-.223-.01-.266-.099"/><path fill="#D89A13" d="M63.687 63.981l7.138 7.955-15.621 17.439 3.339 8.163s24.155-19.666 25.639-23.005c.76-1.711-3.358-10.434-8.005-18.985"/><path fill="#907E63" d="M39.404 18.504c.021 2.062.83 3.728 1.805 3.718.975-.011 1.748-1.692 1.727-3.755-.022-2.063-.829-3.728-1.804-3.717-.976.009-1.75 1.69-1.728 3.754"/><path fill="#583F07" d="M27.989 38.209L31.086 45.22 30.232 36.567 27.989 38.209"/><path fill="#886210" d="M29.74 33.119L26.456 34.268 24.54 42.862 30.232 36.567 29.74 33.119"/><path fill="#886210" d="M47.748 30.82L39.921 38.757 48.623 36.403 49.5 40.618 51.196 32.024 47.748 30.82"/></g></g></g></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/banner_default_all.svg b/addons/website_slides/static/src/img/banner_default_all.svg new file mode 100644 index 00000000000..7d5c3a66dae --- /dev/null +++ b/addons/website_slides/static/src/img/banner_default_all.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="515"><defs><path id="a" d="M0 0L1280 0 1280 515 0 515z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#845978" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(-238 -978)"><path fill="#FFF" opacity=".101" d="M2560 1257.215L1494.187 1286.119 2545.323 1103.766 2526.265 1005.092 1481.549 1245.226 2481.399 853.6 2443.725 760.682 1469.888 1204.02 2372.678 624.649 2318.107 540.942 1442.872 1171.185 2219.915 418.519 2150.277 347.403 1416.649 1137.689 2033.686 249.494 1951.901 193.491 1379.408 1117.883 1815.318 118.773 1724.348 80.284 1342.631 1097.186 1579.95 35.41 1483.409 15.639 1300.81 1093.465 1329.232 0 1230.768 0 1259.073 1088.687 1080.55 14.989 983.951 34.456 1219.029 1101.615 835.652 80.284 744.685 118.773 1178.704 1113.549 611.515 191.345 529.566 247.096 1146.553 1141.127 409.713 347.396 340.088 418.516 1113.739 1167.899 244.251 537.623 189.416 621.159 1094.35 1205.947 116.272 760.679 78.594 853.6 1074.111 1243.515 34.664 1001.099 15.304 1099.718 1070.511 1286.254 0 1257.215 0 1357.78 1065.826 1328.876 14.672 1511.237 33.732 1609.9 1078.463 1369.772 78.594 1761.395 116.272 1854.316 1090.122 1410.977 187.322 1990.346 241.893 2074.063 1117.133 1443.818 340.088 2196.479 409.708 2267.597 1143.349 1477.318 526.309 2365.501 608.092 2421.514 1180.607 1497.092 744.675 2496.227 835.644 2534.721 1217.369 1517.806 980.047 2579.588 1076.586 2599.359 1259.195 1521.54 1230.768 2615 1329.232 2615 1300.932 1526.29 1479.453 2600.009 1576.049 2580.541 1340.973 1513.385 1724.348 2534.721 1815.318 2496.227 1381.305 1501.468 1948.487 2423.652 2030.447 2367.904 1413.474 1473.89 2150.284 2267.597 2219.915 2196.479 1446.256 1447.099 2315.757 2077.374 2370.584 1993.836 1465.623 1409.033 2443.733 1854.316 2481.399 1761.395 1485.901 1371.472 2525.339 1613.896 2544.686 1515.272 1489.581 1328.754 2560 1357.78 2560 1257.215"/><path fill="#845978" d="M238 1530L1649 1530 1649 1288 238 1288z"/><path fill="#62495B" d="M1184.337 1239.582c.847 2.898-.292 11.332-2.838 16.44-2.61 5.243-3.973 11.4-3.87 16.3.104 4.9.696 9.381 1.768 13.249.61 2.202 1.424 4.075 1.53 5.704.116 1.291-.188 2.43-1.356 3.457-2.603 2.295-3.393.38-6.824.172-3.432-.206-3.377.77-5.994 2.872-2.615 2.1-7.481 1.773-10.954.786-1.726-.491-2.233-1.085-2.187-1.666-.052-.639.623-1.273 1.146-1.75 1.139-1.047 7.964-5.337 9.272-6.39 1.31-1.052 2.808-1.722 4.869-4.58 1.502-2.083 1.38-8.905 1.677-9.706.296-.8.117-25.32.223-26.503.103-1.182.032-11.58-.154-14.905-.187-3.326-1.853-20.897-1.838-23.644.015-2.42-.22-10.46-.078-12.351.41-5.36 1.482-11.253 1.951-13.672-4.047-5.29-17.535-20.276-17.058-27.208-.057-.897.08-1.685.456-2.335 1.178-2.043 4.149-6.69 10.967-14.3 1.108-1.237 5.609-8.446 6.549-9.792 2.508-3.59 4.842-6.03 7.015-7.427-.027.117-.055.234-.085.351.4-.253.795-.47 1.184-.652.237-.186.453-.34.638-.455.473-.493 1.117-.989 1.75-1.443.481-1.24 1.321-2.015 2.509-2.517-3.11-1.568-6.466-5.624-7.866-11.291-.793-3.207-1.213-6.385-1.116-9.238a36.606 36.606 0 0 1-.004-1.584c.05-2.044-.037 2.043 0 0 .03-1.213.25-3.094.303-4.57a7.24 7.24 0 0 1-.374-.35 4.327 4.327 0 0 1-1.252-2.69c-.086-1.057.41-1.68-.018-2.645-.936-2.078-.973-2.206.426-4.024 3.676-4.777 11.967-6.44 17.705-5.739 7.173.876 14.116 3.421 20.537 6.594 3.653 1.805 7.251 3.718 10.968 5.39 3.375 1.521 6.992 3.053 10.734 3.371 4.154.355 8.352-.273 12.49.385 1.772.281 4.26.434 5.608 1.746.248.241 2.206 3.148 1.81 3.342.095-.226-.966-1.376-1.136-1.566-.788-.882-1.703-1.326-2.893-1.368-2.524-.101-5.084.242-7.542.785.274.095 5.011 2 4.888 2.282-.025-.343-4.147-.683-4.444-.707a27.4 27.4 0 0 0-7.93.534c2.858.064 5.61.666 8.367 1.327-1.572.141-3.148.277-4.711.525-2.476.384-4.768 1.278-7.145 2.015 1.693.203 3.296-.053 4.942-.39 2.869-.573 5.825-.874 8.754-.652 2.58.202 5.574.812 7.277 2.893.675.826 1.216 1.822 1.359 2.883.022.163-.131.914-.333 1.544 4.53-.863 9.602-.703 13.088 2.02 1.676 1.31 2.9 3.46 3.444 5.487.458 1.708.577 3.55.225 5.286-.36 1.774-1.093 2.183-.192 3.927 1.311 2.557 3.37 4.884 5.808 6.424 1.445.911 3.14 1.577 4.877 1.47.442-.026 4.739-1.7 4.322-2.072.184.126-2.103 2.09-2.29 2.202-1.71 1.025-3.767 1.167-5.687.749-3.085-.667-6.333-2.054-9.115-3.504 1.537 2.123 2.864 4.395 4.24 6.623-1.362-1.229-2.709-2.476-4.09-3.683-1.857-1.615-3.806-3.24-6.152-4.093-2.191.973-4.198 2.336-6.114 3.76 3.956-.25 8.58.182 11.377 3.33 1.338 1.483 2.231 3.402 2.041 5.427-.035.392-1.534 4.865-1.974 4.541 1.149.416.78-5.87.497-6.42-1.249-2.423-3.903-3.582-6.528-3.677-4.764-.174-9.383 1.896-13.727 3.607-6.39 2.517-12.823 3.525-18.704 1.79 2.999 6.302 5.64 12.88 11.16 17.713 8.531 7.055 19.945 9.093 30.411 10.297 13.644 1.476 26.787 2.094 37.221 10.814 7.107 5.805 18.999 8.258 28.462 7.75-12.617.678-17.857 12.01-26.522 18.664-8.667 6.653-20.557 4.2-30.995 3.437-13.168-1.06-15.554 12.77-20.491 21.434-4.558 7.316-14.275 11.375-22.461 10.491-7.107-.997-15.08-7.592-22.722-9.996.467 2.288.937 4.31 1.045 4.606.212.6 3.277 6.553 5.59 12.14 1.463 3.533 2.892 9.43 1.173 17.681-1.72 8.25-3.245 11.791-2.26 21.321.641 6.24 3.83 8.669 1.184 14.64-.103.294-.219.596-.349.907-1.936 4.633-2.625 6.691-4.74 6.816-.578.031-1.172-.081-1.816-.352-2.175-.91-3.326-5.513-3.108-9.464.019-1.23.178-2.415.49-3.412.688-2.203 1.28-3.8 1.771-5.177.302-.93.556-1.757.77-2.639.54-2.223-2.647-21.654-3.117-23.984-.05-.245-.1-.596-.154-1.034-.475-3.534-1.13-13.188-2.435-16.05-1.452-3.185-2.71-2.913-6.159-12.517-.37-1.031-.726-2.042-1.071-3.033-5.187 1.108-9.326 2.34-12.572 3.377-.905.288-2.457.932-4.1 1.502.263 1.753.538 3.221.777 4.037zm-11.709-65.5c1.087-5.093 2.296-10.76 2.52-12.129.378-2.328 1.356-4.99.86-8.373-.388-2.641-.758-4.978-1.322-7.285-.747.847-1.202 1.338-1.202 1.338s-9.15 7.925-10.484 10.847c.444 1.05 2.883 5.021 5.29 8.849 1.714 2.697 3.396 5.3 4.338 6.753zm61.355-68.14c.228.352.467.7.718 1.041 3.884 5.285 10.252 2.212 15.227.516 1.19-.406 2.49-.765 3.843-1.034.062-.433.087-.88.074-.937-.198-.864-.796-1.621-1.454-2.205-1.619-1.431-3.95-1.446-6.002-1.208-3.992.463-7.307 2.44-11.08 3.506-.437.124-.88.23-1.326.32z"/><path fill="#62495B" d="M1331.97 1238.626l.367-.223.221.408c.275.51.574.931.84 1.303.216.303.42.59.583.883l.064.114c4.362 7.816 5.312 15.846 5.339 21.21.039 7.821-2.491 17.906-4.167 24.58l-.242.962c.212 1.792.492 4.332.815 8.157.27 3.188.858 5.808 1.379 8.121 1.164 5.17 2.085 9.252-1.103 16.058-3.496 7.467-5.598 10.801-8.8 10.801h-.002c-.797 0-1.66-.2-2.712-.635-4.58-1.881-6.41-13.277-4.25-19.88 1.368-4.174 2.033-6.992 2.62-9.475.303-1.28.59-2.487.94-3.746.417-1.5.013-5.424-1.203-11.658-2.256-7.179-4.35-18.57-5.474-24.693-.154-.832-.289-1.57-.406-2.188-.387-2.067-.62-4.118-.848-6.1-.298-2.614-.581-5.083-1.173-7.008l-.327-1.061.544.309a8.815 8.815 0 0 0-.566-1.255c-1.825-3.28-2.65-1.935-4.132-7.236-1.126-4.03-2.505-7.587-5.064-12.134-.473 1.561-1.02 3.08-1.54 4.657-1.003 3.032-1.03 5.612.02 8.575.39 1.104.693 2.256.921 3.436l.417-.178.096.547c.7 3.956.67 8.541-.086 13.632-1.32 8.76-5.312 16.59-9.638 24.507-.478.873-1.003 1.739-1.557 2.657-2.172 3.594-4.418 7.31-3.705 11.336.252 1.42.697 2.841 1.172 4.348.744 2.373 1.515 4.829 1.452 7.264-.071 2.677-2.49 5.787-5.507 5.787-.307 0-.615-.032-.918-.098a34.52 34.52 0 0 1-2.077-.564c-1.4-.41-2.846-.837-4.24-.837-.855 0-1.618.157-2.331.483-.81.365-1.509 1.016-2.185 1.646-.21.2-.42.394-.636.582-1.716 1.51-3.194 2.332-4.942 2.743a16.802 16.802 0 0 1-3.87.429c-2.328 0-4.836-.409-7.456-1.216-1.04-.323-3.206-.989-3.178-2.633.018-1.058.919-1.86 1.576-2.447l.345-.306c1.544-1.341 3.353-2.45 5.107-3.526.66-.407 1.35-.829 1.996-1.248 2.074-1.342 4.258-2.833 6.874-4.683 2.628-1.862 4.36-3.785 5.296-5.883.38-.85.81-1.695 1.225-2.51.812-1.6 1.654-3.254 2.13-4.984 1.076-3.925 1.9-16.317 1.908-16.43.205-5.779.481-11.955 1.098-17.95l.02-.194.158-.115c.106-.078.212-.156.32-.233.162-1.516.347-3.027.56-4.53.592-4.18 1.876-8.963-.08-12.97-1.553-3.182-1.278-6.316-1.553-9.79-.595-7.467-3.566-14.373-4.586-21.76a134.789 134.789 0 0 1-1.17-22.97c.096-2.784.253-5.595.683-8.337-.568-.9-1.161-1.672-1.78-2.214-2.905-2.543-17.648-11.6-20.243-16.44-2.6-4.838-.735-14.198.534-19.22 1.237-4.89 3.433-4.748 3.988-10.246.554-5.498 7.1-15.47 7.1-15.47s3.67-10.345 10.06-13.431c2.578-2.049 5.136-2.368 7.123-2.245.422-.104 4.63-1.145 9.092-2.29l-.047-.343c-2.981-.45-2.067-1.528-2.067-1.528s-.803.548-1.445.326c-1.642-.568-2.723-1.923-3.084-3.084-.362-1.16-.341-9.855-.934-11.167-.593-1.311-2.168-3.855-1.793-4.93.378-1.074 1.331-2.826 1.128-3.224-.203-.397-1.608-3.293-1.04-4.966.206-.605.958-1.9 1.858-3.332-.183.196-.289.314-.289.314s.023-2.072 1.68-5.607c1.66-3.534 5.244-6.47 5.244-6.47l-.234 3.078s.56-2.669 4.162-3.616c3.6-.947 6.081.213 6.081.213l-2.045 1.5s3.3-1.604 6.868-1.187c3.568.414 6.966 3.295 6.966 3.295l-1.901.277s1.904 1.836 4.233 3.226c2.33 1.388 2.706 2.427 2.706 2.427s-1.916.397-2.174 2.605c-.26 2.207-.802 8.977-1.719 11.48-.695 1.896-1.884 3.735-2.418 4.514l.3.797c-1.185 2.732.29 4.964-.128 7.925a2.378 2.378 0 0 1-.358.918c14.484-2.305 31.741-1.504 40.772 1.926 17.559 6.732 27.689 24.24 47.948 24.912 20.262.674 40.522-12.793 61.456-7.406 6.078 1.347 10.13 4.714 14.184 8.753 5.402 4.713 10.803 9.426 16.884 12.793-12.833 4.714-22.961 2.693-24.99 19.527-1.348 14.14-2.026 27.606-18.233 30.973-14.183 3.367-29.04-4.04-41.872 4.714-8.104 5.386-7.429 15.487-12.156 22.892-4.726 10.1-11.48 16.833-22.96 15.487-12.834-1.347-22.96-10.1-35.794-9.427-2.62 0-5.245.635-7.866 1.903.945 9.11 1.565 17.69 1.13 21.006-.56 4.294-.024 6.665.71 8.211zm-50.243-80.861c1.092-4.104-.2-11.253-.843-15.414-.464-2.979-1.828-5.89-3.08-8.56-2.677 5.731-6.853 10.803-5.855 14.223 1.166 3.992 5.353 5.992 8.902 11.585.251-.663.726-2.018.726-2.018l.15.184z"/></g></g></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/channel-default.jpg b/addons/website_slides/static/src/img/channel-default.jpg Binary files differdeleted file mode 100644 index dce2a23b9b2..00000000000 --- a/addons/website_slides/static/src/img/channel-default.jpg +++ /dev/null diff --git a/addons/website_slides/static/src/img/channel_banner_mountain.jpg b/addons/website_slides/static/src/img/channel_banner_mountain.jpg Binary files differnew file mode 100644 index 00000000000..2934f960ce5 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_banner_mountain.jpg diff --git a/addons/website_slides/static/src/img/channel_banner_mountain_all.jpg b/addons/website_slides/static/src/img/channel_banner_mountain_all.jpg Binary files differnew file mode 100644 index 00000000000..d30646d8b56 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_banner_mountain_all.jpg diff --git a/addons/website_slides/static/src/img/channel_banner_moutain.jpg b/addons/website_slides/static/src/img/channel_banner_moutain.jpg Binary files differdeleted file mode 100644 index c27e2a47434..00000000000 --- a/addons/website_slides/static/src/img/channel_banner_moutain.jpg +++ /dev/null diff --git a/addons/website_slides/static/src/img/channel_demo_gardening.jpg b/addons/website_slides/static/src/img/channel_demo_gardening.jpg Binary files differindex 3993e10b42b..2446a667733 100644 --- a/addons/website_slides/static/src/img/channel_demo_gardening.jpg +++ b/addons/website_slides/static/src/img/channel_demo_gardening.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg b/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg Binary files differindex 701af0f6588..f5899039d13 100644 --- a/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg +++ b/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg b/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg Binary files differnew file mode 100644 index 00000000000..3993e10b42b --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_tree_1.jpg b/addons/website_slides/static/src/img/channel_demo_tree_1.jpg Binary files differnew file mode 100644 index 00000000000..af533083e08 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_tree_1.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg b/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg Binary files differnew file mode 100644 index 00000000000..4169af2313a --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg diff --git a/addons/website_slides/static/src/js/slides_join_channel.js b/addons/website_slides/static/src/js/slides_join_channel.js index 342e27e738f..39e454dc93a 100644 --- a/addons/website_slides/static/src/js/slides_join_channel.js +++ b/addons/website_slides/static/src/js/slides_join_channel.js @@ -51,10 +51,10 @@ var JoinChannelButton = Widget.extend({ }, }).then(function (data) { if (! data.error) { - self.$('a.o_wslides_join_channel_link').text(_t('Joined')); + location.reload(); } else { if (data.error === 'public_user') { - self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to join this channel.'), (document.URL))); + self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to join this course.'), (document.URL))); } else if (data.error === 'join_done') { self._popoverAlert(self.$el, _t('You have already joined this channel')); } else { diff --git a/addons/website_slides/static/src/js/slides_like.js b/addons/website_slides/static/src/js/slides_like.js index 589ca6fefda..f0589373cb9 100644 --- a/addons/website_slides/static/src/js/slides_like.js +++ b/addons/website_slides/static/src/js/slides_like.js @@ -86,8 +86,11 @@ sAnimations.registry.websiteSlidesLike = sAnimations.Class.extend({ * @param {Object} parent */ start: function () { + var self = this; var defs = [this._super.apply(this, arguments)]; - defs.push(new LikeButton(this).attachTo($('.o_wslides_like'))); + $('.o_wslides_like').each(function () { + defs.push(new LikeButton(self).attachTo($(this))); + }); return $.when.apply($, defs); }, }); diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss index de5493d2f7e..d7368160733 100644 --- a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -66,7 +66,7 @@ .o_wslides_fullscreen_container { display: flex; align-items: stretch; - height: 100%; + height: calc(100% - 50px); position: relative; background-color: #313A44; } diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss index 75dd02662e4..b884a0df64f 100644 --- a/addons/website_slides/static/src/scss/website_slides.scss +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -1,13 +1,7 @@ // FIXME may be useless (check initial code here: https://github.com/odoo/odoo/blob/51be5bbfb38db784cb3406877cc0e8806b1bf55a/addons/website_slides/static/src/less/website_slides.less#L1) $nav-tabs-border-color: #dddddd !default; -$info: #dddddd !default; $body-bg: #FFFFFF !default; -$primary: #428bca !default; $link-color: theme-color('primary') !default; -$panel-footer-bg: #f5f5f5 !default; -$nav-link-hover-bg: #FFFFFF !default; -$navbar-default-link-hover-color: #333 !default; -$light: #f8f8f8 !default; $MAX-Z-INDEX : 2147483647 !default; @mixin top-covered-image { @@ -19,74 +13,61 @@ $MAX-Z-INDEX : 2147483647 !default; // Common to new slides pages // ************************************************** -.o_wslides_wrap { - .text_small_caps { - font-variant: small-caps; - } - .image-muted { - opacity: 0.5; - } - .cta-title { - font-size: 1.2em; - } +.o_wslides_container { + margin: auto; + width: 100%; + max-width: 1140px; + padding: 0px 32px 0px 32px; } - -// New home page -// ************************************************** - div .o_wslides_home_nav { - position: relative; - top: -50px; -} + top: -40px; -div .o_wslides_home_main { - .o_wslides_home_header { - } - .o_wslides_home_footer { - } + @include media-breakpoint-up(md) { + font-size: 1rem; - .o_wslides_home_aside { - .fa-check { - color: #00A09D; - font-size: 1.2em; - } - } - .o_wslides_home_content_section_title, .o_wslides_home_aside_title { - h3 { - font-size: 1.4em; - } - h4 { - font-size: 1.3em; + .o_wslides_nav_navbar_right { + margin-left: auto; + padding-left: $spacer; + border-left: 1px solid $border-color; } } +} + +.o_wslides_wrap { + background-color: desaturate(map-get($grays, "200"), 80%); .o_wslides_course_card { - .card-body { - padding: 0.75em 1em; - } - .card-footer { - padding-top: 0.75em 1em; - } + border-width: 0 0 3px 0; } - // responsive card-deck like - .row.o_wslides_row > div[class*='col-'] { - display: flex; - flex:1 0 auto; - .card { - width:100%; - } + + .o_wslides_lesson_card { + border-width: 0 0 3px 0; } + // responsive card-deck like + // .row.o_wslides_row > div[class*='col-'] { + // display: flex; + // flex:1 0 auto; + // .card { + // width:100%; + // } + // } + // tools // **************************************** + .text_small_caps { font-variant: small-caps; } - .image-muted { + .o_wslides_entry_muted { opacity: 0.5; } + img.o_wslides_img_center_crop { + object-fit: cover; + object-position: center; + } .o_wslides_home_circled { border-radius: 50%; @@ -134,16 +115,39 @@ div .o_wslides_home_main { } } -// New course page + +// New home page // ************************************************** -.o_wslides_container { - margin: auto; - width: 100%; - max-width: 1140px; - padding: 32px 32px 0px 32px; +div .o_wslides_home_nav { + top: -40px; + + @include media-breakpoint-up(md) { + font-size: 1rem; + + .o_wslides_nav_navbar_right { + margin-left: auto; + padding-left: $spacer; + border-left: 1px solid $border-color; + } + } } +div .o_wslides_home_main { + .o_wslides_home_aside { + .fa-check { + color: #00A09D; + font-size: 1.2em; + } + } + .o_wprofile_next_rank_card { + background-color: transparent !important; + } +} + +// New course page +// ************************************************** + .o_wslides_course_nav { margin: auto; background: linear-gradient(150deg, #875A7B 20%, #62495B 80%) !important; @@ -153,6 +157,10 @@ div .o_wslides_home_main { color: #FFFFFF; } + small a { + color: #000000; + } + nav { .breadcrumb-item::before { font-weight: bold; @@ -187,66 +195,54 @@ div .o_wslides_home_main { top: 48px; padding-top: 0px; - .o_wslides_course_header_content { - font-size: 16px; - color: #FFFFFF; - .o_wslides_course_description { - margin-right: 36px; - } - } - .o_wslides_course_header_aside { + max-width: 286px; img { width: 100%; max-width: 256px; } } + + .o_wslides_course_header_content { + font-size: 16px; + color: #FFFFFF; + + .o_wslides_course_header_nav { + ul.o_wslides_nav_tabs { + border-bottom: 0px; + li.nav-item { + margin-bottom: 0px; + } + a.nav-link { + color: white; + border-radius: 0px; + height: 48px; + font-weight: bold; + padding-top: 12px; + border-left: 2px solid #875A7B; + text-align: center; + width: 120px; + &:hover { + border: 0px; + border-radius: 0px; + background-color: #875A7B; + } + &.active { + color: #212529; + background-color: #f2f2f2; + border: 0px; + } + } + } + } + } } } -.o_wslides_course_content { +.o_wslides_course_main { margin: auto; background-color: #f2f2f2; - .o_wslides_content_container { - position: relative; - top: -80px; - } - - // responsive card-deck like - .row.o_wslides_row > div[class*='col-'] { - display: flex; - flex:1 0 auto; - .card { - width:100%; - } - } - - .o_wslides_course_content_nav { - .o_wslides_nav_tabs { - border-bottom: 0px; - } - .o_wslides_navlink { - color: white; - border-radius: 0px; - height: 48px; - font-weight: bold; - padding-top: 12px; - border-left: 2px solid #875A7B; - text-align: center; - width: 120px; - &:hover { - border: 0px; - border-radius: 0px; - background-color: #875A7B; - } - } - .o_wslides_navlink.active { - color: #212529; - background-color: #f2f2f2; - border: 0px; - } - } .o_wslides_course_content_main { h3 { font-size: 1.4em; @@ -257,20 +253,18 @@ div .o_wslides_home_main { } .o_wslides_course_content_aside { - top: 1px; - width: 256px; - max-width: 256px; - margin-left: 15px; - background-color: white; - - .o_wslides_course_aside_line { + max-width: 286px; + .bg-white { + max-width: 256px; + background-color: white; + padding-bottom: 8px; + > div.row { + padding-left: 15px; + padding-right: 15px; + } } - } - .o_wslides_lesson_card { - .card-footer { - padding-top: 0.75em; - padding-bottom: 0.75em; + .o_wslides_course_aside_line { } } } @@ -297,34 +291,6 @@ div .o_wslides_home_main { overflow-y: auto !important; } -.oe_slide_embed_wrapper { - position: relative; -} - -section.oe_slides_promote_box { - background-color: $panel-footer-bg; - border-bottom: 1px solid $nav-tabs-border-color; - border-top: 1px solid $nav-tabs-border-color; -} - -img.oe_slides_channel_thumbnail { - @include top-covered-image; - height: 250px; - width: 100% -} - -img.oe_slides_opacity { - opacity: .5; -} - -img.oe_slides_promote_image { - @include top-covered-image; - max-height: 300px; - width: 100%; - overflow: hidden; - display: block; -} - .oe_slides_apart_small { @include top-covered-image; height: 60px; @@ -340,26 +306,12 @@ img.oe_slides_promote_image { max-height: 500px; } -.oe_slides_bottom_border { - border-bottom: 1px solid $nav-tabs-border-color; -} - .o_wslides_like_up, .o_wslides_like_down { cursor: pointer; color: $link-color; } -textarea.o_slides_comment { - resize: vertical; -} - // Thumbnails -.oe_slides_grid_thumbnail { - @include top-covered-image; - height: 200px; - width: 100%; - overflow: hidden; -} .oe_slides_ellipsis { white-space: nowrap; width: 100%; diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml index a9aff0d2ec7..c1161a02488 100644 --- a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -3,11 +3,11 @@ <div t-if="slide.slide_type == 'video' && !showMiniQuiz" class="player embed-responsive embed-responsive-16by9 embed-responsive-item"> <iframe id="youtube-player" t-att-src="embed_url" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe> </div> - <div t-if="slide.slide_type in ['presentation', 'document'] && !showMiniQuiz" class="embed-responsive embed-responsive-4by3 embed-responsive-item "> + <div t-if="['presentation', 'document'].indexOf(slide.slide_type) !== -1 && !showMiniQuiz" class="embed-responsive embed-responsive-4by3 embed-responsive-item "> <iframe t-att-src="embed_url" class="o_wslides_iframe_viewer" allowFullScreen="true" height="315" width="420" frameborder="0"></iframe> </div> - <div style="width:100%;height:100%;" t-if="slide.slide_type == 'infographic' && !showMiniQuiz"> - <img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width: 100%" alt="Slide image"/> + <div style="width:100%;height:100%;text-align:center;" t-if="slide.slide_type == 'infographic' && !showMiniQuiz"> + <img t-attf-src="/web/image/slide.slide/#{slide.id}/image" class="img-fluid" style="height: 100%" alt="Slide image"/> </div> <div t-if="(slide.slide_type == 'quiz' || showMiniQuiz) && questions " class="o_wslides_fullscreen_quiz"> <div> diff --git a/addons/website_slides/tests/test_statistics.py b/addons/website_slides/tests/test_statistics.py index d59590d1cbf..8068c073bf3 100644 --- a/addons/website_slides/tests/test_statistics.py +++ b/addons/website_slides/tests/test_statistics.py @@ -37,10 +37,10 @@ class TestStatistics(common.SlidesCase): channel_publisher = self.channel.sudo(self.user_publisher) # slide type computation self.assertEqual(channel_publisher.total_slides, len(channel_publisher.slide_ids)) - self.assertEqual(channel_publisher.nbr_infographics, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'infographic'))) - self.assertEqual(channel_publisher.nbr_presentations, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'presentation'))) - self.assertEqual(channel_publisher.nbr_documents, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'document'))) - self.assertEqual(channel_publisher.nbr_videos, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'video'))) + self.assertEqual(channel_publisher.nbr_infographic, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'infographic'))) + self.assertEqual(channel_publisher.nbr_presentation, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'presentation'))) + self.assertEqual(channel_publisher.nbr_document, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'document'))) + self.assertEqual(channel_publisher.nbr_video, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'video'))) # slide statistics computation self.assertEqual(float_compare(channel_publisher.total_time, sum(s.completion_time for s in channel_publisher.slide_ids), 3), 0) # members computation diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index 74f920a68a7..e85097bdd8b 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -114,10 +114,11 @@ <page string="Statistics" groups="base.group_no_one"> <group> <group> - <field name="nbr_presentations"/> - <field name="nbr_documents"/> - <field name="nbr_videos"/> - <field name="nbr_infographics"/> + <field name="nbr_presentation"/> + <field name="nbr_document"/> + <field name="nbr_video"/> + <field name="nbr_infographic"/> + <field name="nbr_webpage"/> <field name="total_slides"/> </group> <group> diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml new file mode 100644 index 00000000000..3459fba05a5 --- /dev/null +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -0,0 +1,437 @@ +<?xml version="1.0" ?> +<odoo><data> + +<!-- Channels sub-template: header --> +<template id="course_nav" name="Course Navigation Header"> + <div class="o_wslides_course_nav"> + <div class="container"> + <div class="row align-items-center justify-content-between"> + <!-- nav --> + <nav aria-label="breadcrumb" class="col-md-6 col-xs-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a href="/slides">Home</a> + </li> + <li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')"> + <a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a> + </li> + <li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category"> + <a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a> + </li> + <li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag"> + <a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a> + </li> + <li class="breadcrumb-item active" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized"> + <a t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">Uncategorized</a> + </li> + <li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type"> + <a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a> + </li> + </ol> + </nav> + <div class="col-md-6 col-xs-12 d-flex flex-row align-items-center"> + <!-- karma / profile --> + <a t-if="not is_public_user" t-att-href="'/profile/user/%s?channel_id=%s' % (user.id, channel.id)" class="font-weight-bold mr-3"> + <i class="fa fa-diamond" /> <t t-esc="user_id.karma"/> + </a> + <!-- search --> + <form t-attf-action="/slides/#{slug(channel)}" role="search" method="get"> + <div class="input-group"> + <input type="text" class="form-control" name="search" t-attf-placeholder="Search in #{channel.name}" t-att-value="search"/> + <span class="input-group-append"> + <button class="btn btn-primary" type="submit" aria-label="Search" title="Search"> + <i class="fa fa-search"></i> + </button> + </span> + </div> + </form> + <!-- sort / filter --> + <small class="dropdown float-right ml-3"> + <h6 class="m-0"> + <button class="btn btn-primary dropdown-toggle" type="button" id="slidesChannelDropdownSort" + data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <t t-if="sorting == 'most_voted'">Most Voted</t> + <t t-elif="sorting == 'most_viewed'">Most Viewed</t> + <t t-else="">Newest</t> + </button> + <div class="dropdown-menu" aria-labelledby="slidesChannelDropdownSort" role="menu"> + <div role="separator" class="dropdown-header">Sort by</div> + <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='latest'))" + t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'latest' else '')">Newest</a> + <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_voted'))" + t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_voted' else '')">Most Voted</a> + <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_viewed'))" + t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_viewed' else '')">Most Viewed</a> + </div> + </h6> + </small> + </div> + </div> + </div> + </div> +</template> + +<!-- Channel main template --> +<template id='course_main' name="Course Main"> + <t t-set="head"> + <t t-call-assets="web.pdf_js_lib" t-css="false"/> + <script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script> + </t> + <t t-call="website.layout"> + <div class="wrap o_wslides_wrap"> + <t t-set="main_object" t-value="channel"/> + <t t-call="website_slides.course_nav"/> + + <div class="row o_wslides_course_header"> + <div class="col-lg-12 o_wslides_container o_wslides_course_header_container"> + <div class="row align-items-end d-flex"> + <div class="col-md-4 d-none d-sm-block o_wslides_course_header_aside"> + <div t-field="channel.image" widget="image" t-options="{'widget': 'image'}"/> + </div> + <div class="col-md-8 col-xs-12 align-self-stretch d-flex flex-column o_wslides_course_header_content"> + <div class="row"> + <h3 class="col-12"><span class="font-weight-bold" t-field="channel.name"/></h3> + <div class="col-12" t-field="channel.description"/> + <div class="col-xs-12 col-md-6 mt8"> + <t t-call="website_rating.rating_stars_static_popup_composer"> + <t t-set="rating_avg" t-value="rating_avg"/> + <t t-set="rating_total" t-value="rating_count"/> + <t t-set="object" t-value="channel"/> + <t t-set="hash" t-value="message_post_hash"/> + <t t-set="pid" t-value="message_post_pid"/> + <t t-set="default_message_id" t-value="last_message_id"/> + <t t-set="default_message" t-value="last_message"/> + <t t-set="default_rating_value" t-value="last_rating_value"/> + <t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/> + </t> + </div> + </div> + <div class="row mt-auto o_wslides_course_header_nav"> + <div class="col-xs-12 col-md-8"> + <ul class="nav nav-tabs o_wslides_nav_tabs" role="tablist" id="profile_extra_info_tablist"> + <li class="nav-item o_wslides_nav_tabs_item_home"> + <a t-att-href="'/slides/%s%s?%s' % (slug(channel), '/%s' % slug(category) if category else '', keep_query('debug'))" + t-att-class="'nav-link %s' % ('' if search_slide_type else 'active')"> + <i class="fa fa-home"/> Course</a> + </li> + <li class="nav-item"> + <a t-att-href="'/slides/%s' % (slug(channel))" + t-att-class="'nav-link'">Review</a> + </li> + </ul> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="row o_wslides_course_main"> + <div class="col-lg-12 o_wslides_container"> + <div class="row o_wslides_course_content_main"> + <!-- Channel presentation, aka general information + CTAs --> + <div class="col-md-4 col-xs-12 o_wslides_course_content_aside"> + <div class="bg-white"> + <div class="row justify-content-center o_wslides_join_channel"> + <a t-if="not channel.is_member and channel.enroll == 'public'" role="button" + class="col-10 mt8 btn btn-primary o_wslides_join_channel_link" + title="Start Course" aria-label="Start Course Channel" + t-att-href="'#'" + t-att-data-channel-id="channel.id"> + <span class="cta-title text_small_caps"> + <t t-if="channel.channel_type == 'documentation'">Start Course</t> + <t t-else="">Join Course</t> + </span> + </a> + <t t-if="not channel.is_member and channel.enroll == 'invite'"> + <a role="button" class="col-10 mt8 btn btn-light" t-att-href="'#'"> + <span class="cta-title text_small_caps">Private Course</span> + </a> + <div t-if="channel.enroll_msg" t-field="channel.enroll_msg"/> + <span t-if="not channel.enroll_msg">Contact website administrator.</span> + </t> + <span t-if="channel.is_member" class="col-10 mt8 btn btn-primary"> + <i class="fa fa-chevron-right"></i> + <span class="cta-title text_small_caps">Already member</span> + </span> + </div> + <div class="row mt8"> + <span class="col-5">Last Update</span> + <span class="col"><t t-esc="channel.slide_last_update" t-options="{'widget': 'date'}"/></span> + </div> + <div class="row mt8"> + <span class="col-5">Members</span> + <span class="col" t-esc="channel.members_count"/> + </div> + <t t-if="channel.channel_type == 'documentation'"> + <div class="row mt8"><div class="col"><hr/></div></div> + <t t-set="slide_type_keys" t-value="slide_types.keys()"/> + <t t-foreach="slide_type_keys" t-as="slide_type_key"> + <div class="row mt8 align-items-center"> + <span class="col-5"> + <t t-if="search_category"> + <a t-att-href="'/slides/%s/category/%s?%s' % (slug(channel), slug(search_category), keep_query('debug', slide_type=slide_type_key))" + t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')"> + <t t-esc="slide_types[slide_type_key]"/> + </a> + </t> + <t t-else=""> + <a t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', slide_type=slide_type_key))" + t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')"> + <t t-esc="slide_types[slide_type_key]"/> + </a> + </t> + </span> + <span class="col"> + <t t-if="search_category"> + <span class="badge badge-pill badge-info" t-esc="search_category['nbr_%s' % slide_type_key]"/> + </t> + <t t-else=""> + <span class="badge badge-pill badge-info" t-esc="channel['nbr_%s' % slide_type_key]"/> + </t> + </span> + </div> + </t> + </t> + <div class="row mt8"><div class="col"><hr/></div></div> + <div class="row justify-content-center mt8"> + <button role="button" class="btn btn-primary col-10" title="Share Channel" + aria-label="Share Channel" + data-toggle="modal" data-target="#slideChannelShareModal"> + <i class="fa fa-share-square"></i> Share + </button> + <div class="modal fade" id="slideChannelShareModal" tabindex="-1" role="dialog" aria-labelledby="slideChannelShareModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="slideChannelShareModalLabel"> + Share + </h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span>&times;</span> + </button> + </div> + <div class="modal-body"> + <h5>Share on Social Networks</h5> + <t t-call="website_slides.slides_share"> + <t t-set="record" t-value="channel"/> + </t> + <h5 class="mt16">Share Link</h5> + <input type="text" class="form-control" readonly="readonly" onclick="this.select();" + t-att-value="channel.website_url"/> + </div> + </div> + </div> + </div> + </div> + <div t-if="channel.can_upload" class="row justify-content-center mt8"> + <a role="button" class="btn btn-primary oe_slide_js_upload col-10" title="Upload Presentation" + aria-label="Upload Presentation" href="#" + t-att-data-channel-id="channel.id" + t-att-data-can-upload="channel.can_upload" + t-att-data-can-publish="channel.can_publish"> + <i class="fa fa-cloud-upload"></i> Upload + </a> + </div> + </div> + </div> + <t t-if="channel.channel_type == 'training'" t-call="website_slides.course_content_training"/> + <t t-if="channel.channel_type == 'documentation'" t-call="website_slides.course_content_documentation"/> + </div> + </div> + </div> + </div> + </t> +</template> + +<template id="course_content_training" name="Content of a training channel"> + <div class="col-8 d-flex flex-column ml-4"> + <div class="course-content oe_js_course_slides_list w-100 mt8"> + <ul> + <t t-set="i" t-value="1"/> + <t t-set="j" t-value="0"/> + <t t-foreach="channel.category_ids" t-as="category"> + <li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}"> + <div t-attf-category_id="#{category.id}" class="course-section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center"> + <div style="width:50%;" class="d-flex align-items-center"> + <i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i> + <div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div> + <span t-field="category.name"/> + </div> + <a class="mr-2 oe_slide_js_upload" + role="button" + aria-label="Upload Presentation" + href="#" + style="font-size: 1.5rem;text-decoration: none;" + t-att-data-channel-id="channel.id" + t-att-data-category-id="category.id" + t-att-data-can-upload="channel.can_upload" + t-att-data-can-publish="channel.can_publish">+</a> + <t t-set="i" t-value="i+1"/> + </div> + <ul t-attf-category_id="#{category.id}" > + <t t-foreach="category.slide_ids" t-as="slide"> + <li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}" t-attf-class="#{'content-slide slide-draggable d-flex justify-content-between align-items-center p-2' if channel.user_id == user else 'content-slide d-flex justify-content-between align-items-center p-2'}"> + <div class="content-slide-infos ml-2"> + <i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i> + <i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i> + <a class="link-to-slide" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a> + </div> + <div class="content-slide-controls mr-2"> + <i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i> + <i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i> + <a t-if="channel.user_id == user and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&action=#{slide_action}&model=slide.slide&view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a> + <a t-if="channel.user_id == user and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><i class="fa fa-pencil text-muted mr-1"></i></a> + <i t-if="channel.user_id == user" t-attf-slide_id="#{slide.id}" class="fa fa-trash text-muted oe_slide_js_delete_slide"></i> + </div> + </li> + <t t-set="j" t-value="j+1"/> + </t> + </ul> + </li> + </t> + </ul> + <div t-if="channel.can_upload" class="content-actions"> + <a class="mr-2 oe_slide_js_upload" + role="button" + aria-label="Upload Presentation" + href="#" + t-att-data-channel-id="channel.id" + t-att-data-can-upload="channel.can_upload" + t-att-data-can-publish="channel.can_publish">Add Content</a> + <a class="oe_slide_js_add_section" t-attf-channel_id="#{channel.id}" href="#" + groups="website.group_website_publisher">Add Section</a> + </div> + </div> + <div t-field="channel.description_html"/> + </div> +</template> + +<template id="course_content_documentation" name="Content of a training channel"> + <div t-if="not search" class="col-md-8 col-xs-12 mt16 o_wslides_channel_content_promoted"> + <div class="row align-items-center"> + <div class="col"> + <h5 class="m-0"> Featured lesson</h5> + <hr class="mt-2"/> + </div> + </div> + <div class="row" t-if="slide_promoted"> + <div class="col-4"> + <img class="img img-fluid" style="" + t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide_promoted.id" + t-att-alt="slide_promoted.name"/> + </div> + <div class="col-8"> + <div class="row"> + <h3 class="col-6" t-att-title="slide_promoted.name"> + <t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"> + <a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/> + </t> + <t t-else=""> + <span class="font-weight-bold text-muted" t-field="slide_promoted.name"/> + </t> + </h3> + <div class="col-12" t-field="slide_promoted.description"/> + </div> + </div> + </div> + </div> + <div t-if="search and not category_data" class="col-12"> + <div class="row mt16 justify-content-end"> + <div class="col-12"> + <p class="text-center">No content was found using your search <span class="font-weight-bold" t-esc="search"/>.</p> + </div> + </div> + </div> + <div t-if="category_data" class="col-12"> + <div class="tab-content mt16" id="slideChannelContent"> + <div class="tab-pane active" role="tabpanel" aria-labelledby="profile-tab" id="slideChannelContentAbout"> + <t t-foreach="category_data" t-as="category"> + <div class="row mt16 align-items-center"> + <div class="col"> + <t t-if="category['id']"> + <a t-att-href="'/slides/%s/category/%s' % (slug(channel), category['slug_name'])" class="float-right">View all</a> + </t> + <t t-else=""> + <a t-att-href="'/slides/%s?uncategorized=1' % (slug(channel))" class="float-right">View all</a> + </t> + <h5 class="m-0"><t t-esc="category['name']"/></h5> + <hr class="mt-2"/> + </div> + </div> + <t t-call="website_slides.course_lessons_grid"> + <t t-set="slides" t-value="category['slides']"/> + </t> + </t> + </div> + </div> + <div t-field="channel.description_html"/> + </div> +</template> + +<!-- Embedded template: display a list of slides in a grid view --> +<template id='course_lessons_grid' name="Course: Lessons Grid View"> + <div class="row o_wslides_row mx-n2 mt8"> + <t t-foreach="slides" t-as="slide"> + <div class="col-md-3 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.lesson_card"/> + </t> + </div> +</template> + +<template id='lesson_card' name="Lesson Card"> + <div class="card w-100 o_wslides_lesson_card mb-4"> + <t t-set="lesson_image" t-value="'/slides/slide/%s/get_image?field=image_large' % slide.id"/> + <t t-set="can_access" t-value="slide.is_preview or channel.is_member or is_slides_publisher"/> + <a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name"> + <div class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/> + </a> + <t t-else=""> + <div class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/> + </t> + <div class="card-body"> + <a t-if="can_access" class="card-title h5" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/> + <span t-else="" class="card-title h5 text-muted" t-esc="slide.name"/> + <div class="card-subtitle mb-2 text-muted"> + <t t-if="slide.is_preview"> + <span class="badge badge-info">Free preview</span> + </t> + <t t-if="not slide.is_published and user.has_group('website.group_website_publisher')"> + <span class="badge badge-danger">Unpublished</span> + </t> + </div> + <div class="card-text mt-1"> + <div class="font-weight-light oe_no_empty" t-field="slide.description"/> + <div t-if="slide.tag_ids" class="border-top mt-2 pt-1"> + <t t-foreach="slide.tag_ids" t-as="tag"> + <a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-light" t-esc="tag.name"/> + </t> + </div> + </div> + </div> + <div class="card-footer border-top-0 text-600"> + <div class="d-flex align-items-center small"> + <span class="font-weight-bold mr-auto" t-field="slide.completion_time" t-options='{"widget": "float_time"}'/> + <div class="o_wslides_like mr-2"> + <span class="o_wslides_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id"> + <i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i> + <span t-esc="slide.likes"/> + </span> + <span class="o_wslides_like_down" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id"> + <i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i> + <span t-esc="slide.dislikes"/> + </span> + </div> + <t t-if="channel.is_member and slide.id in done_slide_ids"> + <span class="badge badge-pill badge-success"><i class="fa fa-check"/> Completed</span> + </t> + </div> + </div> + </div> +</template> + +</data></odoo> diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml index 4364b486987..cad54cf323d 100644 --- a/addons/website_slides/views/website_slides_templates_homepage.xml +++ b/addons/website_slides/views/website_slides_templates_homepage.xml @@ -4,84 +4,94 @@ <!-- Channels home template --> <template id='courses_home' name="Odoo Courses Homepage"> <t t-call="website.layout"> - <t t-call="website_slides.courses_header"> - <t t-set="header_title" t-value="'Reach new heights'"/> - <t t-set="header_subtitle" t-value="'Start your online course today !'"/> - <t t-set="header_text" t-value="'Skill up and have an impact! Your business career starts here. Time to start a course.'"/> - <t t-set="header_cta_url" t-value="'slides'"/> - <t t-set="header_cta_title" t-value="'Pick a course'"/> - </t> - <div class="wrap"> - <div class="container mt16 o_wslides_home_nav"> - <nav class="navbar navbar-expand-lg navbar-light bg-light"> + <div class="wrap o_wslides_wrap"> + <section class="s_banner overflow-hidden bg-900" style="background-image: url("/website_slides/static/src/img/banner_default.svg"); background-size: cover; background-position: 55% 65%"> + <div class="container align-items-center d-flex mb-5 mt-lg-5 pt-lg-4 pb-lg-1"> + <div> + <h1 class="display-3 mb-0">Reach new heights</h1> + <h2 class="mb-4">Start your online course today !</h2> + <div class="row mt-1 mb-3"> + <div class="col-lg-4"> + <p>Skill up and have an impact! Your business career starts here. Time to start a course.</p> + </div> + <div class="col-lg-3"> + <a href="slides/all" class="btn btn-primary btn-lg">Pick a course</a> + </div> + </div> + </div> + </div> + </section> + <div class="container mt16 o_wslides_home_nav position-relative"> + <nav class="navbar navbar-expand-md navbar-light shadow-sm" style="background: white!important"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSlidesHomepage" aria-controls="navbarSlidesHomepage" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> + <span class="navbar-toggler-icon"/> </button> <div class="collapse navbar-collapse" id="navbarSlidesHomepage"> - <a class="navbar-brand" href="#"></a> - <ul class="navbar-nav mr-auto"> - <li class="nav-item o_wslides_home_all_slides"> - <h4><a class="nav-link" href="/slides/all"><i class="fa fa-rocket"/> All courses</a></h4> - </li> - <li class="nav-item ml16"> - <h4><a class="nav-link" href="/slides/all"><i class="fa fa-file"/> My courses</a></h4> - </li> - </ul> + <div class="navbar-nav"> + <a class="nav-link nav-link mr-md-2 o_wslides_home_all_slides" href="/slides/all"><i class="fa fa-graduation-cap mr-1"/>All courses</a> + </div> + <div class="navbar-nav o_wslides_nav_navbar_right"> + <t t-if="is_public_user"> + <a class="nav-link nav-link" href="/web/login?redirect=/slides/all?my=1"><i class="fa fa-file mr-1"/>My courses</a> + </t><t t-else=""> + <a class="nav-link nav-link" href="/slides/all?my=1"><i class="fa fa-file mr-1"/>My courses</a> + </t> + </div> </div> </nav> </div> - <div class="container mt16 o_wslides_home_main"> + <div class="container o_wslides_home_main"> <div class="row"> - <div class="col-md-9"> + <div class="col-lg-9 pr-lg-5"> <t t-if="channels_my"> <t t-set="void_count" t-value="3 - len(channels_my[:3])"/> - <div class="o_wslides_home_content_section"> - <div class="row mt16 o_wslides_home_content_section_title align-items-center"> + <div class="o_wslides_home_content_section mb-5"> + <div class="row o_wslides_home_content_section_title align-items-center"> <div class="col"> <a href="slides/all" class="float-right">View all</a> - <h3><i class="fa fa-certificate"/> Complete latest courses</h3> - <hr/> + <h5 class="m-0">Complete latest courses</h5> + <hr class="mt-2"/> </div> </div> - <div class="row o_wslides_row mt8"> + <div class="row o_wslides_row mx-n2 mt8"> <t t-foreach="channels_my[:3]" t-as="channel"> - <div class="col-md-4 col-sm-6 col-xs-12" t-call="website_slides.course_card"/> + <div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/> </t> </div> </div> </t> - <div class="o_wslides_home_content_section"> - <div class="row mt16 o_wslides_home_content_section_title align-items-center"> + <div class="o_wslides_home_content_section mb-5"> + <div class="row o_wslides_home_content_section_title align-items-center"> <div class="col"> <a href="slides/all" class="float-right">View all</a> - <h3><i class="fa fa-fire"/> Most popular courses</h3> - <hr/> + <h5 class="m-0">Most popular courses</h5> + <hr class="mt-2"/> </div> </div> - <div class="row o_wslides_row mt8"> + <div class="row o_wslides_row mx-n2 mt8"> <t t-foreach="channels_popular[:3]" t-as="channel"> - <div class="col-md-4 col-sm-6 col-xs-12" t-call="website_slides.course_card"/> + <div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/> </t> </div> </div> - <div class="o_wslides_home_content_section"> - <div class="row mt16 o_wslides_home_content_section_title align-items-center"> + <div class="o_wslides_home_content_section mb-5"> + <div class="row o_wslides_home_content_section_title align-items-center"> <div class="col"> <a href="slides/all" class="float-right">View all</a> - <h3><i class="fa fa-plus-square"/> Newest courses</h3> - <hr/> + <h5 class="m-0">Newest courses</h5> + <hr class="mt-2"/> </div> </div> - <div class="row o_wslides_row mt8"> + <div class="row o_wslides_row mx-n2 mt8"> <t t-foreach="channels_newest[:3]" t-as="channel"> - <div class="col-md-4 col-sm-6 col-xs-12" t-call="website_slides.course_card"/> + <div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/> </t> </div> </div> </div> - <div class="col-md-3"> + <div class="col-lg-3"> <t t-if="is_public_user"> <t t-call="website_slides.slides_home_achievements_small"/> <t t-call="website_slides.slides_home_users_small"/> @@ -94,28 +104,40 @@ </div> </div> </div> + <section class="s_banner bg-900" style="background-image: url("/website/static/src/img/backgrounds/people.jpg"); background-size: cover; background-position: 65% 55%"> + <div class="container py-5"> + <h1 class="display-3 mb-0">Meet the people who transformed their company</h1> + <h2 class="mb-4">Nobody gave me a real text so instead of a lorem ipsum I put that one.</h2> + <div class="row mt-1 mb-3"> + <div class="col-lg-4"> + <p>Nobody gave me a real text so instead of a lorem ipsum I put that one.</p> + </div> + <div class="col-lg-3"> + <a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a> + </div> + </div> + </div> + </section> </div> - <t t-call="website_slides.courses_footer"> - <t t-set="footer_title" t-value="'Meet the people who transformed their company'"/> - <t t-set="footer_text" t-value="'Nobody gave me a real text so instead of a lorem ipsum I put that one.'"/> - <t t-set="footer_cta_url" t-value="'slides'"/> - <t t-set="footer_cta_title" t-value="'Go meet them'"/> - </t> </t> </template> <!-- Channel all/main template --> <template id='courses_all' name="Odoo All Courses"> <t t-call="website.layout"> - <t t-call="website_slides.courses_header"> - <t t-set="header_title" t-value="'All Courses'"/> - <t t-set="header_subtitle" t-value="'Start your online course today !'"/> - </t> - <div class="wrap"> - <div class="container mt16 o_wslides_home_nav"> + <div class="wrap o_wslides_wrap"> + <section class="s_banner bg-900" style="background-image: url("/website_slides/static/src/img/banner_default_all.svg"); background-size: cover; background-position: 80% 20%"> + <div class="container py-5"> + <h1 class="display-3 mb-0"><t t-if="search_my">My Courses</t><t t-else="">All Courses</t></h1> + <h3 class="mb-4">Start your online course today !</h3> + </div> + </section> + <div class="container mt16 o_wslides_home_nav position-relative"> <!-- Navbar dynamically composed using displayed channel tag groups. --> - <nav class="navbar navbar-expand-lg navbar-light bg-light"> - <a class="navbar-brand" t-att-href="'/slides?%s' % keep_query('debug')"><i class="fa fa-chevron-left"/></a> + <nav class="navbar navbar-expand-md navbar-light shadow-sm" style="background: white!important"> + <div class="navbar-nav border-right pr-3"> + <a class="nav-link nav-item" t-att-href="'/slides?%s' % keep_query('debug')"><i class="fa fa-chevron-left"/></a> + </div> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarTagGroups" aria-controls="navbarTagGroups" aria-expanded="false" aria-label="Toggle navigation"> @@ -123,15 +145,15 @@ </button> <div class="collapse navbar-collapse" id="navbarTagGroups"> <t t-set="search_tag_groups" t-value="search_tags.mapped('group_id')"/> - <ul class="navbar-nav mr-auto"> + <ul class="navbar-nav flex-grow-1"> <t t-foreach="tag_groups" t-as="tag_group"> <li t-att-class="'nav-item dropdown ml16 %s' % ('active' if tag_group in search_tag_groups else '')"> - <h4><a class="nav-link dropdown-toggle" + <a class="nav-link dropdown-toggle" href="/slides/all" t-att-data-target="'#navToogleTagGroup%s' % tag_group.id" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" - t-esc="tag_group.name"/></h4> + t-esc="tag_group.name"/> <div class="dropdown-menu" t-att-id="'navToogleTagGroup%s' % tag_group.id"> <t t-foreach="tag_group.tag_ids" t-as="tag"> <a t-att-class="'dropdown-item %s' % ('active' if tag in search_tags else '')" @@ -141,19 +163,29 @@ </div> </li> </t> + <!-- karma / profile --> + <li class="nav-item ml-auto" t-if="not is_public_user"> + <a t-att-href="'/profile/user/%s' % user.id" class="nav-link mr-2" + role="button" title="My Profile"> + <i class="fa fa-diamond"/> <t t-esc="user_id.karma"/> + </a> + </li> + <!-- clear filtering --> + <li class="nav-item" t-if="search_slide_type or search_my or search_tags or search_channel_tag_id"> + <a t-att-href="'/slides/all?%s' % keep_query('debug')" class="nav-link mr-2" + role="button" title="Clear filters"> + <i class="fa fa-eraser"/> + </a> + </li> </ul> - <!-- karma / profile --> - <a t-if="not is_public_user" t-att-href="'/profile/user/%s' % user.id" class="font-weight-bold mr-3"> - <i class="fa fa-diamond"/> <t t-esc="user_id.karma"/> - </a> <!-- search box --> - <form method="GET" class="form-inline justify-content-center"> + <form method="GET" class="form-inline o_wslides_nav_navbar_right"> <div class="input-group"> <input type="search" name="search" class="form-control" placeholder="Search something" aria-label="Search" t-att-value="search_term"/> <div class="input-group-append"> - <button class="btn btn-outline-success oe_search_button" type="submit" aria-label="Search" title="Search"> + <button class="btn border oe_search_button" type="submit" aria-label="Search" title="Search"> <i class="fa fa-search"/> </button> </div> @@ -162,127 +194,62 @@ </div> </nav> </div> - <div class="container mt16 o_wslides_home_main"> - <t t-if="search_term and not channels_layouted"> - <div class="row"> - <div class="col"> - <p class="alert alert-info">No channel was found matching your search <t t-esc="search_term"/>.</p> - </div> - </div> - </t> - <t t-foreach="channels_layouted" t-as="channels_row"> - <t t-set="missing" t-value="0"/> - <div class="card-deck mb32"> - <t t-foreach="channels_row" t-as="channel"> - <t t-if="channel"> - <t t-call="website_slides.course_card"/> - </t> - <t t-else=""> - <t t-set="missing" t-value="missing + 1"/> - </t> - </t> - <t t-if="missing"> - <div t-att-class="'col-%s' % (missing * 3)"/> - </t> - </div> - </t> - </div> - </div> - <t t-call="website_slides.courses_footer"> - <t t-set="footer_title" t-value="'Meet the people who transformed their company'"/> - <t t-set="footer_text" t-value="'Nobody gave me a real text so instead of a lorem ipsum I put that one.'"/> - <t t-set="footer_cta_url" t-value="'slides'"/> - <t t-set="footer_cta_title" t-value="'Go meet them'"/> - </t> - </t> -</template> - -<template id="courses_header"> - <section class="s_cover parallax bg-black-50 pt40 pb40 o_wslides_home_header" data-scroll-background-ratio="0" style="background-image: none;"> - <span class="s_parallax_bg oe_img_bg oe_custom_bg" style="background-image: url("/website_slides/static/src/img/channel_banner_moutain.jpg"); background-position: 52.1% 17.69%;"></span> - <div class="container"> - <div class="row s_nb_column_fixed"> - <div class="s_title pt0 col-lg-12" data-name="Title"> - <h1 class="s_title_small_caps" style="font-size: 62px; text-align: left;"><t t-esc="header_title"/></h1> - <h4 t-if="header_subtitle" class="s_title_small_caps" style="font-size: 34px; text-align: left;"><t t-esc="header_subtitle"/></h4> + <div class="container o_wslides_home_main pb-5"> + <div t-if="search_term and not channels" class="alert alert-info mb-5"> + No channel was found matching your search <code><t t-esc="search_term"/></code>. </div> - </div> - <div class="row s_nb_column_fixed"> - <div t-if="header_text" class="col-lg-3 s_text pt16 pb16" data-name="Text"> - <p class="lead" style="text-align: left;"><t t-esc="header_text"/></p> - </div> - <div t-if="header_cta_title" class="col-lg-3 s_btn text-center pt16 pb16" data-name="Buttons"> - <a t-att-href="header_cta_url" - class="btn btn-primary o_default_snippet_text text_small_caps"><t t-esc="header_cta_title"/></a> + <div t-else="" class="row mx-n2"> + <t t-foreach="channels" t-as="channel"> + <div class="col-12 col-sm-6 col-md-4 col-lg-3 px-2 d-flex flex-grow-1" t-call="website_slides.course_card"/> + </t> </div> </div> - </div> - </section> -</template> -<template id="courses_footer"> - <section class="s_cover parallax bg-black-50 pt40 pb40 o_wslides_home_footer mt16" data-scroll-background-ratio="0" style="background-image: none;"> - <span class="s_parallax_bg oe_img_bg oe_custom_bg" style="background-image: url("/website/static/src/img/backgrounds/people.jpg"); background-position: 50.11% 29.1%;"></span> - <div class="container"> - <div class="row s_nb_column_fixed"> - <div class="col-lg-12 s_title" data-name="Title"> - <h1 class="s_title_small_caps" style="font-size: 40px; text-align: left;"><t t-esc="footer_title"/></h1> - <h4 t-if="footer_subtitle" class="s_title_small_caps" style="font-size: 26px; text-align: left;"><t t-esc="footer_subtitle"/></h4> - </div> - </div> - <div class="row s_nb_column_fixed"> - <div t-if="footer_text" class="col-lg-3 s_text pt16 pb16" data-name="Text"> - <p class="lead o_default_snippet_text" style="text-align: left;"><t t-esc="footer_text"/></p> - </div> - <div t-if="footer_cta_title" class="col-lg-3 s_btn text-center pt16 pb16" data-name="Buttons"> - <a t-att-href="footer_cta_url" - class="btn btn-primary o_default_snippet_text text_small_caps"><t t-esc="footer_cta_title"/></a> + <section class="s_banner bg-900" style="background-image: url("/website/static/src/img/backgrounds/people.jpg"); background-size: cover; background-position: 65% 55%"> + <div class="container py-5"> + <h1 class="display-3 mb-0">Meet the people who transformed their company</h1> + <h2 class="mb-4">Nobody gave me a real text so instead of a lorem ipsum I put that one.</h2> + <div class="row mt-1 mb-3"> + <div class="col-lg-4"> + <p>Nobody gave me a real text so instead of a lorem ipsum I put that one.</p> + </div> + <div class="col-lg-3"> + <a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a> + </div> + </div> </div> - </div> + </section> </div> - </section> + </t> </template> <template id='course_card' name="Course Card"> - <div class="card o_wslides_course_card"> - <img class="card-img-top img-fuild" - style="max-height: 160px;" - t-att-src="'/web/image/slide.channel/%s/image_large' % channel.id if channel.image else '/website_slides/static/src/img/channel-default.jpg'" - t-att-alt="channel.name"/> + <div class="card w-100 o_wslides_course_card mb-4"> + <t t-set="course_image" t-value="'/web/image/slide.channel/%s/image_large' % channel.id"/> + <a t-attf-href="/slides/#{slug(channel)}" t-title="channel.name"> + <div class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{course_image}); background-size: cover; background-position:center"/> + </a> <div class="card-body"> - <h5 class="card-title" t-att-title="channel.name"> - <a t-attf-href="/slides/#{slug(channel)}" t-esc="channel.name"/> - </h5> - <div class="card-text"> - <div class="text-muted oe_no_empty" t-field="channel.description"/> - <div t-if="channel.tag_ids"> + <a class="card-title h5" t-attf-href="/slides/#{slug(channel)}" t-esc="channel.name"/> + <div class="card-text mt-1"> + <div class="font-weight-light oe_no_empty" t-field="channel.description"/> + <div t-if="channel.tag_ids" class="border-top mt-2 pt-1"> <t t-foreach="channel.tag_ids" t-as="tag"> - <a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-secondary"> - <t t-esc="tag.name"/> - </a> + <a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/> </t> </div> </div> </div> - <div class="card-footer"> - <div class="row justify-content-between align-items-center"> - <small class="text-muted col-3"> - <t t-call="website_slides.slides_misc_float_time"> - <t t-set="value" t-value="channel.total_time"/> - </t> - </small> - <t t-if="channel.is_member"> - <div class="col"> - <div class="progress"> - <div class="progress-bar" role="progressbar" t-att-aria-valuenow="channel.completion" aria-valuemin="0" aria-valuemax="100" t-attf-style="width:#{channel.completion}%;"> - <!-- <span><t t-esc="channel.completion"/>%</span> --> - </div> - </div> - </div> - </t> - <t t-else=""> - <small class="text-muted col-5"><span t-esc="channel.total_slides"/> steps</small> + <div class="card-footer border-top-0 text-600"> + <div class="d-flex justify-content-between align-items-center small"> + <span class="font-weight-bold" t-field="channel.total_time" t-options='{"widget": "float_time"}'/> + <t t-if="channel.is_member and channel.completed"> + <span class="badge badge-pill badge-success pull-right"><i class="fa fa-check"/> Completed</span> </t> + <div t-elif="channel.is_member" class="progress w-50"> + <div class="progress-bar" role="progressbar" t-att-aria-valuenow="channel.completion" aria-valuemin="0" aria-valuemax="100" t-attf-style="width:#{channel.completion}%;"/> + </div> + <t t-else=""><span class="font-weight-bold" t-esc="channel.total_slides"/> steps</t> </div> </div> </div> @@ -290,13 +257,11 @@ <template id='slides_home_achievements_small' name="Latest Achievements"> <div class="o_wslides_home_aside"> - <div class="row mt16 o_wslides_home_aside_title"> + <div class="row o_wslides_home_aside_title"> <div class="col"> - <a href="slides" class="text-muted float-right"> - <small class="float-right">View all</small> - </a> - <h3>Latest achievements</h3> - <hr/> + <a href="/slides" class="float-right">View all</a> + <h5 class="m-0">Latest achievements</h5> + <hr class="mt-2"/> </div> </div> <div class="row"> @@ -327,13 +292,11 @@ <template id='slides_home_users_small' name="Latest Users"> <div class="o_wslides_home_aside"> - <div class="row mt16 o_wslides_home_aside_title"> + <div class="row o_wslides_home_aside_title"> <div class="col"> - <a href="slides" class="text-muted float-right"> - <small class="float-right">View all</small> - </a> - <h3>Heroes of the week</h3> - <hr/> + <a href="/profile/users" class="float-right">View all</a> + <h5 class="m-0">Heroes of the week</h5> + <hr class="mt-2"/> </div> </div> <div class="row"> @@ -370,41 +333,47 @@ <template id='slides_home_user_profile_small' name="User Profile"> <div class="o_wslides_home_aside"> - <div class="row mt16 o_wslides_home_aside_title align-items-center"> - <div class="col-12"> - <t t-call="website_slides.slides_misc_user_image"/> - <a t-att-href="'/profile/user/%s' % user.id" class="float-right">Edit</a> - <h4 t-esc="user.name" class="font-weight-bold"/> - <hr/> + <div class="o_wslides_home_aside_title"> + <div class="d-flex align-items-center"> + <t t-call="website_slides.slides_misc_user_image"> + <t t-set="img_class" t-value="'rounded-circle mr-1'"/> + <t t-set="img_style" t-value="'height: 22px'"/> + </t> + <h5 t-esc="user.name" class="d-flex flex-grow-1 mb-0"/> + <a t-att-href="'/profile/user/%s' % user.id">Edit</a> </div> - <div class="col-12 mt8"> - <span class="font-weight-bold text-muted">Current rank:</span> - <a href="/profile/ranks" class="float-right" t-field="user.rank_id"/> - <t t-call="website_profile.profile_next_rank_card"/> - <div t-field="user.next_rank_id.description_motivational"/> + </div> + <hr class="mt-2"/> + <div class="mt8"> + <div class="d-flex align-items-center"> + <span class="font-weight-bold text-muted mr-2">Current rank:</span> + <img t-attf-src="/web/image/gamification.karma.rank/#{user.rank_id.id}/image_small" height="16" alt="" class="mr-1"/> + <a href="/profile/ranks" t-field="user.rank_id"/> </div> + <div class="font-weight-bold text-muted mt-1">Next rank:</div> + <t t-call="website_profile.profile_next_rank_card"> + <t t-set="bg_class">bg-200</t> + <t t-set="img_max_width">50%</t> + </t> + <div t-field="user.next_rank_id.description_motivational"/> </div> </div> </template> <template id='slides_home_user_achievements_small' name="User Achievements"> - <div class="o_wslides_home_aside mt16"> + <div class="o_wslides_home_aside mb-2"> <t t-foreach="challenges" t-as="challenge"> <t t-set="challenge_done" t-value="challenge in challenges_done if challenges_done else False"/> - <div class="row no-gutters mt8 align-items-center"> - <div class="col-3"> - <img t-att-class="'rounded-circle float-left %s' % 'image-muted' if not challenge_done else ''" - style="max-height: 36px;" - t-att-src="'/web/image/gamification.badge/%s/image_medium' % challenge.reward_id.id" - t-att-alt="challenge.reward_id.name"/> - </div> - <div class="col-7"> - <span t-att-class="'font-weight-bold text_small_caps %s' % 'text-muted' if not challenge_done else ''" t-esc="challenge.reward_id.name"/><br/> + <div t-attf-class="d-flex mt-3 align-items-center #{'o_wslides_entry_muted' if not challenge_done else ''}"> + <img class="mr-2" + style="max-height: 36px;" + t-att-src="'/web/image/gamification.badge/%s/image_medium' % challenge.reward_id.id" + t-att-alt="challenge.reward_id.name"/> + <div class="flex-grow-1"> + <b class="text_small_caps" t-esc="challenge.reward_id.name"/><br/> <span class="text-muted" t-esc="challenge.reward_id.description"/> </div> - <div class="col-2" t-if="challenge_done"> - <i class="fa fa-check float-right" aria-label="Done" title="Done" role="img"></i> - </div> + <i t-if="challenge_done" class="fa fa-check" aria-label="Done" title="Done" role="img"></i> </div> </t> </div> @@ -417,13 +386,4 @@ t-att-alt="user.name"/> </template> -<template id='slides_misc_float_time' name="Float Time"> - <t t-set="hours" t-value="int(value)"/> - <t t-set="minuts" t-value="int(60.0 * (value - int(value)))"/> - <span t-esc="hours"/> hr - <t t-if="minuts"> - <span t-esc="minuts"/> mins - </t> -</template> - </data></odoo> diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index 7f0bc0d9553..6c3c9c9f54b 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -1,454 +1,7 @@ <?xml version="1.0" ?> -<odoo> -<data> - -<!-- Tools template: share on social networkds --> -<template id='slides_share' name="Slides Media Share"> - <div> - <a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="o_slides_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-2x"/></a> - <a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="o_slides_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-2x"/></a> - <a t-attf-href="http://www.linkedin.com/shareArticle?mini=true&url=#{record.website_url}&title=#{record.name}&" social-key="linkedin" class="o_slides_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-2x"/></a> - </div> -</template> - -<!-- Channels: no channel found --> -<template id="channel_not_found" name="Private Presentation"> - <t t-call="website.layout"> - <div class="container"> - <div class="oe_structure"> - <t t-if="search_term"> - <h2>No channel found when searching "<i><t t-esc="search_term"/></i>".</h2> - </t> - <t t-else=""> - <h2>No channel created or published yet.</h2> - </t> - </div> - </div> - </t> -</template> - -<!-- Channels sub-template: header --> -<template id="course_nav" name="Course Navigation Header"> - <div class="o_wslides_course_nav"> - <div class="container"> - <div class="row align-items-center justify-content-between"> - <!-- nav --> - <nav aria-label="breadcrumb" class="col-6"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"> - <a href="/slides">Home</a> - </li> - <li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')"> - <a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a> - </li> - <li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category"> - <a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a> - </li> - <li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag"> - <a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a> - </li> - <li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type"> - <a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a> - </li> - </ol> - </nav> - <div class="col-6 d-flex flex-row align-items-center"> - <!-- karma / profile --> - <a t-if="not is_public_user" t-att-href="'/profile/user/%s?channel_id=%s' % (user.id, channel.id)" class="font-weight-bold mr-3"> - <i class="fa fa-diamond" /> <t t-esc="user_id.karma"/> - </a> - <!-- search --> - <form t-attf-action="/slides/#{slug(channel)}" role="search" method="get"> - <div class="input-group"> - <input type="text" class="form-control" name="search" t-attf-placeholder="Search in #{channel.name}" t-att-value="search"/> - <span class="input-group-append"> - <button class="btn btn-primary" type="submit" aria-label="Search" title="Search"> - <i class="fa fa-search"></i> - </button> - </span> - </div> - </form> - <!-- sort / filter --> - <small class="dropdown float-right ml-3"> - <h6 class="m-0"> - <button class="btn btn-primary dropdown-toggle" type="button" id="slidesChannelDropdownSort" - data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> - <t t-if="sorting == 'most_voted'">Most Voted</t> - <t t-elif="sorting == 'most_viewed'">Most Viewed</t> - <t t-else="">Newest</t> - </button> - <div class="dropdown-menu" aria-labelledby="slidesChannelDropdownSort" role="menu"> - <div role="separator" class="dropdown-header">Sort by</div> - <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='latest'))" - t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'latest' else '')">Newest</a> - <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_voted'))" - t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_voted' else '')">Most Voted</a> - <a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_viewed'))" - t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_viewed' else '')">Most Viewed</a> - </div> - </h6> - </small> - </div> - </div> - </div> - </div> -</template> - -<!-- Channel main template --> -<template id='course_main' name="Course Main"> - <t t-set="head"> - <t t-call-assets="web.pdf_js_lib" t-css="false"/> - <script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script> - </t> - <t t-call="website.layout"> - <div class="wrap o_wslides_wrap"> - <t t-set="main_object" t-value="channel"/> - <t t-call="website_slides.course_nav"/> - - <div class="row o_wslides_course_header"> - <div class="col-lg-12 o_wslides_container o_wslides_course_header_container"> - <div class="row align-items-end"> - <div class="col o_wslides_course_header_aside main_image"> - <div t-field="channel.image" widget="image" t-options="{'widget': 'image'}"/> - </div> - <div class="col-8 o_wslides_course_header_content"> - <h3 class="row"><span class="font-weight-bold col" t-field="channel.name"/></h3> - <div class="row"> - <div class="col-12 o_wslides_course_description" t-field="channel.description"/> - <div class="col-6 mt8 mb64"> - <t t-call="website_rating.rating_stars_static_popup_composer"> - <t t-set="rating_avg" t-value="rating_avg"/> - <t t-set="rating_total" t-value="rating_count"/> - <t t-set="object" t-value="channel"/> - <t t-set="hash" t-value="message_post_hash"/> - <t t-set="pid" t-value="message_post_pid"/> - <t t-set="default_message_id" t-value="last_message_id"/> - <t t-set="default_message" t-value="last_message"/> - <t t-set="default_rating_value" t-value="last_rating_value"/> - <t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/> - </t> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="row o_wslides_course_content"> - <div class="col-lg-12 o_wslides_container o_wslides_content_container"> - <div class="row justify-content-end o_wslides_course_content_nav"> - <div class="col-8"> - <ul class="nav nav-tabs o_wslides_nav_tabs" role="tablist" id="profile_extra_info_tablist"> - <li class="nav-item o_wslides_nav_tabs_item_home"> - <a t-att-href="'/slides/%s%s?%s' % (slug(channel), '/%s' % slug(category) if category else '', keep_query('debug'))" - t-att-class="'nav-link o_wslides_navlink %s' % ('' if search_slide_type else 'active')"> - <i class="fa fa-home"/> Home</a> - </li> - <li class="nav-item"> - <a t-att-href="'/slides/%s' % (slug(channel))" - t-att-class="'nav-link o_wslides_navlink'">Review</a> - </li> - </ul> - </div> - </div> - <div class="row o_wslides_course_content_main"> - <!-- Channel presentation, aka general information + CTAs --> - <div class="col-4 o_wslides_course_content_aside"> - <div class="row justify-content-center mt8 o_wslides_join_channel"> - <a t-if="not channel.is_member and channel.enroll == 'public'" role="button" - class="col-10 btn btn-primary o_wslides_join_channel_link" - title="Start Course" aria-label="Start Course Channel" - t-att-href="'#'" - t-att-data-channel-id="channel.id"> - <span class="cta-title text_small_caps"> - <t t-if="channel.channel_type == 'documentation'">Start Course</t> - <t t-else="">Join Course</t> - </span> - </a> - <t t-if="not channel.is_member and channel.enroll == 'invite'"> - <a role="button" class="col-10 btn btn-light" t-att-href="'#'"> - <span class="cta-title text_small_caps">Private Course</span> - </a> - <div t-if="channel.enroll_msg" t-field="channel.enroll_msg"/> - <span t-if="not channel.enroll_msg">Contact website administrator.</span> - </t> - <span t-if="channel.is_member" class="col-10 btn btn-primary"> - <i class="fa fa-chevron-right"></i> - <span class="cta-title text_small_caps">Already member</span> - </span> - </div> - <div class="row mt8"> - <span class="col-5">Last Update</span> - <span class="col"><t t-esc="channel.slide_last_update" t-options="{'widget': 'date'}"/></span> - </div> - <div class="row mt8"> - <span class="col-5">Members</span> - <span class="col" t-esc="channel.members_count"/> - </div> - <div class="row mt8"><div class="col"><hr/></div></div> - <t t-set="slide_type_keys" t-value="slide_types.keys()"/> - <t t-foreach="slide_type_keys" t-as="slide_type_key"> - <div class="row mt8 align-items-center"> - <span class="col-5"> - <t t-if="search_category"> - <a t-att-href="'/slides/%s/category/%s?%s' % (slug(channel), slug(search_category), keep_query('debug', slide_type=slide_type_key))" - t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')"> - <t t-esc="slide_types[slide_type_key]"/> - </a> - </t> - <t t-else=""> - <a t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', slide_type=slide_type_key))" - t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')"> - <t t-esc="slide_types[slide_type_key]"/> - </a> - </t> - </span> - <span class="col"> - <t t-if="search_category"> - <span class="badge badge-pill badge-info" t-esc="search_category['nbr_%ss' % slide_type_key]"/> - </t> - <t t-else=""> - <span class="badge badge-pill badge-info" t-esc="channel['nbr_%ss' % slide_type_key]"/> - </t> - </span> - </div> - </t> - <div class="row mt8"><div class="col"><hr/></div></div> - <div class="row justify-content-center mt8"> - <button role="button" class="btn btn-primary col-10" title="Share Channel" - aria-label="Share Channel" - data-toggle="modal" data-target="#slideChannelShareModal"> - <i class="fa fa-share-square"></i> Share - </button> - <div class="modal fade" id="slideChannelShareModal" tabindex="-1" role="dialog" aria-labelledby="slideChannelShareModalLabel" aria-hidden="true"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="slideChannelShareModalLabel"> - Share - </h5> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span>&times;</span> - </button> - </div> - <div class="modal-body"> - <h5>Share on Social Networks</h5> - <t t-call="website_slides.slides_share"> - <t t-set="record" t-value="channel"/> - </t> - <h5 class="mt16">Share Link</h5> - <input type="text" class="form-control" readonly="readonly" onclick="this.select();" - t-att-value="channel.website_url"/> - </div> - </div> - </div> - </div> - - </div> - <div t-if="channel.can_upload" class="row justify-content-center mt8 mb8"> - <a role="button" class="btn btn-primary oe_slide_js_upload col-10" title="Upload Presentation" - aria-label="Upload Presentation" href="#" - t-att-data-channel-id="channel.id" - t-att-data-can-upload="channel.can_upload" - t-att-data-can-publish="channel.can_publish"> - <i class="fa fa-cloud-upload"></i> Upload - </a> - </div> - </div> - <!-- training mode ==============================================--> - <div t-if="channel.channel_type == 'training'" style="width:70%;" class="d-flex align-items-center ml-4 justify-content-center"> - <div class="course-content oe_js_course_slides_list" style="width: 100%"> - <ul> - <t t-set="i" t-value="1"/> - <t t-set="j" t-value="0"/> - <t t-foreach="channel.category_ids" t-as="category"> - <li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}"> - <div t-attf-category_id="#{category.id}" class="course-section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center"> - <div style="width:50%;" class="d-flex align-items-center"> - <i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i> - <div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div> - <span t-field="category.name"/> - </div> - <!-- <a t-if="channel.user_id == user" title="Add content to this section" class="mr-2 p-0 oe_slide_js_add_slide" href="#" t-attf-channel_id="#{channel.id}" t-attf-category_id="#{category.id}">+</a> --> - <a class="mr-2 oe_slide_js_upload" - role="button" - aria-label="Upload Presentation" - href="#" - style="font-size: 1.5rem;text-decoration: none;" - t-att-data-channel-id="channel.id" - t-att-data-category-id="category.id" - t-att-data-can-upload="channel.can_upload" - t-att-data-can-publish="channel.can_publish">+</a> - <t t-set="i" t-value="i+1"/> - </div> - <ul t-attf-category_id="#{category.id}" > - <t t-foreach="category.slide_ids" t-as="slide"> - <li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}" t-attf-class="#{'content-slide slide-draggable d-flex justify-content-between align-items-center p-2' if channel.user_id == user else 'content-slide d-flex justify-content-between align-items-center p-2'}"> - <div class="content-slide-infos ml-2"> - <i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i> - <i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i> - <a class="link-to-slide" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a> - </div> - <div class="content-slide-controls mr-2"> - <i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i> - <i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i> - <a t-if="channel.user_id == user and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&action=#{slide_action}&model=slide.slide&view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a> - <a t-if="channel.user_id == user and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><i class="fa fa-pencil text-muted mr-1"></i></a> - <i t-if="channel.user_id == user" t-attf-slide_id="#{slide.id}" class="fa fa-trash text-muted oe_slide_js_delete_slide"></i> - </div> - </li> - <t t-set="j" t-value="j+1"/> - </t> - </ul> - </li> - </t> - </ul> - <div t-if="channel.user_id == user" class="content-actions"> - <a class="mr-2 oe_slide_js_upload" - role="button" - aria-label="Upload Presentation" - href="#" - t-att-data-channel-id="channel.id" - t-att-data-can-upload="channel.can_upload" - t-att-data-can-publish="channel.can_publish">Add Content</a> - <a class="oe_slide_js_add_section" t-attf-channel_id="#{channel.id}" href="#">Add Section</a> - </div> - </div> - </div> - <!-- Channel content, aka slides (lessons in documentation mode) --> - <div t-if="channel.channel_type == 'documentation'" class="col-8 mt24 ml32 o_wslides_channel_content_promoted"> - <div class="row mt16 align-items-center"> - <div class="col"> - <h3><i class="fa fa-plus-square"/> Featured lesson</h3> - <hr/> - </div> - </div> - <div class="row" t-if="slide_promoted"> - <div class="col-4"> - <img class="img img-fluid" style="" - t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide_promoted.id" - t-att-alt="slide_promoted.name"/> - </div> - <div class="col-8"> - <div class="row"> - <h3 class="col-6" t-att-title="slide_promoted.name"> - <t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"> - <a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/> - </t> - <t t-else=""> - <span class="font-weight-bold text-muted" t-field="slide_promoted.name"/> - </t> - </h3> - <div class="col-12" t-field="slide_promoted.description"/> - </div> - </div> - </div> - </div> - <!-- Channel content, aka slides (lessons in documentation mode) --> - <div t-if="channel.channel_type == 'documentation'" class="col-12"> - <div class="tab-content mt16" id="slideChannelContent"> - <div class="tab-pane active" role="tabpanel" aria-labelledby="profile-tab" id="slideChannelContentAbout"> - <t t-foreach="category_data" t-as="category"> - <div class="row mt16 align-items-center"> - <div class="col"> - <a t-att-href="'/slides/%s/category/%s' % (slug(channel), category['id'])" class="float-right"> - View all - </a> - <h3><i class="fa fa-plus-square"/> <t t-esc="category['name']"/></h3> - <hr/> - </div> - </div> - <t t-call="website_slides.slides_grid_view"> - <t t-set="slides" t-value="category['slides']"/> - </t> - </t> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </t> -</template> - -<!-- Embedded template: display a list of slides in a grid view --> -<template id='slides_grid_view' name="Slides (Grid View)"> - <div class="row o_wslides_row mt8"> - <div class="col-lg-3 col-md-6 col-12" t-foreach="slides" t-as="slide"> - <div class="card o_wslides_lesson_card"> - <img class="card-img-top img-fuild" style="max-height: 160px;" - t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide.id" - t-att-alt="slide.name"/> - <div class="card-body"> - <h5 class="card-title" t-att-title="slide.name"> - <t t-if="slide.is_preview or channel.is_member or is_slides_publisher"> - <a t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/> - </t> - <t t-else=""> - <span class="text-muted" t-esc="slide.name"/> - </t> - </h5> - <h6 class="card-subtitle mb-2 text-muted"> - <t t-if="user.has_group('website.group_website_publisher')"> - <i class="fa fa-globe float-right o_wslides_slide_manage_publish"></i> - <i class="fa fa-pencil float-right o_wslides_slide_manage_edit"></i> - <i class="fa fa-trash float-right o_wslides_slide_manage_unlink"></i> - </t> - <t t-if="slide.is_preview"> - <span class="badge badge-info">Free preview</span> - </t> - <t t-if="not slide.is_published and user.has_group('website.group_website_publisher')"> - <span class="badge badge-danger">Unpublished</span> - </t> - </h6> - <div class="card-text"> - <div class="text-muted oe_no_empty" t-field="slide.description"/> - <div t-if="slide.tag_ids"> - <t t-foreach="slide.tag_ids" t-as="tag"> - <a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-secondary"> - <t t-esc="tag.name"/> - </a> - </t> - </div> - </div> - </div> - <div class="card-footer"> - <!-- CHECKME: like only if logged --> - <div class="float-right text-right o_wslides_like"> - <span class="o_wslides_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id"> - <i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i> - <span t-esc="slide.likes"/> - </span> - <span class="o_wslides_like_down" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id"> - <i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i> - <span t-esc="slide.dislikes"/> - </span> - </div> - <small class="text-muted"> - <t t-call="website_slides.slides_misc_float_time"> - <t t-set="value" t-value="slide.completion_time"/> - </t> -<!-- <t t-esc="slide.total_views"/> Views - <timeago class="timeago" t-att-datetime="slide.create_date"></timeago> --> - </small> - </div> - </div> - </div> - </div> -</template> +<odoo><data> <!-- Slide: main template: detailed view --> -<template id="slide_edit_options" inherit_id="website.user_navbar" name="Edit Slide Options"> - <xpath expr="//li[@id='edit-page-menu']" position="after"> - <t t-if="main_object._name == 'slide.slide'" t-set="action" t-value="'website_slides.action_slides_slides'" /> - </xpath> -</template> <template id="slide_detail_view" name="Slide Detailed View"> <t t-call="website.layout"> <t t-set="main_object" t-value="slide"/> @@ -542,8 +95,8 @@ <div class="row"> <div style="width:100%;"> <div class="o_w_slides_slide_detail_container" style="width: 100%;"> - <t t-if="slide.datas and slide.slide_type == 'infographic'"> - <img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width:100%" alt="Slide image"/> + <t t-if="slide.slide_type == 'infographic'"> + <img t-attf-src="/web/image/slide.slide/#{slide.id}/image" class="img-fluid" style="width:100%" alt="Slide image"/> </t> <div style="height: 600px;" t-if="slide.slide_type in ('presentation', 'document')" class="embed-responsive embed-responsive-4by3 embed-responsive-item mb8"> <t t-raw="slide.embed_code"/> @@ -1004,19 +557,6 @@ </li> </template> - -<!-- Slide sub-template: share: social media --> -<template id='slide_social_media' name="Share on Social Networks"> - <h4 class="mt0">Share on Social Networks</h4> - <t t-call="website_slides.slides_share"> - <t t-set="record" t-value="slide"/> - </t> - <h4 class="mt-3">Share Link</h4> - <input type="text" class="form-control" t-att-value="slide.website_url" readonly="readonly" onClick="this.select();"/> - <span class="form-text">Use permanent link to share in social media</span> -</template> - - <!-- Slide sub-template: share: send by email --> <template id='slide_social_email' name="Share by Email"> <h4 class="mt-3">Share with a friend</h4> @@ -1057,15 +597,4 @@ </div> </template> -<!-- User Navbar --> -<template id="user_navbar_inherit_website_slides" inherit_id="website.user_navbar"> - <xpath expr="//div[@id='o_new_content_menu_choices']//div[@name='module_website_slides']" position="attributes"> - <attribute name="name"/> - <attribute name="t-att-data-module-id"/> - <attribute name="t-att-data-module-shortdesc"/> - <attribute name="groups">website.group_website_designer</attribute> - </xpath> -</template> - -</data> -</odoo> +</data></odoo> diff --git a/addons/website_slides/views/website_slides_embed_templates.xml b/addons/website_slides/views/website_slides_templates_lesson_embed.xml index 059d2898a7e..ac8ecbde8ec 100644 --- a/addons/website_slides/views/website_slides_embed_templates.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_embed.xml @@ -80,8 +80,8 @@ </div> <canvas id="PDFViewerCanvas" class="img-fluid" style="margin: auto; height: 100%; display: none;"></canvas> </t> - <t t-if="slide.slide_type in ('infographic')"> - <img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width: 100%" alt="Slide image"/> + <t t-if="slide.slide_type == 'infographic'"> + <img t-attf-src="/web/image/slide.slide/#{slide.id}/image" class="img-fluid" style="width: 100%" alt="Slide image"/> </t> </div> <!-- Fixed bottom navbar --> diff --git a/addons/website_slides/views/website_slides_templates_utils.xml b/addons/website_slides/views/website_slides_templates_utils.xml new file mode 100644 index 00000000000..57d5c2fced5 --- /dev/null +++ b/addons/website_slides/views/website_slides_templates_utils.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" ?> +<odoo><data> + +<!-- Share on social networks --> +<template id='slides_share' name="Slides Media Share"> + <div> + <a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="o_slides_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-2x"/></a> + <a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="o_slides_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-2x"/></a> + <a t-attf-href="http://www.linkedin.com/shareArticle?mini=true&url=#{record.website_url}&title=#{record.name}&" social-key="linkedin" class="o_slides_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-2x"/></a> + </div> +</template> + +<!-- Share: social media --> +<template id='slide_social_media' name="Share on Social Networks"> + <h4 class="mt0">Share on Social Networks</h4> + <t t-call="website_slides.slides_share"> + <t t-set="record" t-value="slide"/> + </t> + <h4 class="mt-3">Share Link</h4> + <input type="text" class="form-control" t-att-value="slide.website_url" readonly="readonly" onClick="this.select();"/> + <span class="form-text">Use permanent link to share in social media</span> +</template> + +<!-- Website edit page --> +<template id="slide_edit_options" inherit_id="website.user_navbar" name="Edit Slide Options"> + <xpath expr="//li[@id='edit-page-menu']" position="after"> + <t t-if="main_object._name == 'slide.slide'" t-set="action" t-value="'website_slides.action_slides_slides'" /> + </xpath> +</template> + +<!-- User Navbar --> +<template id="user_navbar_inherit_website_slides" inherit_id="website.user_navbar"> + <xpath expr="//div[@id='o_new_content_menu_choices']//div[@name='module_website_slides']" position="attributes"> + <attribute name="name"/> + <attribute name="t-att-data-module-id"/> + <attribute name="t-att-data-module-shortdesc"/> + <attribute name="groups">website.group_website_designer</attribute> + </xpath> +</template> + +</data></odoo> diff --git a/addons/website_slides/wizard/__init__.py b/addons/website_slides/wizard/__init__.py new file mode 100644 index 00000000000..4020988908b --- /dev/null +++ b/addons/website_slides/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import slide_channel_invite diff --git a/addons/website_slides/models/slide_channel_invite.py b/addons/website_slides/wizard/slide_channel_invite.py index 094e82f57b4..094e82f57b4 100644 --- a/addons/website_slides/models/slide_channel_invite.py +++ b/addons/website_slides/wizard/slide_channel_invite.py diff --git a/addons/website_slides/views/slide_channel_invite_views.xml b/addons/website_slides/wizard/slide_channel_invite_views.xml index cdb9ddd79bf..cdb9ddd79bf 100644 --- a/addons/website_slides/views/slide_channel_invite_views.xml +++ b/addons/website_slides/wizard/slide_channel_invite_views.xml diff --git a/addons/website_slides_forum/__manifest__.py b/addons/website_slides_forum/__manifest__.py index 793665230e1..c367b8306c6 100644 --- a/addons/website_slides_forum/__manifest__.py +++ b/addons/website_slides_forum/__manifest__.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. { - 'name': 'Forum on course', + 'name': 'Forum on Courses', 'category': 'Hidden', 'version': '1.0', 'summary': 'Allows to link forum on a course', diff --git a/addons/website_slides_forum/views/website_slides_templates.xml b/addons/website_slides_forum/views/website_slides_templates.xml index 0acb40bc241..f2f79731725 100644 --- a/addons/website_slides_forum/views/website_slides_templates.xml +++ b/addons/website_slides_forum/views/website_slides_templates.xml @@ -5,7 +5,7 @@ <xpath expr="//li[hasclass('o_wslides_nav_tabs_item_home')]" position="after"> <li class="nav-item" t-if="channel.forum_id"> <a t-att-href="'/forum/%s' % (slug(channel.forum_id))" - t-att-class="'nav-link o_wslides_navlink'" target="new">Forum</a> + t-att-class="'nav-link'" target="new">Forum</a> </li> </xpath> </template> @@ -17,5 +17,4 @@ </xpath> </template> - </data></odoo> diff --git a/addons/website_slides_survey/__manifest__.py b/addons/website_slides_survey/__manifest__.py index ec5407ede7f..93c6b3159dd 100644 --- a/addons/website_slides_survey/__manifest__.py +++ b/addons/website_slides_survey/__manifest__.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { - 'name': "Course certifications", + 'name': "Course Certifications", 'summary': 'Add certification capabilities to your courses', 'description': """This module lets you use the full power of certifications within your courses.""", 'category': 'Hidden', - 'version': '0.1', + 'version': '1.0', 'depends': ['website_slides', 'website_survey'], 'installable': True, 'auto_install': True, diff --git a/addons/website_slides_survey/data/slide_slide_demo.xml b/addons/website_slides_survey/data/slide_slide_demo.xml index 90fdc30d889..588ddba7511 100644 --- a/addons/website_slides_survey/data/slide_slide_demo.xml +++ b/addons/website_slides_survey/data/slide_slide_demo.xml @@ -4,6 +4,7 @@ <field name="name">Furniture Creation Certification</field> <field name="slide_type">certification</field> <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/> + <field name="category_id" ref="website_slides.slide_category_demo_5_0"/> <field name="survey_id" ref="website_slides_survey.furniture_certification"/> <field name="website_published" eval="True"/> <field name="is_preview" eval="False"/> diff --git a/addons/website_slides_survey/models/slide_channel.py b/addons/website_slides_survey/models/slide_channel.py index d4132d33151..75d5d6ad88f 100644 --- a/addons/website_slides_survey/models/slide_channel.py +++ b/addons/website_slides_survey/models/slide_channel.py @@ -7,26 +7,10 @@ from odoo import fields, models class Channel(models.Model): _inherit = 'slide.channel' - nbr_certifications = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True) - - def _compute_slides_statistics_type(self, read_group_res): - result = super(Channel, self)._compute_slides_statistics_type(read_group_res) - for cid in result: - result[cid]['nbr_certifications'] = 0 - for res_group in read_group_res: - cid = res_group['channel_id'][0] - result[cid]['nbr_certifications'] += res_group.get('slide_type', '') == 'certification' and res_group['__count'] or 0 - return result + nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True) class Category(models.Model): _inherit = 'slide.category' - nbr_certifications = fields.Integer("Number of Certifications", compute='_count_presentations', store=True) - - def _extract_count_presentations_type(self, result, record_id): - statistics = super(Category, self)._extract_count_presentations_type(result, record_id) - statistics['nbr_certifications'] = result[record_id].get('certification', 0) - statistics['total_slides'] += statistics['nbr_certifications'] - - return statistics + nbr_certification = fields.Integer("Number of Certifications", compute='_count_presentations', store=True) diff --git a/addons/website_slides_survey/static/src/scss/website_slides.scss b/addons/website_slides_survey/static/src/scss/website_slides.scss index fd1506e345b..3cef87764d8 100644 --- a/addons/website_slides_survey/static/src/scss/website_slides.scss +++ b/addons/website_slides_survey/static/src/scss/website_slides.scss @@ -1,10 +1,7 @@ .o_wslides_fullscreen_certification { - background-color: #f6f6f6; display: flex; justify-content: center; align-items: center; - width: 400px; - height: 300px; position: absolute; top: 50%; left: 50%; diff --git a/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml index f6171db9a13..c56cf727dfc 100644 --- a/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml +++ b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml @@ -3,7 +3,7 @@ <t t-extend="website.slides.fullscreen"> <t t-jquery=".o_wslides_fullscreen_content" t-operation="append"> <div class="o_wslides_fullscreen_certification" t-if="slide.slide_type == 'certification'"> - <a t-att-href="'/slides/slide/' + slide.slug" target="new">Pass Certification</a> + <a class="btn btn-primary" t-att-href="'/slides/slide/' + slide.slug" target="new"><i class="fa fa-trophy"/> Pass Certification</a> </div> </t> </t> diff --git a/addons/website_slides_survey/views/slide_channel_views.xml b/addons/website_slides_survey/views/slide_channel_views.xml index 566bef457fa..e28b67997cf 100644 --- a/addons/website_slides_survey/views/slide_channel_views.xml +++ b/addons/website_slides_survey/views/slide_channel_views.xml @@ -6,7 +6,7 @@ <field name="inherit_id" ref="website_slides.view_slide_channel_form"/> <field name="arch" type="xml"> <xpath expr="//field[@name='total_slides']" position="before"> - <field name="nbr_certifications"/> + <field name="nbr_certification"/> </xpath> </field> </record> diff --git a/addons/website_slides_survey/views/website_slides_templates_homepage.xml b/addons/website_slides_survey/views/website_slides_templates_homepage.xml index 24132371f7f..bc4be6ba9b2 100644 --- a/addons/website_slides_survey/views/website_slides_templates_homepage.xml +++ b/addons/website_slides_survey/views/website_slides_templates_homepage.xml @@ -2,10 +2,8 @@ <odoo> <data> <template id="courses_home_inherit_survey" inherit_id="website_slides.courses_home"> - <xpath expr="//li[hasclass('o_wslides_home_all_slides')]" position="after"> - <li class="nav-item ml16"> - <h4><a class="nav-link" href="/slides/all?slide_type=certification"><i class="fa fa-trophy"/> Certifications</a></h4> - </li> + <xpath expr="//a[hasclass('o_wslides_home_all_slides')]" position="after"> + <a class="nav-link nav-link" href="/slides/all?slide_type=certification"><i class="fa fa-trophy mr-1"/>Certifications</a> </xpath> </template> </data> |
