diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index c086c4e830..44a2197c07 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -989,6 +989,7 @@ class AccountMove(models.Model): domain = [ ('company_id', '=', self.company_id.id), ('internal_type', '=', 'receivable' if self.move_type in ('out_invoice', 'out_refund', 'out_receipt') else 'payable'), + ('deprecated', '=', False), ] return self.env['account.account'].search(domain, limit=1) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 1ed8b0f30c..1bb0a1277d 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -472,9 +472,11 @@ class AccountPayment(models.Model): self.reconciled_statements_count = 0 return - self.env['account.move'].flush() - self.env['account.move.line'].flush() - self.env['account.partial.reconcile'].flush() + self.env['account.journal'].flush(fnames=['payment_debit_account_id', 'payment_credit_account_id']) + self.env['account.payment'].flush(fnames=['move_id']) + self.env['account.move'].flush(fnames=['move_type', 'payment_id', 'statement_line_id', 'journal_id']) + self.env['account.move.line'].flush(fnames=['move_id', 'account_id', 'statement_line_id']) + self.env['account.partial.reconcile'].flush(fnames=['debit_move_id', 'credit_move_id']) self._cr.execute(''' SELECT diff --git a/addons/hr_expense/models/account_move.py b/addons/hr_expense/models/account_move.py index 271a3024af..684788cd76 100644 --- a/addons/hr_expense/models/account_move.py +++ b/addons/hr_expense/models/account_move.py @@ -12,3 +12,9 @@ class AccountMove(models.Model): if l.expense_id: l.expense_id.refuse_expense(reason=_("Payment Cancelled")) return super().button_cancel() + + def button_draft(self): + for line in self.line_ids: + if line.expense_id: + line.expense_id.sheet_id.write({'state': 'post'}) + return super().button_draft() diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 035659c079..1b438b68a0 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -278,6 +278,8 @@ class HrExpense(models.Model): return super(HrExpense, self).unlink() def write(self, vals): + if 'sheet_id' in vals: + self.env['hr.expense.sheet'].browse(vals['sheet_id']).check_access_rule('write') if 'tax_ids' in vals or 'analytic_account_id' in vals or 'account_id' in vals: if any(not expense.is_editable for expense in self): raise UserError(_('You are not authorized to edit this expense report.')) diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py index 431e2edc65..6893fbbb73 100644 --- a/addons/hr_expense/tests/test_expenses.py +++ b/addons/hr_expense/tests/test_expenses.py @@ -350,3 +350,63 @@ class TestExpenses(TestExpenseCommon): 'amount_paid': formatLang(self.env, 11.0, currency_obj=self.env.company.currency_id), 'currency': self.env.company.currency_id }) + + def test_reset_move_to_draft(self): + """ + Test the state of an expense and its report + after resetting the paid move to draft + """ + # Create expense and report + expense = self.env['hr.expense'].create({ + 'name': 'expense_1', + 'employee_id': self.expense_employee.id, + 'product_id': self.product_a.id, + 'unit_amount': 1000.00, + }) + expense.action_submit_expenses() + expense_sheet = expense.sheet_id + + self.assertEqual(expense.state, 'draft', 'Expense state must be draft before sheet submission') + self.assertEqual(expense_sheet.state, 'draft', 'Sheet state must be draft before submission') + + # Submit report + expense_sheet.action_submit_sheet() + + self.assertEqual(expense.state, 'reported', 'Expense state must be reported after sheet submission') + self.assertEqual(expense_sheet.state, 'submit', 'Sheet state must be submit after submission') + + # Approve report + expense_sheet.approve_expense_sheets() + + self.assertEqual(expense.state, 'approved', 'Expense state must be draft after sheet approval') + self.assertEqual(expense_sheet.state, 'approve', 'Sheet state must be draft after approval') + + # Create move + expense_sheet.action_sheet_move_create() + + self.assertEqual(expense.state, 'approved', 'Expense state must be draft after posting move') + self.assertEqual(expense_sheet.state, 'post', 'Sheet state must be draft after posting move') + + # Pay move + move = expense_sheet.account_move_id + self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ + 'amount': 1000.0, + })._create_payments() + + self.assertEqual(expense.state, 'done', 'Expense state must be done after payment') + self.assertEqual(expense_sheet.state, 'done', 'Sheet state must be done after payment') + + # Reset move to draft + move.button_draft() + + self.assertEqual(expense.state, 'approved', 'Expense state must be approved after resetting move to draft') + self.assertEqual(expense_sheet.state, 'post', 'Sheet state must be done after resetting move to draft') + + # Post and pay move again + move.action_post() + self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ + 'amount': 1000.0, + })._create_payments() + + self.assertEqual(expense.state, 'done', 'Expense state must be done after payment') + self.assertEqual(expense_sheet.state, 'done', 'Sheet state must be done after payment') diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index ae4ed3d706..fe447e5e13 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -10,12 +10,14 @@ import psycopg2 from flectra import api, exceptions, fields, models, _, SUPERUSER_ID from flectra.tools import consteq, float_round, image_process, ustr -from flectra.exceptions import ValidationError +from flectra.exceptions import UserError, ValidationError from flectra.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT from flectra.tools.misc import formatLang from flectra.http import request from flectra.osv import expression +from flectra.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG + _logger = logging.getLogger(__name__) @@ -336,6 +338,19 @@ class PaymentAcquirer(models.Model): self._check_required_if_provider() return result + def unlink(self): + """ Prevent the deletion of the payment acquirer if it has an xmlid. """ + external_ids = self.get_external_id() + for acquirer in self: + external_id = external_ids[acquirer.id] + if external_id \ + and not external_id.startswith('__export__') \ + and not self._context.get(MODULE_UNINSTALL_FLAG): + raise UserError( + _("You cannot delete the payment acquirer %s; archive it instead.", acquirer.name) + ) + return super().unlink() + def get_acquirer_extra_fees(self, amount, currency_id, country_id): extra_fees = { 'currency_id': currency_id diff --git a/addons/stock/report/report_stock_quantity.py b/addons/stock/report/report_stock_quantity.py index afd9659a27..8833eefb1d 100644 --- a/addons/stock/report/report_stock_quantity.py +++ b/addons/stock/report/report_stock_quantity.py @@ -18,7 +18,6 @@ class ReportStockQuantity(models.Model): ('out', 'Forecasted Deliveries'), ], string='State', readonly=True) product_qty = fields.Float(string='Quantity', readonly=True) - move_ids = fields.One2many('stock.move', readonly=True) company_id = fields.Many2one('res.company', readonly=True) warehouse_id = fields.Many2one('stock.warehouse', readonly=True) diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss index 7518fe24ba..892d91cfc5 100644 --- a/addons/web/static/src/scss/modal.scss +++ b/addons/web/static/src/scss/modal.scss @@ -18,6 +18,11 @@ padding: 0; } + // Fix dropzones of nested sortable. + .mjs-nestedSortable-error { + outline: none; + } + .o_modal_header { @include o-webclient-padding($top: 10px, $bottom: 10px); @include clearfix; diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 94adbd8398..fb5b956500 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -236,12 +236,26 @@ class Website(Home): return request.make_response(content, [('Content-Type', mimetype)]) - @http.route('/website/info', type='http', auth="public", website=True, sitemap=True) + def sitemap_website_info(env, rule, qs): + website = env['website'].get_current_website() + if not ( + website.viewref('website.website_info', False).active + and website.viewref('website.show_website_info', False).active + ): + # avoid 404 or blank page in sitemap + return False + + if not qs or qs.lower() in '/website/info': + yield {'loc': '/website/info'} + + @http.route('/website/info', type='http', auth="public", website=True, sitemap=sitemap_website_info) def website_info(self, **kwargs): - try: - request.website.get_template('website.website_info').name - except Exception as e: - return request.env['ir.http']._handle_exception(e) + if not request.website.viewref('website.website_info', False).active: + # Deleted or archived view (through manual operation in backend). + # Don't check `show_website_info` view: still need to access if + # disabled to be able to enable it through the customize show. + raise request.not_found() + Module = request.env['ir.module.module'].sudo() apps = Module.search([('state', '=', 'installed'), ('application', '=', True)]) l10n = Module.search([('state', '=', 'installed'), ('name', '=like', 'l10n_%')]) diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index f665b3d5bf..6598107030 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -8,6 +8,7 @@ import mimetypes from werkzeug.utils import redirect from flectra import http +from flectra.exceptions import AccessError from flectra.http import request from flectra.addons.sale.controllers.portal import CustomerPortal from flectra.addons.website_sale.controllers.main import WebsiteSale @@ -81,25 +82,25 @@ class WebsiteSaleDigital(CustomerPortal): else: return redirect(self.orders_page) - # Check if the user has bought the associated product - res_model = attachment['res_model'] - res_id = attachment['res_id'] - purchased_products = request.env['account.move.line'].get_digital_purchases() + try: + request.env['ir.attachment'].browse(attachment_id).check('read') + except AccessError: # The user does not have read access on the attachment. + # Check if access can be granted through their purchases. + res_model = attachment['res_model'] + res_id = attachment['res_id'] + digital_purchases = request.env['account.move.line'].get_digital_purchases() + if res_model == 'product.product': + purchased_product_ids = digital_purchases + elif res_model == 'product.template': + purchased_product_ids = request.env['product.product'].sudo().browse( + digital_purchases + ).mapped('product_tmpl_id').ids + else: + purchased_product_ids = [] # The purchases must be related to products. + if res_id not in purchased_product_ids: # No related purchase was found. + return redirect(self.orders_page) # Prevent the user from downloading. - if res_model == 'product.product': - if res_id not in purchased_products: - return redirect(self.orders_page) - - # Also check for attachments in the product templates - elif res_model == 'product.template': - template_ids = request.env['product.product'].sudo().browse(purchased_products).mapped('product_tmpl_id').ids - if res_id not in template_ids: - return redirect(self.orders_page) - - else: - return redirect(self.orders_page) - - # The client has bought the product, otherwise it would have been blocked by now + # The user has bought the product, or has the rights to the attachment if attachment["type"] == "url": if attachment["url"]: return redirect(attachment["url"]) diff --git a/addons/website_slides/controllers/mail.py b/addons/website_slides/controllers/mail.py index d79df6da2d..a4f1120d3a 100644 --- a/addons/website_slides/controllers/mail.py +++ b/addons/website_slides/controllers/mail.py @@ -46,10 +46,11 @@ class SlidesPortalChatter(PortalChatter): # fetch and update mail.message message_id = int(message_id) message_body = plaintext2html(message) + subtype_comment_id = request.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') domain = [ ('model', '=', res_model), ('res_id', '=', res_id), - ('is_internal', '=', False), + ('subtype_id', '=', subtype_comment_id), ('author_id', '=', request.env.user.partner_id.id), ('message_type', '=', 'comment'), ('id', '=', message_id) @@ -64,7 +65,7 @@ class SlidesPortalChatter(PortalChatter): # update rating if post.get('rating_value'): - domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('message_id', '=', message.id)] + domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('message_id', '=', message.id)] rating = request.env['rating.rating'].sudo().search(domain, order='write_date DESC', limit=1) rating.write({ 'rating': float(post['rating_value']), diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 73637a37ff..14b6c3621a 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -484,12 +484,13 @@ class WebsiteSlides(WebsiteProfile): 'enable_slide_upload': 'enable_slide_upload' in kw, } if not request.env.user._is_public(): + subtype_comment_id = request.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') last_message = request.env['mail.message'].search([ ('model', '=', channel._name), ('res_id', '=', channel.id), ('author_id', '=', request.env.user.partner_id.id), ('message_type', '=', 'comment'), - ('is_internal', '=', False) + ('subtype_id', '=', subtype_comment_id) ], order='write_date DESC', limit=1) if last_message: last_message_values = last_message.read(['body', 'rating_value', 'attachment_ids'])[0] diff --git a/addons/website_slides/static/src/tests/tours/slides_course_reviews.js b/addons/website_slides/static/src/tests/tours/slides_course_reviews.js new file mode 100644 index 0000000000..8949149bd0 --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_course_reviews.js @@ -0,0 +1,46 @@ +flectra.define('website_slides.tour.slide.course.reviews', function (require) { +'use strict'; + +const tour = require('web_tour.tour'); + +/** + * This tour test that a log note isn't considered + * as a course review. And also that a member can + * add only one review. + */ +tour.register('course_reviews', { + url: '/slides', + test: true +}, [ +{ + trigger: 'a:contains("Basics of Gardening - Test")', +}, { + trigger: 'a[id="review-tab"]', +}, { + trigger: '.o_portal_chatter_message:contains("Log note")', + run: function() {}, +}, { + trigger: 'button:contains("Add a review")', + // If it fails here, it means the log note is considered as a review +}, { + trigger: 'form.o_portal_chatter_composer_form textarea', + extra_trigger: 'div#ratingpopupcomposer.modal_shown', + run: 'text Great course!', + in_modal: false, +}, { + trigger: 'button.o_portal_chatter_composer_btn', + in_modal: false, +}, { + trigger: 'a[id="review-tab"]', +}, { + trigger: 'button:contains("Visible")', +}, { + trigger: 'button:contains("Modify your review")', + // If it fails here, it means the system is allowing you to add another review. +}, { + trigger: 'form.o_portal_chatter_composer_form textarea:contains("Great course!")', + run: function() {}, +} +]); + +}); diff --git a/addons/website_slides/tests/test_ui_wslides.py b/addons/website_slides/tests/test_ui_wslides.py index 909dc5da19..8bb229ec5f 100644 --- a/addons/website_slides/tests/test_ui_wslides.py +++ b/addons/website_slides/tests/test_ui_wslides.py @@ -19,7 +19,7 @@ class TestUICommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal): img_path = get_module_resource('website_slides', 'static', 'src', 'img', 'slide_demo_gardening_1.jpg') img_content = base64.b64encode(open(img_path, "rb").read()) - self.env['slide.channel'].create({ + self.channel = self.env['slide.channel'].create({ 'name': 'Basics of Gardening - Test', 'user_id': self.env.ref('base.user_admin').id, 'enroll': 'public', @@ -148,6 +148,24 @@ class TestUi(TestUICommon): 'flectra.__DEBUG__.services["web_tour.tour"].tours.full_screen_web_editor.ready', login=user_demo.login) + def test_course_reviews_elearning_officer(self): + user_demo = self.user_demo + user_demo.write({ + 'groups_id': [(6, 0, (self.env.ref('base.group_user') | self.env.ref( + 'website_slides.group_website_slides_officer')).ids)] + }) + + # The user must be a course member before being able to post a log note. + self.channel._action_add_members(user_demo.partner_id) + self.channel.with_user(user_demo).message_post( + body='Log note', subtype_xmlid='mail.mt_note', message_type='comment') + + self.browser_js( + '/slides', + 'flectra.__DEBUG__.services["web_tour.tour"].run("course_reviews")', + 'flectra.__DEBUG__.services["web_tour.tour"].tours.course_reviews.ready', + login=user_demo.login) + @tests.common.tagged('external', 'post_install', '-standard', '-at_install') class TestUiYoutube(HttpCaseWithUserDemo): diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml index fd5d530287..045f706eb9 100644 --- a/addons/website_slides/views/assets.xml +++ b/addons/website_slides/views/assets.xml @@ -44,6 +44,7 @@