diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f8cf5847cbb..7c2724be4cd 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1570,7 +1570,8 @@ class AccountPartialReconcile(models.Model): move_date = self.debit_move_id.date newly_created_move = self.env['account.move'] with self.env.norecompute(): - for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): + # We use a set here in case the reconciled lines belong to the same move (it happens with POS) + 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 diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index d14646f26c3..c5022e230e9 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -711,6 +711,9 @@ help="Opportunities that are assigned to me"/> + diff --git a/addons/crm/views/crm_team_views.xml b/addons/crm/views/crm_team_views.xml index 091b20733a5..ac295d971b4 100644 --- a/addons/crm/views/crm_team_views.xml +++ b/addons/crm/views/crm_team_views.xml @@ -202,7 +202,7 @@
- + Open Opportunity Open Opportunities diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 92e4c2c4463..68bb8a9c09b 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -305,7 +305,9 @@ class AccountFrFec(models.TransientModel): END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, - CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, + CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' + WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/' + ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, diff --git a/addons/sale/models/sales_team.py b/addons/sale/models/sales_team.py index edcfe64688e..70c2af94c9c 100644 --- a/addons/sale/models/sales_team.py +++ b/addons/sale/models/sales_team.py @@ -66,7 +66,7 @@ class CrmTeam(models.Model): def _compute_sales_to_invoice(self): sale_order_data = self.env['sale.order'].read_group([ ('team_id', 'in', self.ids), - ('order_line.qty_to_invoice', '>', 0), + ('invoice_status','=','to invoice'), ], ['team_id'], ['team_id']) for datum in sale_order_data: self.browse(datum['team_id'][0]).sales_to_invoice_count = datum['team_id_count'] diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 305e04e49b4..2f757d32ab3 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -1173,7 +1173,7 @@ form tree,form,calendar,graph,kanban,pivot - [('state', '=', 'sale'),('invoice_status','=','to invoice')] + [('invoice_status','=','to invoice')] { 'search_default_team_id': [active_id], 'default_team_id': active_id, diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 4ae162788f3..34207e31514 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -390,10 +390,12 @@ class StockMove(models.Model): receipt_moves_to_reassign = self.env['stock.move'] if 'product_uom_qty' in vals: for move in self.filtered(lambda m: m.state not in ('done', 'draft') and m.picking_id): - if vals['product_uom_qty'] != move.product_uom_qty: + if float_compare(vals['product_uom_qty'], move.product_uom_qty, precision_rounding=move.product_uom.rounding): self.env['stock.move.line']._log_message(move.picking_id, move, 'stock.track_move_template', vals) if self.env.context.get('do_not_unreserve') is None: - move_to_unreserve = self.filtered(lambda m: m.state not in ['draft', 'done', 'cancel'] and m.reserved_availability > vals.get('product_uom_qty')) + move_to_unreserve = self.filtered( + lambda m: m.state not in ['draft', 'done', 'cancel'] and float_compare(m.reserved_availability, vals.get('product_uom_qty'), precision_rounding=m.product_uom.rounding) == 1 + ) move_to_unreserve._do_unreserve() (self - move_to_unreserve).filtered(lambda m: m.state == 'assigned').write({'state': 'partially_available'}) # When editing the initial demand, directly run again action assign on receipt moves. diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 77d271abd9f..ae675f7c71c 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -274,7 +274,10 @@ class StockMoveLine(models.Model): # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves. next_moves = self.env['stock.move'] if updates or 'qty_done' in vals: - for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'): + mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product') + if not updates: # we can skip those where qty_done is already good up to UoM rounding + mls = mls.filtered(lambda ml: not float_is_zero(ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding)) + for ml in mls: # undo the original move line qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id, diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index ee3fc8919e2..889fa9d7863 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -89,7 +89,10 @@ class StockMoveLine(models.Model): if 'qty_done' in vals: moves_to_update = {} for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())): - moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done + rounding = move_line.product_uom_id.rounding + qty_difference = float_round(vals['qty_done'] - move_line.qty_done, precision_rounding=rounding) + if not float_is_zero(qty_difference, precision_rounding=rounding): + moves_to_update[move_line.move_id] = qty_difference for move_id, qty_difference in moves_to_update.items(): move_vals = {} diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 7c37d1cbeaa..57f7e705939 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1652,7 +1652,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({ 'id': this.res_id, 'field': this.name, 'filename_field': filename_fieldname, - 'filename': this.recordData[filename_fieldname] || null, + 'filename': this.recordData[filename_fieldname] || "", 'download': true, 'data': utils.is_bin_size(this.value) ? null : this.value, }, diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 205be37929e..cbd6a0d90c3 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -347,9 +347,9 @@ class IrHttp(models.AbstractModel): if not filename: if filename_field in obj: filename = obj[filename_field] - elif module_resource_path: + if not filename and module_resource_path: filename = os.path.basename(module_resource_path) - else: + if not filename: filename = "%s-%s-%s" % (obj._name, obj.id, field) # mimetype @@ -364,6 +364,11 @@ class IrHttp(models.AbstractModel): if not mimetype: mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype) + # extension + extension = mimetypes.guess_extension(mimetype) + if extension and not filename.endswith(extension): + filename = "%s%s" % (filename, extension) + headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')] # cache diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 57d217e770d..f401760b88a 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -59,6 +59,8 @@ class MigrationManager(object): Python file names must start by `pre-` or `post-` and will be executed, respectively, before and after the module initialisation. `end-` scripts are run after all modules have been updated. + A special folder named `0.0.0` can contain scripts that will be run on any version change. + In `pre` stage, `0.0.0` scripts are run first, while in `post` and `end`, they are run last. Example: `-- migrations @@ -71,6 +73,8 @@ class MigrationManager(object): |-- 9.0.1.1 # processed only on a 9.0 server | |-- pre-delete_table_z.py | `-- post-clean-data.py + |-- 0.0.0 + | `-- end-invariants.py # processed on all version update `-- foo.py # not processed """ @@ -117,13 +121,20 @@ class MigrationManager(object): return version # the version number already containt the server version return "%s.%s" % (release.major_version, version) - def _get_migration_versions(pkg): + def _get_migration_versions(pkg, stage): versions = sorted({ ver for lv in self.migrations[pkg.name].values() for ver, lf in lv.items() if lf }, key=lambda k: parse_version(convert_version(k))) + if "0.0.0" in versions: + # reorder versions + versions.remove("0.0.0") + if stage == "pre": + versions.insert(0, "0.0.0") + else: + versions.append("0.0.0") return versions def _get_migration_files(pkg, version, stage): @@ -150,10 +161,11 @@ class MigrationManager(object): parsed_installed_version = parse_version(installed_version) current_version = parse_version(convert_version(pkg.data['version'])) - versions = _get_migration_versions(pkg) + versions = _get_migration_versions(pkg, stage) for version in versions: - if parsed_installed_version < parse_version(convert_version(version)) <= current_version: + if ((version == "0.0.0" and parsed_installed_version < current_version) + or parsed_installed_version < parse_version(convert_version(version)) <= current_version): strfmt = {'addon': pkg.name, 'stage': stage,