diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index d2555241023..05035015de2 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -70,7 +70,7 @@ class AccountInvoice(models.Model): @api.onchange('amount_total') def _onchange_amount_total(self): for inv in self: - if inv.amount_total < 0: + if float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1: raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.')) @api.model @@ -891,7 +891,7 @@ class AccountInvoice(models.Model): raise UserError(_("The field Vendor is required, please complete it to validate the Vendor Bill.")) if to_open_invoices.filtered(lambda inv: inv.state != 'draft'): raise UserError(_("Invoice must be in draft state in order to validate it.")) - if to_open_invoices.filtered(lambda inv: inv.amount_total < 0): + if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1): raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead.")) if to_open_invoices.filtered(lambda inv: not inv.account_id): raise UserError(_('No account was found to create the invoice, be sure you have installed a chart of account.')) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 15a80780709..2c2b80d3385 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -866,7 +866,7 @@ var StatementModel = BasicModel.extend({ }) : false, account_code: self.accounts[line.st_line.open_balance_account_id], }; - line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1; + line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1; }); }, /** diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 9b9df5dab7d..02bc1e110f2 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -133,7 +133,7 @@ - Create Write-offOpen balanceChoose counterpart + Open balanceChoose counterpart or Create Write-off diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 0ef3ec63b6e..7ebffc19e94 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -1047,8 +1047,8 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines"); clientAction.destroy(); @@ -1154,8 +1154,8 @@ QUnit.module('account', { $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); clientAction.destroy(); @@ -1179,7 +1179,7 @@ QUnit.module('account', { assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), - "Create Write-off229.12", "should display the 'Create Write-off' line with value in left column"); + "Open balance229.12", "should display the 'Open balance' line with value in left column"); widget.$('.create .create_amount input').val('100').trigger('input'); @@ -1187,7 +1187,7 @@ QUnit.module('account', { " 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ", "should update the value of the 4 lines (because the line must have 100% of the value)"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), - "Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax"); + "Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax"); widget.$('.accounting_view tbody .cell_account_code:first').trigger('click'); widget.$('.accounting_view tbody .cell_label:first').trigger('click'); diff --git a/addons/barcodes/doc/index.rst b/addons/barcodes/README.rst similarity index 89% rename from addons/barcodes/doc/index.rst rename to addons/barcodes/README.rst index 6c93b1f0283..696ab62fbc5 100644 --- a/addons/barcodes/doc/index.rst +++ b/addons/barcodes/README.rst @@ -11,10 +11,10 @@ Barcodes encoding The Barcodes module defines barcode nomenclatures whose rules identify specific type of items e.g. products, locations. It contains the following features: -- Barcode patterns to identify barcodes containing a numerical value (e.g. weight, price) +- Patterns to identify barcodes containing a numerical value (e.g. weight, price) - Definitin of barcode aliases that allow to identify the same product with different barcodes - Unlimited barcode patterns and definitions, -- Barcode EAN13 encoding supported. +- Support for encodings EAN-13, EAN-8 and UPC-A Barcode encodings ----------------- @@ -65,7 +65,7 @@ We now want to write a barcode for 2.75kg of oranges. This barcode should be '21 numerical content of this barcode is '02750', and the correct checksum is '6'). When scanned, this barcode matches the Weighted Product rule (since is starts with '21'). The numerical content is extracted, and replaced by a sequence of '0's. The correct checksum is then computed for the obtained barcode -('2100001000004') and the corresponding product (oranges) qgit is retrieved from product table. +('2100001000004') and the corresponding product (oranges) is retrieved from product table. Note: the special characters '{' and '}' in patterns are used to identify numerical content. To explicitely specify '{' or '}' in a pattern, they must be escaped. @@ -86,6 +86,17 @@ matching the original, 12-digit long, barcode. Barcodes scanning ============================== +Scanning +-------- + +Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo. +The scanner must be configured to use no prefix and a carriage return or tab as suffix. +The delay between each character input must be less than or equal to 50 milliseconds. +Most barcode scanners will work out of the box. +However, make sure the scanner uses the same keyboard layout as the device it's plugged in. +Either by setting the device's keyboard layout to US QWERTY (default value for most readers) +or by changing the scanner's keyboard layout (check the manual). + Barcode events ------------------------------ diff --git a/addons/barcodes/__manifest__.py b/addons/barcodes/__manifest__.py index bb7b3cf4276..579e28e4ee2 100644 --- a/addons/barcodes/__manifest__.py +++ b/addons/barcodes/__manifest__.py @@ -3,27 +3,6 @@ 'version': '2.0', 'category': 'Extra Tools', 'summary': 'Barcodes Scanning and Parsing', - 'description': """ -This module adds support for barcode scanning and parsing. - -Scanning --------- -Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo. -The scanner must be configured to use no prefix and a carriage return or tab as suffix. -The delay between each character input must be less than or equal to 50 milliseconds. -Most barcode scanners will work out of the box. -However, make sure the scanner uses the same keyboard layout as the device it's plugged in. -Either by setting the device's keyboard layout to US QWERTY (default value for most readers) -or by changing the scanner's keyboard layout (check the manual). - -Parsing -------- -The barcodes are interpreted using the rules defined by a nomenclature. -It provides the following features: -- Patterns to identify barcodes containing a numerical value (e.g. weight, price) -- Definition of barcode aliases that allow to identify the same product with different barcodes -- Support for encodings EAN-13, EAN-8 and UPC-A -""", 'depends': ['web'], 'data': [ 'data/barcodes_data.xml', diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 61b66b5965d..47b724aac98 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -127,9 +127,10 @@ class MailActivity(models.Model): for record in self.filtered(lambda activity: activity.date_deadline): today = today_default - if record.user_id.tz: + tz = record.user_id.sudo().tz + if tz: today_utc = pytz.UTC.localize(datetime.utcnow()) - today_tz = today_utc.astimezone(pytz.timezone(record.user_id.tz)) + today_tz = today_utc.astimezone(pytz.timezone(tz)) today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) date_deadline = fields.Date.from_string(record.date_deadline) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 772ecf59a8d..17d6a6f1614 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -8,7 +8,7 @@ import random import threading from ast import literal_eval -from odoo import api, fields, models, tools, _ +from odoo import api, fields, models, tools, _, SUPERUSER_ID from odoo.exceptions import UserError from odoo.tools.safe_eval import safe_eval @@ -218,7 +218,8 @@ class MassMailingCampaign(models.Model): _inherits = {'utm.campaign': 'campaign_id'} stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True, - default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1)) + default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1), + group_expand='_group_expand_stage_ids') user_id = fields.Many2one( 'res.users', string='Responsible', required=True, default=lambda self: self.env.uid) @@ -322,31 +323,12 @@ class MassMailingCampaign(models.Model): return res @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - """ Override read_group to always display all states. """ - if groupby and groupby[0] == "stage_id": - # Default result structure - states_read = self.env['mail.mass_mailing.stage'].search_read([], ['name']) - states = [(state['id'], state['name']) for state in states_read] - read_group_all_states = [{ - '__context': {'group_by': groupby[1:]}, - '__domain': domain + [('stage_id', '=', state_value)], - 'stage_id': state_value, - 'state_count': 0, - } for state_value, state_name in states] - # Get standard results - read_group_res = super(MassMailingCampaign, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby) - # Update standard results with default results - result = [] - for state_value, state_name in states: - res = [x for x in read_group_res if x['stage_id'] == (state_value, state_name)] - if not res: - res = [x for x in read_group_all_states if x['stage_id'] == state_value] - res[0]['stage_id'] = [state_value, state_name] - result.append(res[0]) - return result - else: - return super(MassMailingCampaign, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby) + def _group_expand_stage_ids(self, stages, domain, order): + """ Read group customization in order to display all the stages in the + kanban view, even if they are empty + """ + stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) + return stages.browse(stage_ids) class MassMailing(models.Model): @@ -399,7 +381,7 @@ class MassMailing(models.Model): help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email')) clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks") state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')], - string='Status', required=True, copy=False, default='draft') + string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states') color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string='Mailing Manager', default=lambda self: self.env.user) # mailing options @@ -424,6 +406,7 @@ class MassMailing(models.Model): statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics') total = fields.Integer(compute="_compute_total") scheduled = fields.Integer(compute="_compute_statistics") + expected = fields.Integer(compute="_compute_statistics") sent = fields.Integer(compute="_compute_statistics") delivered = fields.Integer(compute="_compute_statistics") opened = fields.Integer(compute="_compute_statistics") @@ -465,7 +448,7 @@ class MassMailing(models.Model): self.env.cr.execute(""" SELECT m.id as mailing_id, - COUNT(s.id) AS total, + COUNT(s.id) AS expected, COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, @@ -486,7 +469,7 @@ class MassMailing(models.Model): m.id """, (tuple(self.ids), )) for row in self.env.cr.dictfetchall(): - total = row.pop('total') or 1 + total = row['expected'] or 1 row['received_ratio'] = 100.0 * row['delivered'] / total row['opened_ratio'] = 100.0 * row['opened'] / total row['clicks_ratio'] = 100.0 * row['clicked'] / total @@ -568,31 +551,8 @@ class MassMailing(models.Model): name=_('%s (copy)') % self.name) return super(MassMailing, self).copy(default=default) - @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - """ Override read_group to always display all states. """ - if groupby and groupby[0] == "state": - # Default result structure - states = [('draft', _('Draft')), ('in_queue', _('In Queue')), ('sending', _('Sending')), ('done', _('Sent'))] - read_group_all_states = [{ - '__context': {'group_by': groupby[1:]}, - '__domain': domain + [('state', '=', state_value)], - 'state': state_value, - 'state_count': 0, - } for state_value, state_name in states] - # Get standard results - read_group_res = super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby) - # Update standard results with default results - result = [] - for state_value, state_name in states: - res = [x for x in read_group_res if x['state'] == state_value] - if not res: - res = [x for x in read_group_all_states if x['state'] == state_value] - res[0]['state'] = state_value - result.append(res[0]) - return result - else: - return super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby) + def _group_expand_states(self, states, domain, order): + return [key for key, val in type(self).state.selection] def update_opt_out(self, email, res_ids, value): model = self.env[self.mailing_model_real].with_context(active_test=False) diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index 91648f22d35..0a095943f99 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -27,11 +27,11 @@ class MailMailStats(models.Model): model = fields.Char(string='Document model') res_id = fields.Integer(string='Document ID') # campaign / wave data - mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') + mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing', index=True) mass_mailing_campaign_id = fields.Many2one( related='mass_mailing_id.mass_mailing_campaign_id', string='Mass Mailing Campaign', - store=True, readonly=True) + store=True, readonly=True, index=True) # Bounce and tracking scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now) sent = fields.Datetime(help='Date when the email has been sent') diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index b0faff60be3..b003e2a2e91 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -559,7 +559,7 @@ - + @@ -618,7 +618,7 @@
- +
diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml index ff2cfec238c..323a8421287 100644 --- a/addons/mrp/views/stock_move_views.xml +++ b/addons/mrp/views/stock_move_views.xml @@ -22,7 +22,7 @@