diff --git a/addons/account/data/mail_template_data.xml b/addons/account/data/mail_template_data.xml index f461a271452..4bb340a4f5c 100644 --- a/addons/account/data/mail_template_data.xml +++ b/addons/account/data/mail_template_data.xml @@ -7,7 +7,7 @@ Invoicing: Invoice email - ${(object.user_id.email and '%s <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} + ${(object.user_id.email and '"%s" <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} ${object.company_id.name} Invoice (Ref ${object.number or 'n/a'}) ${object.partner_id.id} diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index bc809c577f7..94c1fa485c7 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1370,8 +1370,6 @@ class AccountInvoice(models.Model): else: payment_method = self.env.ref('account.account_payment_method_manual_out') journal_payment_methods = pay_journal.outbound_payment_method_ids - if payment_method not in journal_payment_methods: - raise UserError(_('No appropriate payment method enabled on journal %s') % pay_journal.name) communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number if self.origin: diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 8ae4230a7b2..0209ea5c849 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -2,10 +2,12 @@ from datetime import timedelta, datetime import calendar +import time +from dateutil.relativedelta import relativedelta from odoo import fields, models, api, _ from odoo.exceptions import ValidationError, UserError -from odoo.exceptions import UserError +from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT from odoo.tools.float_utils import float_round, float_is_zero @@ -63,6 +65,61 @@ Best Regards,''')) account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.") account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.") + @api.multi + def _check_lock_dates(self, vals): + '''Check the lock dates for the current companies. This can't be done in a api.constrains because we need + to perform some comparison between new/old values. This method forces the lock dates to be irreversible. + + * You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set + after the lock date for users. + * You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set after the + last day of the previous month. + * The new lock date for advisors must be set after the previous lock date. + + :param vals: The values passed to the write method. + ''' + period_lock_date = vals.get('period_lock_date') and\ + time.strptime(vals['period_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + fiscalyear_lock_date = vals.get('fiscalyear_lock_date') and\ + time.strptime(vals['fiscalyear_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + + previous_month = datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT) + relativedelta(months=-1) + days_previous_month = calendar.monthrange(previous_month.year, previous_month.month) + previous_month = previous_month.replace(day=days_previous_month[1]).timetuple() + for company in self: + old_fiscalyear_lock_date = company.fiscalyear_lock_date and\ + time.strptime(company.fiscalyear_lock_date, DEFAULT_SERVER_DATE_FORMAT) + + # The user attempts to remove the lock date for advisors + if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals: + raise ValidationError(_('The lock date for advisors is irreversible and can\'t be removed.')) + + # The user attempts to set a lock date for advisors prior to the previous one + if old_fiscalyear_lock_date and fiscalyear_lock_date and fiscalyear_lock_date < old_fiscalyear_lock_date: + raise ValidationError(_('The new lock date for advisors must be set after the previous lock date.')) + + # In case of no new fiscal year in vals, fallback to the oldest + if not fiscalyear_lock_date: + if old_fiscalyear_lock_date: + fiscalyear_lock_date = old_fiscalyear_lock_date + else: + continue + + # The user attempts to set a lock date for advisors prior to the last day of previous month + if fiscalyear_lock_date > previous_month: + raise ValidationError(_('You cannot lock a period that is not finished yet. Please make sure that the lock date for advisors is not set after the last day of the previous month.')) + + # In case of no new period lock date in vals, fallback to the one defined in the company + if not period_lock_date: + if company.period_lock_date: + period_lock_date = time.strptime(company.period_lock_date, DEFAULT_SERVER_DATE_FORMAT) + else: + continue + + # The user attempts to set a lock date for advisors prior to the lock date for users + if period_lock_date < fiscalyear_lock_date: + raise ValidationError(_('You cannot define stricter conditions on advisors than on users. Please make sure that the lock date on advisor is set before the lock date for users.')) + @api.model def _verify_fiscalyear_last_day(self, company_id, last_day, last_month): company = self.browse(company_id) diff --git a/addons/account/tests/test_account_move_closed_period.py b/addons/account/tests/test_account_move_closed_period.py index 298738a0d6c..5de3e97ac4f 100644 --- a/addons/account/tests/test_account_move_closed_period.py +++ b/addons/account/tests/test_account_move_closed_period.py @@ -1,6 +1,8 @@ from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.osv.orm import except_orm -from datetime import datetime, timedelta +from datetime import datetime +from dateutil.relativedelta import relativedelta +from calendar import monthrange from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.tests import tagged @@ -14,14 +16,16 @@ class TestPeriodState(AccountingTestCase): def setUp(self): super(TestPeriodState, self).setUp() self.user_id = self.env.user - self.day_before_yesterday = datetime.now() - timedelta(2) - self.yesterday = datetime.now() - timedelta(1) - self.yesterday_str = self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) + + last_day_month = datetime.now() - relativedelta(months=1) + last_day_month = last_day_month.replace(day=monthrange(last_day_month.year, last_day_month.month)[1]) + self.last_day_month_str = last_day_month.strftime(DEFAULT_SERVER_DATE_FORMAT) + #make sure there is no unposted entry - draft_entries = self.env['account.move'].search([('date', '<=', self.yesterday_str), ('state', '=', 'draft')]) + draft_entries = self.env['account.move'].search([('date', '<=', self.last_day_month_str), ('state', '=', 'draft')]) if draft_entries: draft_entries.post() - self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday_str}) + self.user_id.company_id.fiscalyear_lock_date = self.last_day_month_str self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0] self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0] @@ -30,7 +34,7 @@ class TestPeriodState(AccountingTestCase): move = self.env['account.move'].create({ 'name': '/', 'journal_id': self.sale_journal_id.id, - 'date': self.day_before_yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'date': self.last_day_month_str, 'line_ids': [(0, 0, { 'name': 'foo', 'debit': 10, diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 4a50c812620..1fce897b602 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -42,6 +42,12 @@ class TestReconciliation(AccountingTestCase): self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id + self.inbound_payment_method = self.env['account.payment.method'].create({ + 'name': 'inbound', + 'code': 'IN', + 'payment_type': 'inbound', + }) + def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): #we create an invoice in given currency invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, @@ -709,6 +715,49 @@ class TestReconciliation(AccountingTestCase): credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile() self.assertAlmostEquals(inv.residual, 111) + def test_revert_payment_and_reconcile(self): + payment = self.env['account.payment'].create({ + 'payment_method_id': self.inbound_payment_method.id, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'journal_id': self.bank_journal_usd.id, + 'payment_date': '2018-06-04', + 'amount': 666, + }) + payment.post() + + self.assertEqual(len(payment.move_line_ids), 2) + + bank_line = payment.move_line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id) + customer_line = payment.move_line_ids - bank_line + + self.assertEqual(len(bank_line), 1) + self.assertEqual(len(customer_line), 1) + self.assertNotEqual(bank_line.id, customer_line.id) + + self.assertEqual(bank_line.move_id.id, customer_line.move_id.id) + move = bank_line.move_id + + # Reversing the payment's move + reversed_move_list = move.reverse_moves('2018-06-04') + self.assertEqual(len(reversed_move_list), 1) + reversed_move = self.env['account.move'].browse(reversed_move_list[0]) + + self.assertEqual(len(reversed_move.line_ids), 2) + + # Testing the reconciliation matching between the move lines and their reversed counterparts + reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id) + reversed_customer_line = reversed_move.line_ids - reversed_bank_line + + self.assertEqual(len(reversed_bank_line), 1) + self.assertEqual(len(reversed_customer_line), 1) + self.assertNotEqual(reversed_bank_line.id, reversed_customer_line.id) + self.assertEqual(reversed_bank_line.move_id.id, reversed_customer_line.move_id.id) + + self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id) + self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id) + def test_partial_reconcile_currencies_02(self): #### # Day 1: Invoice Cust/001 to customer (expressed in USD) diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 712bfe7b1f9..8a925dbdf8c 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -1141,7 +1141,8 @@ + domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]" + attrs="{'readonly': [('parent_state', '=', 'posted')]}"/> diff --git a/addons/account_lock/__init__.py b/addons/account_lock/__init__.py new file mode 100644 index 00000000000..cde864bae21 --- /dev/null +++ b/addons/account_lock/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/addons/account_lock/__manifest__.py b/addons/account_lock/__manifest__.py new file mode 100644 index 00000000000..67e76b49098 --- /dev/null +++ b/addons/account_lock/__manifest__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name' : 'Irreversible Lock Date', + 'version' : '1.0', + 'category': 'Accounting', + 'description': """ + Make the lock date irreversible: + + * You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set before the lock date for users. + * You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set before the last day of the previous month. + * The new lock date for advisors must be set after the previous lock date. + """, + 'depends' : ['account'], + 'data': [], +} diff --git a/addons/account_lock/models/__init__.py b/addons/account_lock/models/__init__.py new file mode 100644 index 00000000000..e759f74fc8d --- /dev/null +++ b/addons/account_lock/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import res_company diff --git a/addons/account_lock/models/res_company.py b/addons/account_lock/models/res_company.py new file mode 100644 index 00000000000..5a01046f36b --- /dev/null +++ b/addons/account_lock/models/res_company.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from odoo import models, api + + +class ResCompany(models.Model): + _inherit = 'res.company' + + @api.multi + def write(self, vals): + # fiscalyear_lock_date can't be set to a prior date + if 'fiscalyear_lock_date' in vals or 'period_lock_date' in vals: + self._check_lock_dates(vals) + return super(ResCompany, self).write(vals) diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index a2e5808f8be..2d602a36b97 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -64,12 +64,14 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { 'position': 'fixed', 'top': '50%', 'transform': 'translateY(-50%)', - 'opacity': 0, + 'z-index': '-1', }, }); + // Avoid to show autocomplete for a non appearing input + this.$barcodeInput.attr('autocomplete', 'off'); } - this.__removeBarcodeField = _.debounce(this._removeBarcodeField, this.inputTimeOut); + this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut); }, handle_buffered_keys: function() { @@ -232,7 +234,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { this.max_time_between_keys_in_ms); } // if the barcode input doesn't receive keydown for a while, remove it. - this.__removeBarcodeField(); + this.__blurBarcodeInput(); } }, @@ -247,21 +249,22 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { var barcodeValue = this.$barcodeInput.val(); if (barcodeValue.match(this.regexp)) { core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]); - this.$barcodeInput.val(''); + this._blurBarcodeInput(); } }, /** - * Remove the temporary input created to store the barcode value. - * If nothing happens, this input will be removed, so the focus will be lost - * and the virtual keyboard on mobile devices will be closed. + * Removes the value and focus from the barcode input. + * If nothing happens, the focus will be lost and + * the virtual keyboard on mobile devices will be closed. * * @private */ - _removeBarcodeField: function () { + _blurBarcodeInput: function () { if (this.$barcodeInput) { - // Reset the value and remove from the DOM. - this.$barcodeInput.val('').remove(); + // Close the virtual keyboard on mobile browsers + // FIXME: actually we can't prevent keyboard from opening + this.$barcodeInput.val('').blur(); } }, diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index c071f23c5b2..c57799109a7 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -162,7 +162,8 @@ class StockPicking(models.Model): if self.carrier_id.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= self.carrier_id.amount: res['exact_price'] = 0.0 self.carrier_price = res['exact_price'] - self.carrier_tracking_ref = res['tracking_number'] + if res['tracking_number']: + self.carrier_tracking_ref = res['tracking_number'] order_currency = self.sale_id.currency_id or self.company_id.currency_id msg = _("Shipment sent to carrier %s for shipping with tracking number %s
Cost: %.2f %s") % (self.carrier_id.name, self.carrier_tracking_ref, self.carrier_price, order_currency.name) self.message_post(body=msg) diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index df0df9e3388..b9392bec08b 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -842,7 +842,7 @@ class GoogleCalendar(models.AbstractModel): try: # if already deleted from gmail or never created recs.delete_an_event(current_event[0]) - except Exception as e: + except requests.exceptions.HTTPError as e: if e.response.status_code in (401, 410,): pass else: diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index 7159ac3b31e..f31b1619894 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -522,6 +522,10 @@ class Escpos: # Print Code if code: self._raw(code) + # We are using type A commands + # So we need to add the 'NULL' character + # https://github.com/python-escpos/python-escpos/pull/98/files#diff-a0b1df12c7c67e38915adbe469051e2dR444 + self._raw('\x00') else: raise exception.BarcodeCodeError() diff --git a/addons/l10n_fr_certification/models/res_company.py b/addons/l10n_fr_certification/models/res_company.py index 97aa993aa7e..f5f962f6fa3 100644 --- a/addons/l10n_fr_certification/models/res_company.py +++ b/addons/l10n_fr_certification/models/res_company.py @@ -30,6 +30,9 @@ class ResCompany(models.Model): if company._is_accounting_unalterable(): sequence_fields = ['l10n_fr_secure_sequence_id'] company._create_secure_sequence(sequence_fields) + # fiscalyear_lock_date can't be set to a prior date + if 'fiscalyear_lock_date' in vals or 'period_lock_date' in vals: + self._check_lock_dates(vals) return res def _create_secure_sequence(self, sequence_fields): diff --git a/addons/l10n_generic_coa/__init__.py b/addons/l10n_generic_coa/__init__.py index 67dee8c60db..d417dc6807c 100644 --- a/addons/l10n_generic_coa/__init__.py +++ b/addons/l10n_generic_coa/__init__.py @@ -1,2 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + + +def uninstall_hook(cr, registry): + cr.execute( + "DELETE FROM ir_model_data WHERE module = 'l10n_generic_coa'" + ) diff --git a/addons/l10n_generic_coa/__manifest__.py b/addons/l10n_generic_coa/__manifest__.py index 314e13f44ff..2f04d8df530 100644 --- a/addons/l10n_generic_coa/__manifest__.py +++ b/addons/l10n_generic_coa/__manifest__.py @@ -24,4 +24,5 @@ Install some generic chart of accounts. 'data/account_invoice_demo.xml', ], 'website': 'https://www.odoo.com/page/accounting', + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 5889bbeede1..537d333cd7b 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -84,7 +84,7 @@ class MailMail(models.Model): def send_get_email_dict(self, partner=None): # TDE: temporary addition (mail was parameter) due to semi-new-API res = super(MailMail, self).send_get_email_dict(partner) - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url').rstrip('/') if self.mailing_id and res.get('body') and res.get('email_to'): emails = tools.email_split(res.get('email_to')[0]) email_to = emails and emails[0] or False diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 1b41565401d..2ef110a6dae 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -364,11 +364,12 @@ class PosConfig(models.Model): @api.multi def write(self, vals): - if (self.is_posbox or vals.get('is_posbox')) and (self.iface_customer_facing_display or vals.get('iface_customer_facing_display')): - facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip() - if not facing_display: - vals['customer_facing_display_html'] = self._compute_default_customer_html() result = super(PosConfig, self).write(vals) + + config_display = self.filtered(lambda c: c.is_posbox and c.iface_customer_facing_display and not (c.customer_facing_display_html or '').strip()) + if config_display: + super(PosConfig, config_display).write({'customer_facing_display_html': self._compute_default_customer_html()}) + self.sudo()._set_fiscal_position() self.sudo()._check_modules_to_install() self.sudo()._check_groups_implied() diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml index d3c075c3ff0..072342afc87 100644 --- a/addons/purchase/data/mail_template_data.xml +++ b/addons/purchase/data/mail_template_data.xml @@ -5,7 +5,7 @@ RFQ - Send by Email - ${(object.create_uid.email and '%s <%s>' % (object.create_uid.name, object.create_uid.email) or '')|safe} + ${(object.create_uid.email and '"%s" <%s>' % (object.create_uid.name, object.create_uid.email) or '')|safe} ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) ${object.partner_id.id} @@ -46,7 +46,7 @@ from ${object.company_id.name}. Purchase Order - Send by Email - ${(object.create_uid.email and '%s <%s>' % (object.create_uid.name, object.create_uid.email) or '')|safe} + ${(object.create_uid.email and '"%s" <%s>' % (object.create_uid.name, object.create_uid.email) or '')|safe} ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) ${object.partner_id.id} diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 35ff379f620..1bdf4b3bf94 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -942,12 +942,12 @@ class ProcurementRule(models.Model): if domain in cache: po = cache[domain] else: - po = self.env['purchase.order'].search([dom for dom in domain]) + po = self.env['purchase.order'].sudo().search([dom for dom in domain]) po = po[0] if po else False cache[domain] = po if not po: vals = self._prepare_purchase_order(product_id, product_qty, product_uom, origin, values, partner) - po = self.env['purchase.order'].create(vals) + po = self.env['purchase.order'].sudo().create(vals) cache[domain] = po elif not po.origin or origin not in po.origin.split(', '): if po.origin: @@ -968,7 +968,7 @@ class ProcurementRule(models.Model): break if not po_line: vals = self._prepare_purchase_order_line(product_id, product_qty, product_uom, values, po, supplier) - self.env['purchase.order.line'].create(vals) + self.env['purchase.order.line'].sudo().create(vals) def _get_purchase_schedule_date(self, values): """Return the datetime value to use as Schedule Date (``date_planned``) for the diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml index b1ee2412b94..57d66f904db 100644 --- a/addons/sale/data/mail_template_data.xml +++ b/addons/sale/data/mail_template_data.xml @@ -5,7 +5,7 @@ Sales Order - Send by Email - ${(object.user_id.email and '%s <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} + ${(object.user_id.email and '"%s" <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} ${object.company_id.name} ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} (Ref ${object.name or 'n/a' }) ${object.partner_id.id} diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index e1ffda478ab..79dffdd719b 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -24,7 +24,7 @@ class ProductTemplate(models.Model): @api.depends('product_variant_ids.sales_count') def _sales_count(self): for product in self: - product.sales_count = sum([p.sales_count for p in product.product_variant_ids]) + product.sales_count = sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]) @api.multi def action_view_sales(self): diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index e59af341990..d88fbca0ceb 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -925,11 +925,15 @@ class StockMove(models.Model): if not move.move_orig_ids: if move.procure_method == 'make_to_order': continue + # If we don't need any quantity, consider the move assigned. + need = move.product_qty - move.reserved_availability + if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding): + assigned_moves |= move + continue # Reserve new quants and create move lines accordingly. available_quantity = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id) if available_quantity <= 0: continue - need = move.product_qty - move.reserved_availability taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, strict=False) if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding): continue diff --git a/addons/stock/report/report_stock_forecast.py b/addons/stock/report/report_stock_forecast.py index 700aa5455de..1ca3b1195b5 100644 --- a/addons/stock/report/report_stock_forecast.py +++ b/addons/stock/report/report_stock_forecast.py @@ -66,7 +66,7 @@ class ReportStockForecat(models.Model): LEFT JOIN stock_location source_location ON sm.location_id = source_location.id WHERE - sm.state IN ('confirmed','assigned','waiting') and + sm.state IN ('confirmed','partially_available','assigned','waiting') and source_location.usage != 'internal' and dest_location.usage = 'internal' GROUP BY sm.date_expected,sm.product_id, sm.company_id UNION ALL @@ -88,7 +88,7 @@ class ReportStockForecat(models.Model): LEFT JOIN stock_location dest_location ON sm.location_dest_id = dest_location.id WHERE - sm.state IN ('confirmed','assigned','waiting') and + sm.state IN ('confirmed','partially_available','assigned','waiting') and source_location.usage = 'internal' and dest_location.usage != 'internal' GROUP BY sm.date_expected,sm.product_id, sm.company_id) as MAIN diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index df57065b8d7..8326328bc95 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -925,6 +925,31 @@ class StockMove(TransactionCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 12.0) self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.customer_location)), 12) + def test_availability_8(self): + """ Test the assignment mechanism when the product quantity is decreased on a partially + reserved stock move. + """ + # make some stock + self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 3.0) + self.assertAlmostEqual(self.product1.qty_available, 3.0) + + move_partial = self.env['stock.move'].create({ + 'name': 'test_partial', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 5.0, + }) + + move_partial._action_confirm() + move_partial._action_assign() + self.assertAlmostEqual(self.product1.virtual_available, -2.0) + self.assertEqual(move_partial.state, 'partially_available') + move_partial.product_uom_qty = 3.0 + move_partial._action_assign() + self.assertEqual(move_partial.state, 'assigned') + def test_unreserve_1(self): """ Check that unreserving a stock move sets the products reserved as available and set the state back to confirmed. @@ -3767,4 +3792,3 @@ class StockMove(TransactionCase): picking.button_validate() self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 2) - diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index 656d6ea7364..77747cdea5c 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -44,7 +44,7 @@ stock.return.picking - + diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 62d862afe35..ead68c28274 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -18,8 +18,9 @@ from odoo import http, models, fields, _ from odoo.http import request from odoo.tools import pycompat, OrderedSet from odoo.addons.http_routing.models.ir_http import slug, _guess_mimetype -from odoo.addons.web.controllers.main import WebClient, Binary, Home +from odoo.addons.web.controllers.main import WebClient, Binary from odoo.addons.portal.controllers.portal import pager as portal_pager +from odoo.addons.portal.controllers.web import Home logger = logging.getLogger(__name__) diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js index 3aa8691fca7..ebe38118559 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -20,8 +20,11 @@ return instance.appendTo($form).then(function () { odoo.define('website_event.website_event', function (require) { var ajax = require('web.ajax'); +var core = require('web.core'); var Widget = require('web.Widget'); +var _t = core._t; + // Catch registration form event, because of JS for attendee details var EventRegistrationForm = Widget.extend({ start: function () { @@ -31,7 +34,6 @@ var EventRegistrationForm = Widget.extend({ .off('click') .removeClass('a-submit') .click(function (ev) { - $(this).attr('disabled', true); self.on_click(ev); }); }); @@ -43,15 +45,18 @@ var EventRegistrationForm = Widget.extend({ var $form = $(ev.currentTarget).closest('form'); var $button = $(ev.currentTarget).closest('[type="submit"]'); var post = {}; + $('#registration_form table').siblings('.alert').remove(); $('#registration_form select').each(function () { post[$(this).attr('name')] = $(this).val(); }); var tickets_ordered = _.some(_.map(post, function (value, key) { return parseInt(value); })); if (!tickets_ordered) { - return $('#registration_form table').after( - '
Please select at least one ticket.
' - ); + $('
') + .text(_t('Please select at least one ticket.')) + .insertAfter('#registration_form table'); + return $.Deferred(); } else { + $button.attr('disabled', true); return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) { var $modal = $(modal); $modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19 diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 385234ad9ea..03bb0252b46 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -420,7 +420,7 @@