[IMP] models: ORM speedup

This branch is the combination of several optimizations in the ORM:

* store field values once in the cache: the cache reflects more
faithfully the database, only fields that explicitly depend on the
context have an extra indirection in the cache;

* delay recomputations by default: use method `recompute` to explicitly
flush out pending recomputations;

* delay updates in method `write`: updates are stored in a data
structure that can be flushed efficiently to the database with method
`flush` (which also flush out recomputations);

* make method `modified` take advantage of inverse fields to inverse
dependencies;

* filter records by evaluating a domain on records in Python;

* a computed field with `readonly=False` behaves like a normal field
with an onchange method;

* computed fields are computed in superuser mode by default.

Work done by Toufik Ben Jaa, Raphael Collet, Denis Ledoux and Fabien
Pinckaers.

closes odoo/odoo#35659

Signed-off-by: Denis Ledoux <beledouxdenis@users.noreply.github.com>
This commit is contained in:
Raphael Collet
2019-08-20 12:43:59 +00:00
committed by Denis Ledoux
parent e28d3aa4d3
commit 9920f20e4c
197 changed files with 3468 additions and 2083 deletions
+8 -6
View File
@@ -1148,16 +1148,18 @@ class AccountTax(models.Model):
@api.constrains('invoice_repartition_line_ids', 'refund_repartition_line_ids')
def _validate_repartition_lines(self):
for record in self:
record._check_repartition_lines(record.invoice_repartition_line_ids)
record._check_repartition_lines(record.refund_repartition_line_ids)
invoice_repartition_line_ids = record.invoice_repartition_line_ids.sorted()
refund_repartition_line_ids = record.refund_repartition_line_ids.sorted()
record._check_repartition_lines(invoice_repartition_line_ids)
record._check_repartition_lines(refund_repartition_line_ids)
if len(record.invoice_repartition_line_ids) != len(record.refund_repartition_line_ids):
if len(invoice_repartition_line_ids) != len(refund_repartition_line_ids):
raise ValidationError(_("Invoice and credit note repartition should have the same number of lines."))
index = 0
while index < len(record.invoice_repartition_line_ids):
inv_rep_ln = record.invoice_repartition_line_ids[index]
ref_rep_ln = record.refund_repartition_line_ids[index]
while index < len(invoice_repartition_line_ids):
inv_rep_ln = invoice_repartition_line_ids[index]
ref_rep_ln = refund_repartition_line_ids[index]
if inv_rep_ln.repartition_type != ref_rep_ln.repartition_type or inv_rep_ln.factor_percent != ref_rep_ln.factor_percent:
raise ValidationError(_("Invoice and credit note repartitions should match (same percentages, in the same order)."))
index += 1
@@ -25,6 +25,8 @@ class account_journal(models.Model):
journal.kanban_dashboard_graph = json.dumps(journal.get_bar_graph_datas())
elif (journal.type in ['cash', 'bank']):
journal.kanban_dashboard_graph = json.dumps(journal.get_line_graph_datas())
else:
journal.kanban_dashboard_graph = False
def _get_json_activity_data(self):
for journal in self:
@@ -243,6 +245,7 @@ class account_journal(models.Model):
#TODO need to check if all invoices are in the same currency than the journal!!!!
elif self.type in ['sale', 'purchase']:
title = _('Bills to pay') if self.type == 'purchase' else _('Invoices owed to you')
self.env['account.move'].flush(['amount_residual', 'currency_id', 'type', 'invoice_date', 'company_id', 'journal_id', 'date', 'state', 'invoice_payment_state'])
(query, query_args) = self._get_open_bills_to_pay_query()
self.env.cr.execute(query, query_args)
+113 -104
View File
@@ -875,6 +875,7 @@ class AccountMove(models.Model):
'line_ids.payment_id.state')
def _compute_amount(self):
invoice_ids = [move.id for move in self if move.id and move.is_invoice(include_receipts=True)]
self.env['account.payment'].flush(['state'])
if invoice_ids:
self._cr.execute(
'''
@@ -1009,14 +1010,15 @@ class AccountMove(models.Model):
def _compute_invoice_partner_display_info(self):
for move in self:
vendor_display_name = move.partner_id.name
move.invoice_icon = ''
if not vendor_display_name:
if move.invoice_source_email:
vendor_display_name = _('From: ') + move.invoice_source_email
move.invoice_partner_icon = '@'
else:
vendor_display_name = _('Created by: %s') % move.sudo().create_uid.name
vendor_display_name = _('Created by: %s') % (move.sudo().create_uid.name or self.env.user.name)
move.invoice_partner_icon = '#'
else:
move.invoice_partner_icon = False
move.invoice_partner_display_name = vendor_display_name
@api.depends('state', 'journal_id', 'invoice_date')
@@ -1032,13 +1034,16 @@ class AccountMove(models.Model):
# Check moves being candidates to set a custom number next.
moves = self.filtered(lambda move: move.is_invoice() and move.name == '/')
if not moves:
self.invoice_sequence_number_next_prefix = False
self.invoice_sequence_number_next = False
return
treated = self.browse()
for key, group in groupby(moves, key=lambda move: (move.journal_id, move._get_sequence())):
journal, sequence = key
domain = [('journal_id', '=', journal.id), ('state', '=', 'posted')]
if not isinstance(self.id, models.NewId):
domain.append(('id', '!=', self.id))
if self.ids:
domain.append(('id', 'not in', self.ids))
if journal.type == 'sale':
domain.append(('type', 'in', ('out_invoice', 'out_refund')))
elif journal.type == 'purchase':
@@ -1053,6 +1058,10 @@ class AccountMove(models.Model):
number_next = sequence._get_current_sequence().number_next_actual
move.invoice_sequence_number_next_prefix = prefix
move.invoice_sequence_number_next = '%%0%sd' % sequence.padding % number_next
treated |= move
remaining = (self - treated)
remaining.invoice_sequence_number_next_prefix = False
remaining.invoice_sequence_number_next = False
def _inverse_invoice_sequence_number_next(self):
''' Set the number_next on the sequence related to the invoice/bill/refund'''
@@ -1074,6 +1083,7 @@ class AccountMove(models.Model):
def _compute_payments_widget_to_reconcile_info(self):
for move in self:
move.invoice_outstanding_credits_debits_widget = json.dumps(False)
move.invoice_has_outstanding = False
if move.state != 'posted' or move.invoice_payment_state != 'not_paid' or not move.is_invoice(include_receipts=True):
continue
@@ -1159,6 +1169,7 @@ class AccountMove(models.Model):
def _compute_payments_widget_reconciled_info(self):
for move in self:
if move.state != 'posted' or not move.is_invoice(include_receipts=True):
move.invoice_payments_widget = json.dumps(False)
continue
reconciled_vals = move._get_reconciled_info_JSON_values()
if reconciled_vals:
@@ -1266,6 +1277,8 @@ class AccountMove(models.Model):
# /!\ As this method is called in create / write, we can't make the assumption the computed stored fields
# are already done. Then, this query MUST NOT depend of computed stored fields (e.g. balance).
# It happens as the ORM makes the create with the 'no_recompute' statement.
self.env['account.move.line'].flush(['debit', 'credit', 'move_id'])
self.env['account.move'].flush(['journal_id'])
self._cr.execute('''
SELECT line.move_id
FROM account_move_line line
@@ -1350,7 +1363,7 @@ class AccountMove(models.Model):
# Shortcut to load the demo data.
# Doing line.account_id triggers a default_get(['account_id']) that could returns a result.
# A section / note must not have an account_id set.
if 'account_id' not in line._cache and not line.display_type:
if not line._cache.get('account_id') and not line.display_type:
line.account_id = line._get_computed_account()
if not line.account_id:
if self.is_sale_document(include_receipts=True):
@@ -1406,6 +1419,7 @@ class AccountMove(models.Model):
move = self_ctx.new(new_vals)
new_vals_list.append(move._move_autocomplete_invoice_lines_values())
return new_vals_list
def _move_autocomplete_invoice_lines_write(self, vals):
@@ -2603,7 +2617,7 @@ class AccountMoveLine(models.Model):
for line in self:
line.balance = line.debit - line.credit
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state', 'company_id')
def _amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
@@ -3118,24 +3132,21 @@ class AccountMoveLine(models.Model):
cash_basis_subjected = []
part_rec = self.env['account.partial.reconcile']
with self.env.norecompute():
for partial_rec_dict in to_create:
debit_move, credit_move, amount_residual_currency = dc_vals[partial_rec_dict['debit_move_id'], partial_rec_dict['credit_move_id']]
# /!\ NOTE: Exchange rate differences shouldn't create cash basis entries
# i. e: we don't really receive/give money in a customer/provider fashion
# Since those are not subjected to cash basis computation we process them first
if not amount_residual_currency and debit_move.currency_id and credit_move.currency_id:
part_rec.create(partial_rec_dict)
else:
cash_basis_subjected.append(partial_rec_dict)
for after_rec_dict in cash_basis_subjected:
new_rec = part_rec.create(after_rec_dict)
# if the pair belongs to move being reverted, do not create CABA entry
if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id.reversed_entry_id'):
new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec)
self.recompute()
for partial_rec_dict in to_create:
debit_move, credit_move, amount_residual_currency = dc_vals[partial_rec_dict['debit_move_id'], partial_rec_dict['credit_move_id']]
# /!\ NOTE: Exchange rate differences shouldn't create cash basis entries
# i. e: we don't really receive/give money in a customer/provider fashion
# Since those are not subjected to cash basis computation we process them first
if not amount_residual_currency and debit_move.currency_id and credit_move.currency_id:
part_rec.create(partial_rec_dict)
else:
cash_basis_subjected.append(partial_rec_dict)
for after_rec_dict in cash_basis_subjected:
new_rec = part_rec.create(after_rec_dict)
# if the pair belongs to move being reverted, do not create CABA entry
if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id.reversed_entry_id'):
new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec)
return debit_moves+credit_moves
def auto_reconcile_lines(self):
@@ -3621,97 +3632,95 @@ class AccountPartialReconcile(models.Model):
self.ensure_one()
move_date = self.debit_move_id.date
newly_created_move = self.env['account.move']
with self.env.norecompute():
# 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
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
# 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
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
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_repartition_line_id.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,
'tax_repartition_line_id': line.tax_repartition_line_id.id,
'tax_base_amount': line.tax_base_amount,
'tag_ids': [(6, 0, line.tag_ids.ids)],
})
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 if rounded_amt > 0 else 0.0,
'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0,
'account_id': line.tax_repartition_line_id.account_id.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': line.analytic_tag_ids.ids,
'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,
'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,
'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,
'tax_repartition_line_id': line.tax_repartition_line_id.id,
'tax_base_amount': line.tax_base_amount,
'tag_ids': [(6, 0, line.tag_ids.ids)],
})
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,
'tax_repartition_line_id': line.tax_repartition_line_id.id,
'tax_base_amount': line.tax_base_amount,
'tag_ids': [(6, 0, line.tag_ids.ids)],
})
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()
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,
})
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
+7 -1
View File
@@ -160,9 +160,11 @@ class account_payment(models.Model):
@api.depends('invoice_ids', 'amount', 'payment_date', 'currency_id', 'payment_type')
def _compute_payment_difference(self):
for pay in self.filtered(lambda p: p.invoice_ids and p.state == 'draft'):
draft_payments = self.filtered(lambda p: p.invoice_ids and p.state == 'draft')
for pay in draft_payments:
payment_amount = -pay.amount if pay.payment_type == 'outbound' else pay.amount
pay.payment_difference = pay._compute_payment_amount(pay.invoice_ids, pay.currency_id, pay.journal_id, pay.payment_date) - payment_amount
(self - draft_payments).payment_difference = 0
@api.onchange('journal_id')
def _onchange_journal(self):
@@ -298,6 +300,10 @@ class account_payment(models.Model):
if not invoices:
return 0.0
self.env['account.move'].flush(['type', 'currency_id'])
self.env['account.move.line'].flush(['amount_residual', 'amount_residual_currency', 'move_id', 'account_id'])
self.env['account.account'].flush(['user_type_id'])
self.env['account.account.type'].flush(['type'])
self._cr.execute('''
SELECT
move.type AS type,
@@ -636,6 +636,9 @@ class AccountReconcileModel(models.Model):
# Type == 'invoice_matching'.
# Map each (st_line.id, model_id) with matching amls.
invoices_models = ordered_models.filtered(lambda m: m.rule_type == 'invoice_matching')
self.env['account.move'].flush(['state'])
self.env['account.move.line'].flush(['balance', 'reconciled'])
self.env['account.bank.statement.line'].flush(['company_id'])
if invoices_models:
query, params = invoices_models._get_invoice_matching_query(st_lines, excluded_ids=excluded_ids, partner_map=partner_map)
self._cr.execute(query, params)
+5 -1
View File
@@ -922,7 +922,11 @@ class AccountTaxTemplate(models.Model):
}
# We also have to delay the assignation of accounts to repartition lines
all_tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
# The below code assigns the account_id to the repartition lines according
# to the corresponding repartition line in the template, based on the order.
# As we just created the repartition lines, tax.invoice_repartition_line_ids is not well sorted.
# But we can force the sort by calling sort()
all_tax_rep_lines = tax.invoice_repartition_line_ids.sorted() + tax.refund_repartition_line_ids.sorted()
all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
for i in range(0, len(all_template_rep_lines)):
# We assume template and tax repartition lines are in the same order
+10
View File
@@ -223,6 +223,7 @@ class ResPartner(models.Model):
_name = 'res.partner'
_inherit = 'res.partner'
@api.depends_context('force_company')
def _credit_debit_get(self):
tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.company.id)._query_get()
where_params = [tuple(self.ids)] + where_params
@@ -238,12 +239,20 @@ class ResPartner(models.Model):
""" + where_clause + """
GROUP BY account_move_line.partner_id, act.type
""", where_params)
treated = self.browse()
for pid, type, val in self._cr.fetchall():
partner = self.browse(pid)
if type == 'receivable':
partner.credit = val
partner.debit = False
treated |= partner
elif type == 'payable':
partner.debit = -val
partner.credit = False
treated |= partner
remaining = (self - treated)
remaining.debit = False
remaining.credit = False
def _asset_difference_search(self, account_type, operator, operand):
if operator not in ('<', '=', '>', '>=', '<='):
@@ -337,6 +346,7 @@ class ResPartner(models.Model):
for partner in self:
# Avoid useless work if has_unreconciled_entries is not relevant for this partner
if not partner.active or not partner.is_company and partner.parent_id:
partner.has_unreconciled_entries = False
continue
self.env.cr.execute(
""" SELECT 1 FROM(
@@ -90,6 +90,12 @@ class TestAccountNoChartCommon(SavepointCase):
'type': 'sale',
'company_id': cls.env.user.company_id.id,
})
cls.journal_general = cls.env['account.journal'].create({
'name': 'General Journal - Test',
'code': 'AJ-GENERAL',
'type': 'general',
'company_id': cls.env.user.company_id.id,
})
@classmethod
def setUpUsers(cls):
+2 -2
View File
@@ -142,6 +142,6 @@ class InvoiceTestCommon(AccountingSavepointCase):
def assertInvoiceValues(self, move, expected_lines_values, expected_move_values):
def sort_lines(lines):
return lines.sorted(lambda line: (line.exclude_from_invoice_tab, not bool(line.tax_line_id), line.name or '', line.balance))
self.assertRecordValues(sort_lines(move.line_ids), expected_lines_values)
self.assertRecordValues(sort_lines(move.invoice_line_ids), expected_lines_values[:len(move.invoice_line_ids)])
self.assertRecordValues(sort_lines(move.line_ids.sorted()), expected_lines_values)
self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)])
self.assertRecordValues(move, [expected_move_values])
@@ -77,6 +77,7 @@ class TestAccountMove(InvoiceTestCommon):
],
})
self.test_move.flush()
self.cr.execute('SAVEPOINT test_misc_tax_lock_date_1')
# Writing something affecting a tax is not allowed.
@@ -106,6 +107,8 @@ class TestAccountMove(InvoiceTestCommon):
with self.assertRaises(ValidationError):
self.test_move.unlink()
self.test_move.flush()
self.test_move.invalidate_cache()
self.cr.execute('ROLLBACK TO SAVEPOINT test_misc_tax_lock_date_1')
with self.assertRaises(UserError):
+2 -2
View File
@@ -213,7 +213,7 @@ class TestInvoiceTaxes(AccountingTestCase):
self.assertEqual(len(inv_tax_lines), 2, "There should be two tax lines, one for each repartition line.")
self.assertEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 4.2, "Tax line on account 1 should amount to 4.2 (10% of 42)")
self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_1).tag_ids, inv_tax_tag_10, "Tax line on account 1 should have 10% tag")
self.assertEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_2).balance), 37.8, "Tax line on account 2 should amount to 37.8 (90% of 42)")
self.assertAlmostEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_2).balance), 37.8, 2, "Tax line on account 2 should amount to 37.8 (90% of 42)")
self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_2).tag_ids, inv_tax_tag_90, "Tax line on account 2 should have 90% tag")
# Test refund repartition
@@ -228,5 +228,5 @@ class TestInvoiceTaxes(AccountingTestCase):
ref_tax_lines = refund.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
self.assertEqual(len(ref_tax_lines), 2, "There should be two refund tax lines")
self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == ref_base_line.account_id).balance), 4.2, "Refund tax line on base account should amount to 4.2 (10% of 42)")
self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
self.assertAlmostEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, 2, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
self.assertEqual(ref_tax_lines.mapped('tag_ids'), ref_tax_tag, "Refund tax lines should have the right tag")
+36 -24
View File
@@ -1040,6 +1040,7 @@ class TestReconciliationExec(TestReconciliation):
self.env.cr.execute('UPDATE account_partial_reconcile SET create_date = %(date)s WHERE id = %(partial_id)s',
{'date': invoice.invoice_date,
'partial_id': statement_partial_id.id})
statement.flush()
# Case 1: report date is invoice date
# There should be an entry for the partner
@@ -1249,6 +1250,7 @@ class TestReconciliationExec(TestReconciliation):
})
payment_move.post()
(purchase_move + payment_move).invalidate_cache(['line_ids'])
to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable')
to_reconcile.reconcile()
@@ -1258,15 +1260,17 @@ class TestReconciliationExec(TestReconciliation):
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
self.assertTrue(purchase_move.line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move.line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_move.line_ids[0] + purchase_move.line_ids[1] + payment_move.line_ids[0])
purchase_move_line_ids = purchase_move.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_move_line_ids[0] + purchase_move_line_ids[1] + payment_move_line_ids[0])
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(purchase_move.line_ids[4].full_reconcile_id.exists())
self.assertEqual(purchase_move.line_ids[4].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move.line_ids[4])
self.assertTrue(purchase_move_line_ids[4].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[4].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move_line_ids[4])
self.assertEqual(len(cash_basis_aml_ids), 8)
@@ -1362,8 +1366,8 @@ class TestReconciliationExec(TestReconciliation):
})
payment_move1.post()
(purchase_move + payment_move0).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
(purchase_move + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
(purchase_move + payment_move0).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
(purchase_move + payment_move1).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
@@ -1371,20 +1375,21 @@ class TestReconciliationExec(TestReconciliation):
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
self.assertTrue(purchase_move.line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move.line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_move_line_ids = purchase_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[0].full_reconcile_id.reconciled_line_ids,
(purchase_move + payment_move0 + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable'))
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(purchase_move.line_ids[3].full_reconcile_id.exists())
self.assertEqual(purchase_move.line_ids[3].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_move.line_ids[3])
self.assertTrue(purchase_move_line_ids[3].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[3].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_move_line_ids[3])
self.assertTrue(purchase_move.line_ids[5].full_reconcile_id.exists())
self.assertEqual(purchase_move.line_ids[5].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move.line_ids[5])
self.assertTrue(purchase_move_line_ids[5].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[5].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move_line_ids[5])
self.assertEqual(len(cash_basis_aml_ids), 24)
@@ -1582,10 +1587,13 @@ class TestReconciliationExec(TestReconciliation):
to_reconcile.reconcile()
# check reconciliation in Payable account
self.assertTrue(purchase_move.line_ids[2].full_reconcile_id.exists())
purchase_line_ids = purchase_move.line_ids.sorted()
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
self.assertEqual(
purchase_move.line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_move.line_ids[2] + fx_move_01.line_ids[0] + payment_move.line_ids[0])
purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_line_ids[2] + fx_move_01_line_ids[0] + payment_move_line_ids[0])
# check cash basis
cash_basis_moves = self.env['account.move'].search(
@@ -1761,11 +1769,15 @@ class TestReconciliationExec(TestReconciliation):
to_reconcile.reconcile()
# check reconciliation in Payable account
self.assertTrue(purchase_move.line_ids[2].full_reconcile_id.exists())
purchase_move_line_ids = purchase_move.line_ids.sorted()
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
fx_move_02_line_ids = fx_move_02.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[2].full_reconcile_id.exists())
self.assertEqual(
purchase_move.line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_move.line_ids[2] + fx_move_01.line_ids[0] + fx_move_02.line_ids[0] +
payment_move.line_ids[0])
purchase_move_line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_move_line_ids[2] + fx_move_01_line_ids[0] + fx_move_02_line_ids[0] +
payment_move_line_ids[0])
# check cash basis
cash_basis_moves = self.env['account.move'].search(
@@ -2059,7 +2071,7 @@ class TestReconciliationExec(TestReconciliation):
'mv_line_ids': [move_payment_lines[1].id, move_product_lines[1].id],
'new_mv_line_dicts': [{
'account_id': liquidity_account.id,
'analytic_tag_ids': [6, None, []],
'analytic_tag_ids': [(6, None, [])],
'credit': 0,
'date': time.strftime('%Y') + '-01-01',
'debit': 15.0,
@@ -23,7 +23,7 @@ class TestReconciliationMatchingRules(AccountingTestCase):
def _check_statement_matching(self, rules, expected_values, statements=None):
if statements is None:
statements = self.bank_st + self.cash_st
statement_lines = statements.mapped('line_ids')
statement_lines = statements.mapped('line_ids').sorted()
matching_values = rules._apply_rules(statement_lines)
for st_line_id, values in matching_values.items():
values.pop('reconciled_lines', None)
@@ -56,7 +56,7 @@ class TestReconciliationMatchingRules(AccountingTestCase):
'name': 'write-off model',
'rule_type': 'writeoff_suggestion',
'match_partner': True,
'match_partner_ids': [6, 0, (self.partner_1 + self.partner_2).ids],
'match_partner_ids': [],
'account_id': current_assets_account.id,
})
@@ -66,6 +66,8 @@ class AccountInvoiceSend(models.TransientModel):
)
else:
wizard.invoice_without_email = False
else:
wizard.invoice_without_email = False
def _send_email(self):
if self.is_email:
@@ -138,6 +138,8 @@ class Partner(models.Model):
# assign the values to the fields
for k, v in vals.items():
partner[k] = v
for k in set(street_fields) - set(vals):
partner[k] = None
def write(self, vals):
res = super(Partner, self).write(vals)
@@ -94,6 +94,7 @@ record['name'] = record.name + 'X'
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="template_id" ref="base_automation.test_mail_template_automation"/>
<field name="state">email</field>
<field name="trigger_field_ids" eval="[(4,ref('base_automation.field_base_automation_lead_test__deadline'))]"/>
<field name="trigger">on_write</field>
<field name="active" eval="True"/>
<field name="filter_domain">[('deadline', '!=', False)]</field>
@@ -258,12 +258,8 @@ class BaseAutomation(models.Model):
return create
def make_write():
""" Instanciate a _write method that processes action rules. """
#
# Note: we patch method _write() instead of write() in order to
# catch updates made by field recomputations.
#
def _write(self, vals, **kw):
""" Instanciate a write method that processes action rules. """
def write(self, vals, **kw):
# retrieve the action rules to possibly execute
actions = self.env['base.automation']._get_actions(self, ['on_write', 'on_create_or_write'])
records = self.with_env(actions.env)
@@ -275,14 +271,42 @@ class BaseAutomation(models.Model):
for old_vals in (records.read(list(vals)) if vals else [])
}
# call original method
_write.origin(records, vals, **kw)
write.origin(records, vals, **kw)
# check postconditions, and execute actions on the records that satisfy them
for action in actions.with_context(old_values=old_values):
records, domain_post = action._filter_post_export_domain(pre[action])
action._process(records, domain_post=domain_post)
return True
return _write
return write
def make_compute_field_value():
""" Instanciate a compute_field_value method that processes action rules. """
#
# Note: This is to catch updates made by field recomputations.
#
def _compute_field_value(self, field):
# retrieve the action rules to possibly execute
if not field.store:
return _compute_field_value.origin(self, field)
actions = self.env['base.automation']._get_actions(self, ['on_write', 'on_create_or_write'])
records = self.with_env(actions.env)
# check preconditions on records
pre = {action: action._filter_pre(records) for action in actions}
# read old values before the update
old_values = {
old_vals.pop('id'): old_vals
for old_vals in (records.read([field.name]))
}
# call original method
_compute_field_value.origin(self, field)
# check postconditions, and execute actions on the records that satisfy them
for action in actions.with_context(old_values=old_values):
records, domain_post = action._filter_post_export_domain(pre[action])
action._process(records, domain_post=domain_post)
return True
return _compute_field_value
def make_unlink():
""" Instanciate an unlink method that processes action rules. """
@@ -340,10 +364,12 @@ class BaseAutomation(models.Model):
elif action_rule.trigger == 'on_create_or_write':
patch(Model, 'create', make_create())
patch(Model, '_write', make_write())
patch(Model, 'write', make_write())
patch(Model, '_compute_field_value', make_compute_field_value())
elif action_rule.trigger == 'on_write':
patch(Model, '_write', make_write())
patch(Model, 'write', make_write())
patch(Model, '_compute_field_value', make_compute_field_value())
elif action_rule.trigger == 'on_unlink':
patch(Model, 'unlink', make_unlink())
@@ -90,6 +90,7 @@ class base_automation_test(common.TransactionCase):
self.assertEqual(lead.user_id, self.user_root, "Responsible should not change on creation of Lead with state from 'draft' to 'open'.")
# change partner, recompute on lead should trigger the rule
partner.write({'employee': True})
lead.flush()
self.assertTrue(lead.employee, "Customer field should updated to True")
self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on write of Lead when Customer becomes True.")
+7 -4
View File
@@ -82,11 +82,14 @@ class Serialized(fields.Field):
column_type = ('text', 'text')
def convert_to_column(self, value, record, values=None, validate=True):
return json.dumps(value)
return self.convert_to_cache(value, record, validate=validate)
def convert_to_cache(self, value, record, validate=True):
# cache format: dict
value = value or {}
return value if isinstance(value, dict) else json.loads(value)
# cache format: json.dumps(value) or None
return json.dumps(value) if isinstance(value, dict) else (value or None)
def convert_to_record(self, value, record):
return json.loads(value or "{}")
fields.Serialized = Serialized
+2 -2
View File
@@ -82,7 +82,7 @@
<field name="partner_ids" eval="[(6,0,[ref('base.partner_demo'),ref('base.res_partner_1')])]"/>
<field name="name">Requirements review</field>
<field name="categ_ids" eval="[(6,0,[ref('categ_meet3')])]"/>
<field name="start" eval="time.strftime('%Y-%m-20 8:00:00')"/>
<field name="start" eval="time.strftime('%Y-%m-20 08:00:00')"/>
<field name="stop" eval="time.strftime('%Y-%m-20 10:30:00')"/>
<field name="duration" eval="2.5"/>
<field name="allday" eval="False"/>
@@ -107,7 +107,7 @@
<field name="partner_ids" eval="[(6,0,[ref('base.partner_root'),ref('base.res_partner_4'),ref('base.res_partner_1'),ref('base.res_partner_12')])]"/>
<field name="name">Presentation for new Services</field>
<field name="categ_ids" eval="[(6,0,[ref('categ_meet1'), ref('categ_meet2')])]"/>
<field name="start" eval="time.strftime('%Y-%m-18 2:00:00')"/>
<field name="start" eval="time.strftime('%Y-%m-18 02:00:00')"/>
<field name="stop" eval="time.strftime('%Y-%m-18 10:30:00')"/>
<field name="duration" eval="8.5"/>
<field name="allday" eval="False"/>
+24 -16
View File
@@ -761,6 +761,8 @@ class Meeting(models.Model):
for event in self:
if event.partner_ids.filtered(lambda s: s.id == partner_id):
event.is_highlighted = True
else:
event.is_highlighted = False
name = fields.Char('Meeting Subject', required=True, states={'done': [('readonly', True)]})
state = fields.Selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, tracking=True, default='draft')
@@ -1609,6 +1611,27 @@ class Meeting(models.Model):
records = self.browse(set(get_real_ids(self.ids)))
return super(Meeting, records).export_data(fields_to_export)
def _read(self, fields):
select = [(x, calendar_id2real_id(x)) for x in self.ids]
result = super(Meeting, self.browse(real_id for calendar_id, real_id in select))._read(fields)
for calendar_id, real_id in select:
if real_id != calendar_id:
calendar = self.browse(calendar_id)
real = self.browse(real_id)
ls = calendar_id2real_id(calendar_id, with_date=True)
for field in fields:
f = self._fields[field]
if field in ('start', 'start_date', 'start_datetime'):
value = ls[1]
elif field in ('stop', 'stop_date', 'stop_datetime'):
value = ls[2]
elif field == 'display_time':
value = self._get_display_time(ls[1], ls[2], real.duration, real.allday)
else:
value = self.env.cache.get(real, f)
self.env.cache.set(calendar, f, value)
return result
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
if 'date' in groupby:
@@ -1633,22 +1656,7 @@ class Meeting(models.Model):
for calendar_id, real_id in select:
if not real_data.get(real_id):
continue
res = real_data[real_id].copy()
ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1)
if not isinstance(ls, (str, int)) and len(ls) >= 2:
res['start'] = ls[1]
res['stop'] = ls[2]
if res['allday']:
res['start_date'] = ls[1]
res['stop_date'] = ls[2]
else:
res['start_datetime'] = ls[1]
res['stop_datetime'] = ls[2]
if 'display_time' in fields:
res['display_time'] = self._get_display_time(ls[1], ls[2], res['duration'], res['allday'])
res = real_data[real_id]
res['id'] = calendar_id
result.append(res)
+11 -2
View File
@@ -192,7 +192,10 @@ class Lead(models.Model):
@api.depends('date_open')
def _compute_day_open(self):
""" Compute difference between create date and open date """
for lead in self.filtered(lambda l: l.date_open and l.create_date):
leads = self.filtered(lambda l: l.date_open and l.create_date)
others = self - leads
others.day_open = None
for lead in leads:
date_create = fields.Datetime.from_string(lead.create_date)
date_open = fields.Datetime.from_string(lead.date_open)
lead.day_open = abs((date_open - date_create).days)
@@ -200,7 +203,10 @@ class Lead(models.Model):
@api.depends('date_closed')
def _compute_day_close(self):
""" Compute difference between current date and log date """
for lead in self.filtered(lambda l: l.date_closed and l.create_date):
leads = self.filtered(lambda l: l.date_closed and l.create_date)
others = self - leads
others.day_close = None
for lead in leads:
date_create = fields.Datetime.from_string(lead.create_date)
date_close = fields.Datetime.from_string(lead.date_closed)
lead.day_close = abs((date_close - date_create).days)
@@ -1493,6 +1499,7 @@ class Lead(models.Model):
args = [sql.Identifier(field) for field in fields] * 2
# Build sql query in safe mode
self.flush(['probability', 'active'])
query = """select probability, active, %s, count(probability) as count
from crm_lead l
where (probability = 0 or probability >= 100)
@@ -1528,6 +1535,7 @@ class Lead(models.Model):
def _pls_update_frequency_table_tag(self, frequencies, team_id, pls_start_date):
# get all tag_ids won / lost count
self.flush(['probability', 'active'])
query = """select l.probability, l.active, t.id, count(l.probability) as count
from crm_lead_tag_rel rel
inner join crm_lead_tag t on rel.tag_id = t.id
@@ -1582,6 +1590,7 @@ class Lead(models.Model):
str_fields = ", ".join(["{}"] * len(fields))
args = [sql.Identifier(field) for field in fields]
# Build sql query in safe mode
self.flush(['probability'])
query = """SELECT id, %s
FROM crm_lead l
WHERE probability > 0 AND probability < 100 AND active = True AND id in %%s order by team_id asc"""
+2
View File
@@ -30,6 +30,8 @@ class SaleOrder(models.Model):
delivery_line = self.order_line.filtered('is_delivery')
if delivery_line:
self.delivery_set = True
else:
self.delivery_set = False
@api.onchange('order_line', 'partner_id')
def onchange_order_line(self):
+3 -1
View File
@@ -12,8 +12,10 @@ class StockMove(models.Model):
@api.depends('product_id', 'product_uom_qty', 'product_uom')
def _cal_move_weight(self):
for move in self.filtered(lambda moves: moves.product_id.weight > 0.00):
moves_with_weight = self.filtered(lambda moves: moves.product_id.weight > 0.00)
for move in moves_with_weight:
move.weight = (move.product_qty * move.product_id.weight)
(self - moves_with_weight).weight = 0
def _get_new_picking_values(self):
vals = super(StockMove, self)._get_new_picking_values()
+2 -2
View File
@@ -30,7 +30,7 @@
<record id="event_0" model="event.event">
<field name="name">Design Fair Los Angeles</field>
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 8:00:00')" name="date_begin"/>
<field eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 08:00:00')" name="date_begin"/>
<field eval="(DateTime.now() + timedelta(days=5)).strftime('%Y-%m-%d 18:00:00')" name="date_end"/>
<field name="seats_availability">limited</field>
<field name="seats_max">500</field>
@@ -76,7 +76,7 @@
<record id="event_2" model="event.event">
<field name="name">Conference for Architects</field>
<field name="user_id" ref="base.user_admin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 7:00:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 07:00:00')" name="date_begin"/>
<field eval="(DateTime.today()+ timedelta(days=5)).strftime('%Y-%m-%d 16:30:00')" name="date_end"/>
<field name="event_type_id" ref="event_type_2"/>
<field name="address_id" ref="base.res_partner_4"/>
+4 -3
View File
@@ -29,17 +29,17 @@ class EventType(models.Model):
return [(0, 0, {
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_id': self.env.ref('event.event_subscription')
'template_id': self.env.ref('event.event_subscription').id,
}), (0, 0, {
'interval_nbr': 1,
'interval_unit': 'days',
'interval_type': 'before_event',
'template_id': self.env.ref('event.event_reminder')
'template_id': self.env.ref('event.event_reminder').id,
}), (0, 0, {
'interval_nbr': 10,
'interval_unit': 'days',
'interval_type': 'before_event',
'template_id': self.env.ref('event.event_reminder')
'template_id': self.env.ref('event.event_reminder').id,
})]
name = fields.Char('Event Category', required=True, translate=True)
@@ -201,6 +201,7 @@ class EventEvent(models.Model):
WHERE event_id IN %s AND state IN ('draft', 'open', 'done')
GROUP BY event_id, state
"""
self.env['event.registration'].flush(['event_id', 'state'])
self._cr.execute(query, (tuple(self.ids),))
for event_id, state, num in self._cr.fetchall():
event = self.browse(event_id)
+6 -1
View File
@@ -43,7 +43,12 @@ class BadgeUser(models.Model):
model=badge_user._name,
res_id=badge_user.id,
composition_mode='mass_mail',
partner_ids=badge_user.user_id.partner_id.ids,
# `website_forum` triggers `_cron_update` which triggers this method for template `Received Badge`
# for which `badge_user.user_id.partner_id.ids` equals `[8]`, which is then passed to `self.env['mail.compose.message'].create(...)`
# which expects a command list and not a list of ids. In master, this wasn't doing anything, at the end composer.partner_ids was [] and not [8]
# I believe this line is useless, it will take the partners to which the template must be send from the template itself (`partner_to`)
# The below line was therefore pointless.
# partner_ids=badge_user.user_id.partner_id.ids,
)
return True
+9
View File
@@ -128,6 +128,15 @@ class HrEmployeePrivate(models.Model):
return super(HrEmployeePrivate, self).name_get()
return self.env['hr.employee.public'].browse(self.ids).name_get()
def _read(self, fields):
if self.check_access_rights('read', raise_exception=False):
return super(HrEmployeePrivate, self)._read(fields)
res = self.env['hr.employee.public'].browse(self.ids).read(fields)
for r in res:
record = self.browse(r['id'])
record._update_cache({k:v for k,v in r.items() if k in fields}, validate=False)
def read(self, fields, load='_classic_read'):
if self.check_access_rights('read', raise_exception=False):
return super(HrEmployeePrivate, self).read(fields, load=load)
+15 -8
View File
@@ -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 api, fields, models
from pytz import timezone, UTC
from odoo.tools import format_time
@@ -33,6 +33,7 @@ class HrEmployeeBase(models.AbstractModel):
last_activity = fields.Date(compute="_compute_last_activity")
last_activity_time = fields.Char(compute="_compute_last_activity")
@api.depends('user_id.im_status')
def _compute_presence_state(self):
"""
This method is overritten in several other modules which add additional
@@ -49,16 +50,22 @@ class HrEmployeeBase(models.AbstractModel):
state = 'absent'
employee.hr_presence_state = state
@api.depends('user_id')
def _compute_last_activity(self):
employees = self.filtered(lambda e: e.user_id)
presences = self.env['bus.presence'].search([('user_id', 'in', employees.mapped('user_id.id'))])
presences = self.env['bus.presence'].search_read([('user_id', 'in', self.mapped('user_id').ids)], ['user_id', 'last_presence'])
# transform the result to a dict with this format {user.id: last_presence}
presences = {p['user_id']: p['last_presence'] for p in presences}
for presence in presences:
for employee in presence.user_id.employee_ids.filtered(lambda e: e in self):
tz = employee.tz
last_activity_datetime = presence.last_presence.replace(tzinfo=UTC).astimezone(timezone(tz)).replace(tzinfo=None)
for employee in self:
tz = employee.tz
last_presence = presences.get(employee.user_id.id, False)
if last_presence:
last_activity_datetime = last_presence.replace(tzinfo=UTC).astimezone(timezone(tz)).replace(tzinfo=None)
employee.last_activity = last_activity_datetime.date()
if employee.last_activity == fields.Date.today():
employee.last_activity_time = format_time(self.env, last_activity_datetime, time_format='short')
else:
employee.last_activity_date = False
employee.last_activity_time = False
else:
employee.last_activity = False
employee.last_activity_time = False
@@ -12,14 +12,14 @@
</record>
<record id="attendance_root1" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 08:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 12:00')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 08:00:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 12:00:00')" name="check_out"/>
<field name="employee_id" ref="hr.employee_admin"/>
</record>
<record id="attendance_root2" model="hr.attendance">
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 13:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 18:00')" name="check_out"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 13:00:00')" name="check_in"/>
<field eval="(datetime.now()+relativedelta(months=-1, days=-1)).strftime('%Y-%m-%d 18:00:00')" name="check_out"/>
<field name="employee_id" ref="hr.employee_admin"/>
</record>
@@ -92,7 +92,7 @@ class TestHrAttendance(TransactionCase):
def tz_datetime(year, month, day, hour, minute):
tz = pytz.timezone('Europe/Brussels')
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(pytz.utc)
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(pytz.utc).replace(tzinfo=None)
employee = self.env['hr.employee'].create({'name': 'Cunégonde', 'tz': 'Europe/Brussels'})
self.env['hr.attendance'].create({
+10 -10
View File
@@ -96,7 +96,7 @@
<field eval="289.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
<field name="date" eval="time.strftime('%Y')+'-4-3'"/>
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
</record>
<record id="laptop_expense" model="hr.expense">
@@ -107,7 +107,7 @@
<field eval="889.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
<field name="date" eval="time.strftime('%Y')+'-4-3'"/>
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
</record>
<record id="travel_ny_sheet" model="hr.expense.sheet">
@@ -176,7 +176,7 @@
<field name="product_id" ref="car_travel"/>
<field eval="0.52" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="date" eval="time.strftime('%Y')+'-1-15'"/>
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
<field eval="152.0" name="quantity"/>
<field name="sheet_id" ref="customer_meeting_sheet"/>
</record>
@@ -186,7 +186,7 @@
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product_product_fixed_cost"/>
<field name="date" eval="time.strftime('%Y')+'-1-15'"/>
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
<field eval="152.8" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
<field name="sheet_id" ref="customer_meeting_sheet"/>
@@ -204,7 +204,7 @@
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product_product_fixed_cost"/>
<field name="date" eval="time.strftime('%Y-%m')+'-5'"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="12.5" name="unit_amount"/>
<field eval="12.0" name="quantity"/>
<field name="sheet_id" ref="team_building_sheet"/>
@@ -215,7 +215,7 @@
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product_product_fixed_cost"/>
<field name="date" eval="time.strftime('%Y-%m')+'-5'"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="2.5" name="unit_amount"/>
<field eval="17.0" name="quantity"/>
<field name="sheet_id" ref="team_building_sheet"/>
@@ -226,7 +226,7 @@
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product_product_fixed_cost"/>
<field name="date" eval="time.strftime('%Y-%m')+'-5'"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="25" name="unit_amount"/>
<field eval="12.0" name="quantity"/>
<field name="sheet_id" ref="team_building_sheet"/>
@@ -244,7 +244,7 @@
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product.product_delivery_01"/>
<field name="date" eval="time.strftime('%Y')+'-6-2'"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="55.75" name="unit_amount"/>
<field eval="6.0" name="quantity"/>
<field name="sheet_id" ref="office_furniture_sheet"/>
@@ -255,7 +255,7 @@
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product.product_delivery_02"/>
<field name="date" eval="time.strftime('%Y')+'-6-2'"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="28.99" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
<field name="sheet_id" ref="office_furniture_sheet"/>
@@ -267,7 +267,7 @@
<field name="employee_id" ref="hr.employee_stw"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="product_product_fixed_cost"/>
<field name="date" eval="time.strftime('%Y-')+'-3-15'"/>
<field name="date" eval="time.strftime('%Y')+'-03-15'"/>
<field eval="112.58" name="unit_amount"/>
<field eval="4.0" name="quantity"/>
</record>
+24 -24
View File
@@ -194,10 +194,10 @@
<record id="hr_holidays_cl_mit_2" model="hr.leave">
<field name="name">Trip</field>
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field eval="time.strftime('%Y-%m-5')" name="date_from"/>
<field eval="time.strftime('%Y-%m-7')" name="date_to"/>
<field eval="time.strftime('%Y-%m-5')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-7')" name="request_date_to"/>
<field eval="time.strftime('%Y-%m-05')" name="date_from"/>
<field eval="time.strftime('%Y-%m-07')" name="date_to"/>
<field eval="time.strftime('%Y-%m-05')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-07')" name="request_date_to"/>
<field name="number_of_days">3</field>
<field name="employee_id" ref="hr.employee_mit"/>
</record>
@@ -239,10 +239,10 @@
<record id="hr_holidays_cl_qdp" model="hr.leave">
<field name="name">Sick day</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-3 1:0:0')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-5 23:0:0')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-3 1:0:0')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-5 23:0:0')" name="request_date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-03 01:00:00')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-05 23:00:00')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-03 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-05 23:00:00')" name="request_date_to"/>
<field name="number_of_days">3</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="state">confirm</field>
@@ -251,10 +251,10 @@
<record id="hr_holidays_sl_qdp" model="hr.leave">
<field name="name">Sick day</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field eval="time.strftime('%Y-%m-1 1:0:0')" name="date_from"/>
<field eval="time.strftime('%Y-%m-3 23:0:0')" name="date_to"/>
<field eval="time.strftime('%Y-%m-1 1:0:0')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-3 23:0:0')" name="request_date_to"/>
<field eval="time.strftime('%Y-%m-01 01:00:00')" name="date_from"/>
<field eval="time.strftime('%Y-%m-03 23:00:00')" name="date_to"/>
<field eval="time.strftime('%Y-%m-01 01:00:00')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-03 23:00:00')" name="request_date_to"/>
<field name="number_of_days">1</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="state">confirm</field>
@@ -332,10 +332,10 @@
<record id="hr_holidays_sl_vad" model="hr.leave">
<field name="name">Doctor Appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field eval="time.strftime('%Y-%m-25 1:0:0')" name="date_from"/>
<field eval="time.strftime('%Y-%m-26 23:0:0')" name="date_to"/>
<field eval="time.strftime('%Y-%m-25 1:0:0')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-26 23:0:0')" name="request_date_to"/>
<field eval="time.strftime('%Y-%m-25 01:00:00')" name="date_from"/>
<field eval="time.strftime('%Y-%m-26 23:00:00')" name="date_to"/>
<field eval="time.strftime('%Y-%m-25 01:00:00')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-26 23:00:00')" name="request_date_to"/>
<field name="number_of_days">2</field>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="state">confirm</field>
@@ -371,10 +371,10 @@
<record id="hr_holidays_sl_kim" model="hr.leave">
<field name="name">Dentist appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-1 1:0:0')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-1 23:0:0')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-1 1:0:0')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-1 23:0:0')" name="request_date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-01 01:00:00')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-01 23:00:00')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-01 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1)).strftime('%Y-%m-01 23:00:00')" name="request_date_to"/>
<field name="number_of_days">1</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="state">confirm</field>
@@ -383,10 +383,10 @@
<record id="hr_holidays_sl_kim_2" model="hr.leave">
<field name="name">Second dentist appointment</field>
<field name="holiday_status_id" ref="holiday_status_sl"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-3 1:0:0')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-3 23:0:0')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-3 1:0:0')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-3 23:0:0')" name="request_date_to"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-03 01:00:00')" name="date_from"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-03 23:00:00')" name="date_to"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-03 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=4)).strftime('%Y-%m-03 23:00:00')" name="request_date_to"/>
<field name="number_of_days">1</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="state">confirm</field>
+6 -6
View File
@@ -605,16 +605,16 @@ class HolidaysRequest(models.Model):
holiday_sudo.activity_update()
return holiday
def _read_from_database(self, field_names, inherited_field_names=[]):
if 'name' in field_names and 'employee_id' not in field_names:
field_names.append('employee_id')
super(HolidaysRequest, self)._read_from_database(field_names, inherited_field_names)
if 'name' in field_names:
def _read(self, fields):
if 'name' in fields and 'employee_id' not in fields:
fields.add('employee_id')
super(HolidaysRequest, self)._read(fields)
if 'name' in fields:
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
return
current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
for record in self:
emp_id = record._cache.get('employee_id', False) and record._cache.get('employee_id')[0]
emp_id = record._cache.get('employee_id') or False
if emp_id != current_employee.id:
try:
record._cache['name']
@@ -552,6 +552,7 @@ class TestAccessRightsRead(TestLeavesRights):
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days': 1,
})
other_leave.invalidate_cache(['name'])
self.assertEqual(
other_leave.with_user(self.user_employee_id).name, '*****',
'Private information should have been stripped, received %s instead' % other_leave.with_user(self.user_employee_id).name
@@ -800,7 +801,7 @@ class TestMultiCompany(TestHrHolidaysBase):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_user(self):
employee_leave = self.employee_leave.with_user(self.user_employee)
employee_leave.invalidate_cache(['name'])
with self.assertRaises(AccessError):
employee_leave.name
@@ -810,7 +811,7 @@ class TestMultiCompany(TestHrHolidaysBase):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_officer(self):
employee_leave_hruser = self.employee_leave.with_user(self.user_hruser)
employee_leave_hruser.invalidate_cache(['name'])
with self.assertRaises(AccessError):
employee_leave_hruser.name
@@ -820,7 +821,7 @@ class TestMultiCompany(TestHrHolidaysBase):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_manager(self):
employee_leave_hrmanager = self.employee_leave.with_user(self.user_hrmanager)
employee_leave_hrmanager.invalidate_cache(['name'])
with self.assertRaises(AccessError):
employee_leave_hrmanager.name
@@ -156,6 +156,8 @@ class TestHolidaysFlow(TestHrHolidaysBase):
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: refuse should lead to refuse state')
# Check left days: 2 days left again
hol_status_2_employee_group.invalidate_cache(['max_leaves'])
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
self.assertEqual(hol2.state, 'refuse',
@@ -127,6 +127,7 @@ class TestLeaveRequests(TestHrHolidaysBase):
hol.with_user(self.user_hrmanager_id).action_approve()
holiday_status.invalidate_cache(['max_leaves'])
self._check_holidays_status(holiday_status, 2.0, 2.0, 0.0, 0.0)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@@ -171,19 +171,26 @@ class Applicant(models.Model):
date_create = applicant.create_date
date_open = applicant.date_open
applicant.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600)
else:
applicant.day_open = False
if applicant.date_closed:
date_create = applicant.create_date
date_closed = applicant.date_closed
applicant.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600)
applicant.delay_close = applicant.day_close - applicant.day_open
else:
applicant.day_close = False
applicant.delay_close = False
@api.depends('email_from')
def _compute_application_count(self):
application_data = self.env['hr.applicant'].read_group([
('email_from', 'in', list(set(self.mapped('email_from'))))], ['email_from'], ['email_from'])
application_data_mapped = dict((data['email_from'], data['email_from_count']) for data in application_data)
for applicant in self.filtered(lambda applicant: applicant.email_from):
applicants = self.filtered(lambda applicant: applicant.email_from)
for applicant in applicants:
applicant.application_count = application_data_mapped.get(applicant.email_from, 1) - 1
(self - applicants).application_count = False
def _compute_meeting_count(self):
for applicant in self:
@@ -94,6 +94,7 @@ class HrWorkEntry(models.Model):
# use '()' to exlude the lower and upper bounds of the range.
# Filter on date_start and date_stop (both indexed) in the EXISTS clause to
# limit the resulting set size and fasten the query.
self.flush(['date_start', 'date_stop', 'employee_id', 'active'])
query = """
SELECT b1.id
FROM hr_work_entry b1
@@ -45,6 +45,8 @@ class MailChannel(models.Model):
for record in self:
if record.channel_type == 'livechat':
record.is_chat = True
else:
record.is_chat = False
def _channel_message_notifications(self, message, message_format=False):
""" When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when
@@ -32,7 +32,7 @@ class TestGetMailChannel(TransactionCase):
self.livechat_channel = self.env['im_livechat.channel'].create({
'name': 'The channel',
'user_ids': (6, 0, self.operators.ids)
'user_ids': [(6, 0, self.operators.ids)]
})
operators = self.operators
+2 -1
View File
@@ -14,7 +14,8 @@ class UtmCampaign(models.Model):
def _compute_items_total(self):
"""Empty method that will be implemented by inheriting classes"""
return
self.clicked_total = False
self.items_total = False
@api.depends('items_total', 'clicked_total')
def _compute_clicks_ratio(self):
+31 -31
View File
@@ -114,7 +114,7 @@
<record model="lunch.product" id="product_bacon">
<field name="name">Bacon</field>
<field name="category_id" eval="str(ref('categ_burger'))"/>
<field name="category_id" ref="categ_burger"/>
<field name="price">7.5</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce</field>
@@ -124,7 +124,7 @@
<record model="lunch.product" id="product_cheeseburger">
<field name="name">Cheese Burger</field>
<field name="category_id" eval="str(ref('categ_burger'))"/>
<field name="category_id" ref="categ_burger"/>
<field name="price">7.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Cheddar, Salad, Fried Onions, BBQ Sauce</field>
@@ -133,7 +133,7 @@
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
@@ -141,7 +141,7 @@
<record model="lunch.product" id="product_spicy_tuna">
<field name="name">Spicy Tuna</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
@@ -149,7 +149,7 @@
<record model="lunch.product" id="product_mozzarella">
<field name="name">Mozzarella</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.9</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Mozzarella, Pesto, Tomatoes</field>
@@ -158,7 +158,7 @@
<record model="lunch.product" id="product_club">
<field name="name">Club</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.4</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Ham, Cheese, Vegetables</field>
@@ -167,7 +167,7 @@
<record model="lunch.product" id="product_maki">
<field name="name">Lunch Maki 18pc</field>
<field name="category_id" eval="str(ref('categ_sushi'))"/>
<field name="category_id" ref="categ_sushi"/>
<field name="price">12.0</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">6 Maki Salmon - 6 Maki Tuna - 6 Maki Shrimp/Avocado</field>
@@ -177,7 +177,7 @@
<record model="lunch.product" id="product_salmon">
<field name="name">Lunch Salmon 20pc</field>
<field name="category_id" eval="str(ref('categ_sushi'))"/>
<field name="category_id" ref="categ_sushi"/>
<field name="price">13.80</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">4 Sushi Salmon - 6 Maki Salmon - 4 Sashimi Salmon </field>
@@ -187,7 +187,7 @@
<record model="lunch.product" id="product_temaki">
<field name="name">Lunch Temaki mix 3pc</field>
<field name="category_id" eval="str(ref('categ_temaki'))"/>
<field name="category_id" ref="categ_temaki"/>
<field name="price">14.0</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna</field>
@@ -197,7 +197,7 @@
<record model="lunch.product" id="product_chirashi">
<field name="name">Salmon and Avocado</field>
<field name="category_id" eval="str(ref('categ_chirashi'))"/>
<field name="category_id" ref="categ_chirashi"/>
<field name="price">9.25</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">2 Tempuras, Cabbages, Onions, Sesame Sauce</field>
@@ -207,61 +207,61 @@
<record model="lunch.product" id="product_cheese_ham">
<field name="name">Cheese And Ham</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.30</field>
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Cheese, Ham, Salad, Tomatoes, cucumbers, eggs</field>
<field name="image_1920" type="base64" file="lunch/static/img/club.png"/>
</record>
<record model="lunch.product" id="product_country">
<field name="name">The Country</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.30</field>
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Brie, Honey, Walnut Kernels</field>
<field name="image_1920" type="base64" file="lunch/static/img/brie.png"/>
</record>
<record model="lunch.product" id="product_tuna">
<field name="name">Tuna</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.50</field>
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Tuna, Mayonnaise</field>
<field name="image_1920" type="base64" file="lunch/static/img/tuna_sandwich.png"/>
</record>
<record model="lunch.product" id="product_gouda">
<field name="name">Gouda Cheese</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.50</field>
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/gouda.png"/>
</record>
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.60</field>
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
</record>
<record model="lunch.product" id="product_margherita">
<field name="name">Pizza Margherita</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="category_id" ref="categ_pizza"/>
<field name="price">6.90</field>
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mozzarella</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza_margherita.png"/>
</record>
<record model="lunch.product" id="product_funghi">
<field name="name">Pizza Funghi</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.00</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mushrooms, Mozzarella</field>
@@ -270,7 +270,7 @@
<record model="lunch.product" id="product_vege">
<field name="name">Pizza Vegetarian</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.00</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mozzarella, Mushrooms, Peppers, Olives</field>
@@ -279,27 +279,27 @@
<record model="lunch.product" id="product_italiana">
<field name="name">Pizza Italiana</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.40</field>
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Fresh Tomatoes, Basil, Mozzarella</field>
<field name="image_1920" type="base64" file="lunch/static/img/italiana.png"/>
</record>
<record model="lunch.product" id="product_Bolognese">
<field name="name">Bolognese Pasta</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="category_id" ref="categ_pasta"/>
<field name="price">7.70</field>
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/pasta_bolognese.png"/>
</record>
<record model="lunch.product" id="product_Napoli">
<field name="name">Napoli Pasta</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="category_id" ref="categ_pasta"/>
<field name="price">7.70</field>
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Basil</field>
<field name="image_1920" type="base64" file="lunch/static/img/napoli.png"/>
</record>
@@ -312,7 +312,7 @@
<record model="lunch.product" id="product_4formaggi">
<field name="name">4 Formaggi</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="category_id" ref="categ_pasta"/>
<field name="price">5.50</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan</field>
+1 -1
View File
@@ -99,7 +99,7 @@ class ServerActions(models.Model):
# Pick an arbitrary field; if it is marked to be recomputed,
# it means we are in an extraneous write triggered by the recompute.
# In this case, we should not create a new activity.
if records._recompute_check(field):
if records & self.env.records_to_compute(field):
return True
return False
+1
View File
@@ -64,6 +64,7 @@ class IrModel(models.Model):
if 'is_mail_blacklist' in vals and not all(rec.is_mail_blacklist <= vals['is_mail_blacklist'] for rec in self):
raise UserError(_('Field "Mail Blacklist" cannot be changed to "False".'))
res = super(IrModel, self).write(vals)
self.flush()
# setup models; this reloads custom models in registry
self.pool.setup_models(self._cr)
# update database schema of models
+1
View File
@@ -46,5 +46,6 @@ class IrModelField(models.Model):
WHERE f.id IN %s
);
"""
self.flush()
self.env.cr.execute(query, (tuple(self.ids),))
return super(IrModelField, self).unlink()
+27 -20
View File
@@ -115,13 +115,13 @@ class MailActivity(models.Model):
return res
# owner
res_id = fields.Integer('Related Document ID', index=True, required=True)
res_model_id = fields.Many2one(
'ir.model', 'Document Model',
index=True, ondelete='cascade', required=True)
res_model = fields.Char(
'Related Document Model',
index=True, related='res_model_id.model', compute_sudo=True, store=True, readonly=True)
res_id = fields.Many2oneReference(string='Related Document ID', index=True, required=True, model_field='res_model')
res_name = fields.Char(
'Document Name', compute='_compute_res_name', compute_sudo=True, store=True,
help="Display name of the related document.", readonly=True)
@@ -224,6 +224,26 @@ class MailActivity(models.Model):
self.activity_type_id = self.recommended_activity_type_id
def _filter_access_rules(self, operation):
# write / unlink: valid for creator / assigned
if operation in ('write', 'unlink'):
valid = super(MailActivity, self)._filter_access_rules(operation)
if valid and valid == self:
return self
else:
valid = self.env[self._name]
return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules')
def _filter_access_rules_python(self, operation):
# write / unlink: valid for creator / assigned
if operation in ('write', 'unlink'):
valid = super(MailActivity, self)._filter_access_rules_python(operation)
if valid and valid == self:
return self
else:
valid = self.env[self._name]
return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules_python')
def _filter_access_rules_remaining(self, valid, operation, filter_access_rules_method):
""" Return the subset of ``self`` for which ``operation`` is allowed.
A custom implementation is done on activities as this document has some
access rules and is based on related document for activities that are
@@ -238,19 +258,6 @@ class MailActivity(models.Model):
* unlink: access rule OR
(``mail_post_access`` or write) rights on related documents);
"""
if self.env.is_superuser():
return self
if not self.check_access_rights(operation, raise_exception=False):
return self.env[self._name]
# write / unlink: valid for creator / assigned
if operation in ('write', 'unlink'):
valid = super(MailActivity, self)._filter_access_rules(operation)
if valid and valid == self:
return self
else: # create / read: linked to document only, no access rules defined
valid = self.env[self._name]
# compute remaining for hand-tailored rules
remaining = self - valid
remaining_sudo = remaining.sudo()
@@ -273,7 +280,7 @@ class MailActivity(models.Model):
doc_operation = 'write'
right = self.env[doc_model].check_access_rights(doc_operation, raise_exception=False)
if right:
valid_doc_ids = self.env[doc_model].browse(doc_ids)._filter_access_rules(doc_operation)
valid_doc_ids = getattr(self.env[doc_model].browse(doc_ids), filter_access_rules_method)(doc_operation)
valid += remaining.filtered(lambda activity: activity.res_model == doc_model and activity.res_id in valid_doc_ids.ids)
return valid
@@ -569,8 +576,7 @@ class MailActivityMixin(models.AbstractModel):
activity_ids = fields.One2many(
'mail.activity', 'res_id', 'Activities',
auto_join=True,
groups="base.group_user",
domain=lambda self: [('res_model', '=', self._name)])
groups="base.group_user",)
activity_state = fields.Selection([
('overdue', 'Overdue'),
('today', 'Today'),
@@ -622,9 +628,8 @@ class MailActivityMixin(models.AbstractModel):
break
if activity_type_id.decoration_type == 'warning':
exception_activity_type_id = activity_type_id
if exception_activity_type_id:
record.activity_exception_decoration = exception_activity_type_id.decoration_type
record.activity_exception_icon = exception_activity_type_id.icon
record.activity_exception_decoration = exception_activity_type_id and exception_activity_type_id.decoration_type
record.activity_exception_icon = exception_activity_type_id and exception_activity_type_id.icon
def _search_activity_exception_decoration(self, operator, operand):
return [('activity_ids.activity_type_id.decoration_type', operator, operand)]
@@ -639,6 +644,8 @@ class MailActivityMixin(models.AbstractModel):
record.activity_state = 'today'
elif 'planned' in states:
record.activity_state = 'planned'
else:
record.activity_state = False
@api.depends('activity_ids.date_deadline')
def _compute_activity_date_deadline(self):
+2
View File
@@ -113,6 +113,8 @@ class MailBlackListMixin(models.AbstractModel):
@api.model
def _search_is_blacklisted(self, operator, value):
# Assumes operator is '=' or '!=' and value is True or False
self.flush(['email_normalized'])
self.env['mail.blacklist'].flush(['email', 'active'])
self._assert_primary_email()
if operator != '=':
if operator == '!=' and isinstance(value, bool):
+5 -2
View File
@@ -82,8 +82,9 @@ class Channel(models.Model):
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners')
# depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen', depends=['channel_partner_ids'])
channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners', depends=['channel_last_seen_partner_ids'])
channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel')
is_member = fields.Boolean('Is a member', compute='_compute_is_member')
# access
@@ -169,6 +170,8 @@ class Channel(models.Model):
for record in self:
if record.channel_type == 'chat':
record.is_chat = True
else:
record.is_chat = False
@api.onchange('public')
def _onchange_public(self):
+8 -5
View File
@@ -26,8 +26,8 @@ class Followers(models.Model):
# (see 'ir.model' inheritance).
res_model = fields.Char(
'Related Document Model Name', required=True, index=True)
res_id = fields.Integer(
'Related Document ID', index=True, help='Id of the followed resource')
res_id = fields.Many2oneReference(
'Related Document ID', index=True, help='Id of the followed resource', model_field='res_model')
partner_id = fields.Many2one(
'res.partner', string='Related Partner', ondelete='cascade', index=True)
channel_id = fields.Many2one(
@@ -46,9 +46,6 @@ class Followers(models.Model):
for record in (vals_list or [{'res_model': rec.res_model, 'res_id': rec.res_id} for rec in self]):
if record.get('res_id'):
to_invalidate[record.get('res_model')].append(record.get('res_id'))
# invalidate in batch for performance
for res_model, res_ids in to_invalidate.items():
self.env[res_model].invalidate_cache(ids=res_ids)
@api.model_create_multi
def create(self, vals_list):
@@ -102,6 +99,12 @@ class Followers(models.Model):
notification status of partner or channel (email or inbox),
user groups of partner (void as irrelevant if channel ID),
"""
self.env['mail.followers'].flush(['partner_id', 'channel_id', 'subtype_ids'])
self.env['mail.message.subtype'].flush(['internal'])
self.env['res.users'].flush(['notification_type', 'active', 'partner_id', 'groups_id'])
self.env['res.partner'].flush(['active', 'partner_share'])
self.env['res.groups'].flush(['users'])
self.env['mail.channel'].flush(['email_send', 'channel_type'])
if records and subtype_id:
query = """
WITH sub_followers AS (
+3
View File
@@ -336,6 +336,9 @@ class MailMail(models.Model):
'failure_type': 'UNKNOWN',
'failure_reason': notif_msg,
})
# `test_mail_bounce_during_send`, force immediate update to obtain the lock.
# see rev. 56596e5240ef920df14d99087451ce6f06ac6d36
notifs.flush(fnames=['notification_status', 'failure_type', 'failure_reason'], records=notifs)
# build an RFC2822 email.message.Message object and send it without queuing
res = None
+23 -4
View File
@@ -55,7 +55,7 @@ class Message(models.Model):
child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages')
# related document
model = fields.Char('Related Document Model', index=True)
res_id = fields.Integer('Related Document ID', index=True)
res_id = fields.Many2oneReference('Related Document ID', index=True, model_field='model')
record_name = fields.Char('Message Record Name', help="Name get of the related document.")
# characteristics
message_type = fields.Selection([
@@ -87,7 +87,7 @@ class Message(models.Model):
# mainly usefull for testing
notified_partner_ids = fields.Many2many(
'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action',
context={'active_test': False})
context={'active_test': False}, depends=['notification_ids'])
needaction = fields.Boolean(
'Need Action', compute='_get_needaction', search='_search_needaction',
help='Need Action')
@@ -99,12 +99,12 @@ class Message(models.Model):
# notifications
notification_ids = fields.One2many(
'mail.notification', 'mail_message_id', 'Notifications',
auto_join=True, copy=False)
auto_join=True, copy=False, depends=['notified_partner_ids'])
# user interface
starred_partner_ids = fields.Many2many(
'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By')
starred = fields.Boolean(
'Starred', compute='_get_starred', search='_search_starred',
'Starred', compute='_get_starred', search='_search_starred', compute_sudo=False,
help='Current user has a starred notification linked to this message')
# tracking
tracking_value_ids = fields.One2many(
@@ -130,6 +130,14 @@ class Message(models.Model):
#keep notification layout informations to be able to generate mail again
email_layout_xmlid = fields.Char('Layout', copy=False) # xml id of layout
add_sign = fields.Boolean(default=True)
# `test_adv_activity`, `test_adv_activity_full`, `test_message_assignation_inbox`,...
# By setting an inverse for mail.mail_message_id, the number of SQL queries done by `modified` is reduced.
# 'mail.mail' inherits from `mail.message`: `_inherits = {'mail.message': 'mail_message_id'}`
# Therefore, when changing a field on `mail.message`, this triggers the modification of the same field on `mail.mail`
# By setting up the inverse one2many, we avoid to have to do a search to find the mails linked to the `mail.message`
# as the cache value for this inverse one2many is up-to-date.
# Besides for new messages, and messages never sending emails, there was no mail, and it was searching for nothing.
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails')
def _get_needaction(self):
""" Need action on a mail.message = notified on my channel """
@@ -159,6 +167,7 @@ class Message(models.Model):
return ['!', ('notification_ids.notification_status', 'in', ('bounce', 'exception'))] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
@api.depends('starred_partner_ids')
@api.depends_context('uid')
def _get_starred(self):
""" Compute if the message is starred by the current user. """
# TDE FIXME: use SQL
@@ -637,6 +646,10 @@ class Message(models.Model):
# check read access rights before checking the actual rules on the given ids
super(Message, self.with_user(access_rights_uid or self._uid)).check_access_rights('read')
self.flush(['model', 'res_id', 'author_id', 'message_type', 'partner_ids', 'channel_ids'])
self.env['mail.notification'].flush(['mail_message_id', 'res_partner_id'])
self.env['mail.channel'].flush(['channel_message_ids'])
self.env['mail.channel.partner'].flush(['channel_id', 'partner_id'])
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.message_type,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
@@ -733,6 +746,12 @@ class Message(models.Model):
# Read mail_message.ids to have their values
message_values = dict((message_id, {}) for message_id in self.ids)
self.flush(['model', 'res_id', 'author_id', 'parent_id', 'moderation_status', 'message_type', 'partner_ids', 'channel_ids'])
self.env['mail.notification'].flush(['mail_message_id', 'res_partner_id'])
self.env['mail.channel'].flush(['channel_message_ids', 'moderator_ids'])
self.env['mail.channel.partner'].flush(['channel_id', 'partner_id'])
self.env['res.users'].flush(['moderation_channel_ids'])
if operation == 'read':
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
+5 -3
View File
@@ -82,8 +82,7 @@ class MailThread(models.AbstractModel):
message_is_follower = fields.Boolean(
'Is Follower', compute='_compute_is_follower', search='_search_is_follower')
message_follower_ids = fields.One2many(
'mail.followers', 'res_id', string='Followers',
domain=lambda self: [('res_model', '=', self._name)])
'mail.followers', 'res_id', string='Followers')
message_partner_ids = fields.Many2many(
comodel_name='res.partner', string='Followers (Partners)',
compute='_get_followers', search='_search_follower_partners')
@@ -92,7 +91,7 @@ class MailThread(models.AbstractModel):
compute='_get_followers', search='_search_follower_channels')
message_ids = fields.One2many(
'mail.message', 'res_id', string='Messages',
domain=lambda self: [('model', '=', self._name), ('message_type', '!=', 'user_notification')], auto_join=True)
domain=lambda self: [('message_type', '!=', 'user_notification')], auto_join=True)
message_unread = fields.Boolean(
'Unread Messages', compute='_get_message_unread',
help="If checked new messages require your attention.")
@@ -1741,6 +1740,9 @@ class MailThread(models.AbstractModel):
self = self.with_lang() # add lang to context imediatly since it will be usefull in various flows latter.
# Explicit access rights check, because display_name is computed as sudo.
self.check_access_rights('read')
self.check_access_rule('read')
record_name = record_name or self.display_name
partner_ids = set(partner_ids or [])
+1
View File
@@ -39,6 +39,7 @@ class Partner(models.Model):
def get_needaction_count(self):
""" compute the number of needaction of the current user """
if self.env.user.partner_id:
self.env['mail.notification'].flush(['is_read', 'res_partner_id'])
self.env.cr.execute("""
SELECT count(*) as needaction_count
FROM mail_message_res_partner_needaction_rel R
+3 -1
View File
@@ -230,6 +230,7 @@ class MailComposer(models.TransientModel):
new_attachment_ids.append(attachment.copy({'res_model': 'mail.compose.message', 'res_id': wizard.id}).id)
else:
new_attachment_ids.append(attachment.id)
new_attachment_ids.reverse()
wizard.write({'attachment_ids': [(6, 0, new_attachment_ids)]})
# Mass Mailing
@@ -359,6 +360,7 @@ class MailComposer(models.TransientModel):
for attach_id in mail_values.pop('attachment_ids'):
new_attach_id = self.env['ir.attachment'].browse(attach_id).copy({'res_model': self._name, 'res_id': self.id})
attachment_ids.append(new_attach_id.id)
attachment_ids.reverse()
mail_values['attachment_ids'] = self.env['mail.thread']._message_post_process_attachments(
mail_values.pop('attachments', []),
attachment_ids,
@@ -416,7 +418,7 @@ class MailComposer(models.TransientModel):
}
attachment_ids.append(Attachment.create(data_attach).id)
if values.get('attachment_ids', []) or attachment_ids:
values['attachment_ids'] = [(5,)] + values.get('attachment_ids', []) + attachment_ids
values['attachment_ids'] = [(6, 0, values.get('attachment_ids', []) + attachment_ids)]
else:
default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'])
values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values)
+6 -1
View File
@@ -27,6 +27,9 @@ class MaintenanceEquipmentCategory(models.Model):
@api.depends('equipment_ids')
def _compute_fold(self):
# fix mutual dependency: 'fold' depends on 'equipment_count', which is
# computed with a read_group(), which retrieves 'fold'!
self.fold = False
for category in self:
category.fold = False if category.equipment_count else True
@@ -147,7 +150,8 @@ class MaintenanceEquipment(models.Model):
@api.depends('effective_date', 'period', 'maintenance_ids.request_date', 'maintenance_ids.close_date')
def _compute_next_maintenance(self):
date_now = fields.Date.context_today(self)
for equipment in self.filtered(lambda x: x.period > 0):
equipments = self.filtered(lambda x: x.period > 0)
for equipment in equipments:
next_maintenance_todo = self.env['maintenance.request'].search([
('equipment_id', '=', equipment.id),
('maintenance_type', '=', 'preventive'),
@@ -184,6 +188,7 @@ class MaintenanceEquipment(models.Model):
else:
next_date = self.effective_date + timedelta(days=equipment.period)
equipment.next_action_date = next_date
(self - equipments).next_action_date = False
@api.depends('maintenance_ids.stage_id.done')
def _compute_maintenance_count(self):
@@ -79,6 +79,8 @@ class MassMailingList(models.Model):
self.ensure_one()
# Put destination is sources lists if not already the case
src_lists |= self
self.env['mailing.contact'].flush(['email', 'email_normalized'])
self.env['mailing.contact.subscription'].flush(['contact_id', 'opt_out', 'list_id'])
self.env.cr.execute("""
INSERT INTO mailing_contact_list_rel (contact_id, list_id)
SELECT st.contact_id AS contact_id, %s AS list_id
@@ -110,6 +112,7 @@ class MassMailingList(models.Model):
)
) st
WHERE st.rn = 1;""", (self.id, tuple(src_lists.ids), self.id))
self.flush()
self.invalidate_cache()
if archive:
(src_lists - self).write({'active': False})
+1 -2
View File
@@ -111,8 +111,7 @@ class Partner(models.Model):
def _cron_update_membership(self):
partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])])
# mark the field to be recomputed, and recompute it
partners._recompute_todo(self._fields['membership_state'])
self.recompute()
self.env.add_to_compute(self._fields['membership_state'], partners)
def create_membership_invoice(self, product, amount):
""" Create Customer Invoice of Membership for partners.
+3
View File
@@ -175,6 +175,7 @@ class MrpWorkorder(models.Model):
to the lot/sn used in other workorders.
"""
productions = self.mapped('production_id')
treated = self.browse()
for production in productions:
if production.product_id.tracking == 'none':
continue
@@ -203,6 +204,8 @@ class MrpWorkorder(models.Model):
workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.filtered(lambda wl: wl.product_id == production.product_id).mapped('lot_id')
else:
workorder.allowed_lots_domain = allowed_lot_ids
treated |= workorder
(self - treated).allowed_lots_domain = False
def name_get(self):
return [(wo.id, "%s - %s - %s" % (wo.production_id.name, wo.product_id.name, wo.name)) for wo in self]
+4
View File
@@ -96,7 +96,11 @@ class StockMove(models.Model):
move.order_finished_lot_ids = finished_lots_ids
move.finished_lots_exist = True
else:
move.order_finished_lot_ids = False
move.finished_lots_exist = False
else:
move.order_finished_lot_ids = False
move.finished_lots_exist = False
@api.depends('product_id.tracking')
def _compute_needs_lots(self):
+8
View File
@@ -23,6 +23,9 @@ class StockPickingType(models.Model):
def _get_mo_count(self):
mrp_picking_types = self.filtered(lambda picking: picking.code == 'mrp_operation')
if not mrp_picking_types:
self.count_mo_waiting = False
self.count_mo_todo = False
self.count_mo_late = False
return
domains = {
'count_mo_waiting': [('reservation_state', '=', 'waiting')],
@@ -36,6 +39,11 @@ class StockPickingType(models.Model):
count = {x['picking_type_id'] and x['picking_type_id'][0]: x['picking_type_id_count'] for x in data}
for record in mrp_picking_types:
record[field] = count.get(record.id, 0)
remaining = (self - mrp_picking_types)
if remaining:
remaining.count_mo_waiting = False
remaining.count_mo_todo = False
remaining.count_mo_late = False
def get_mrp_stock_picking_action_picking_type(self):
return self._get_action('mrp.mrp_production_action_picking_deshboard')
+4 -4
View File
@@ -228,13 +228,13 @@ class TestTraceability(TestMrpCommon):
raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
self.assertEqual(raw_line_raw_1_lot_1.lot_produced_ids.mapped('name'), ['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])
self.assertEqual(set(raw_line_raw_1_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
self.assertEqual(raw_line_raw_1_lot_2.lot_produced_ids.mapped('name'), ['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2'])
self.assertEqual(set(raw_line_raw_1_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2']))
raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
self.assertEqual(raw_line_raw_2_lot_1.lot_produced_ids.mapped('name'), ['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])
self.assertEqual(set(raw_line_raw_2_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
self.assertEqual(raw_line_raw_2_lot_2.lot_produced_ids.mapped('name'), ['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2'])
self.assertEqual(set(raw_line_raw_2_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2']))
finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
@@ -132,6 +132,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
warehouse.delivery_steps = 'pick_pack_ship'
self.warehouse.flush()
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
picking_customer = self.env['stock.picking'].create({
'location_id': self.warehouse.wh_output_stock_loc_id.id,
@@ -210,6 +211,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
cancel depends on the default values on each rule of the chain.
"""
self.warehouse.manufacture_steps = 'pbm_sam'
self.warehouse.flush()
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
picking_customer = self.env['stock.picking'].create({
'location_id': self.warehouse.lot_stock_id.id,
+1 -1
View File
@@ -734,7 +734,7 @@ class TestWorkOrderProcess(TestMrpCommon):
mo_laptop.action_confirm()
mo_laptop.button_plan()
workorders = mo_laptop.workorder_ids
workorders = mo_laptop.workorder_ids.sorted()
self.assertEqual(len(workorders), 3)
workorders[0].button_start()
@@ -16,16 +16,20 @@ class StockPicking(models.Model):
for picking in self:
# Hide if not encoding state
if picking.state in ('draft', 'cancel', 'done'):
picking.display_action_record_components = False
continue
if not picking._is_subcontract():
picking.display_action_record_components = False
continue
# Hide if no components are track
subcontracted_productions = picking._get_subcontracted_productions()
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
picking.display_action_record_components = False
continue
# Hide if the production is to close
if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
picking.display_action_record_components = False
continue
picking.display_action_record_components = True
+1 -1
View File
@@ -86,7 +86,7 @@
<p>* Odoo for Retail and Industrial Management</p>]]>
</field>
<field name="color">7</field>
<field name="stage_ids" eval="['note_stage_03']"/>
<field name="stage_ids" eval="[(6,0,[ref('note_stage_03')])]"/>
<field name="user_id" ref="base.user_admin"/>
</record>
@@ -49,7 +49,7 @@
<field name="weight">0.01</field>
<field name="to_weight">True</field>
<field name="barcode">2100002000003</field>
<field name="taxes_id">False</field>
<field name="taxes_id" eval='[(5,)]'/>
<field name="categ_id" ref="product.product_category_5"/>
<field name="pos_categ_id" ref="pos_category_miscellaneous"/>
<field name="uom_id" ref="uom.product_uom_unit" />
@@ -63,7 +63,7 @@
<field name="default_code">FURN_0008</field>
<field name="type">product</field>
<field name="weight">0.01</field>
<field name="taxes_id">False</field>
<field name="taxes_id" eval='[(5,)]'/>
<field name="categ_id" ref="product.product_category_5"/>
<field name="pos_categ_id" ref="pos_category_miscellaneous"/>
<field name="to_weight">True</field>
+2 -1
View File
@@ -256,6 +256,7 @@ class PosConfig(models.Model):
else:
pos_config.last_session_closing_cash = 0
pos_config.last_session_closing_date = False
pos_config.last_session_closing_cashbox = False
@api.depends('session_ids')
def _compute_current_session_user(self):
@@ -498,7 +499,7 @@ class PosConfig(models.Model):
self._check_company_invoice_journal()
self._check_company_payment()
self._check_currencies()
self.current_session_id = self.env['pos.session'].create({
self.env['pos.session'].create({
'user_id': self.env.uid,
'config_id': self.id
})
+2 -1
View File
@@ -40,7 +40,7 @@ class PosOrder(models.Model):
'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False,
'pos_reference': ui_order['name'],
'partner_id': ui_order['partner_id'] or False,
'date_order': ui_order['creation_date'],
'date_order': ui_order['creation_date'].replace('T', ' ')[:19],
'fiscal_position_id': ui_order['fiscal_position_id'],
'pricelist_id': ui_order['pricelist_id'],
'amount_paid': ui_order['amount_paid'],
@@ -754,6 +754,7 @@ class PosOrderLine(models.Model):
self.price_subtotal = taxes['total_excluded']
self.price_subtotal_incl = taxes['total_included']
@api.depends('order_id', 'order_id.fiscal_position_id')
def _get_tax_ids_after_fiscal_position(self):
for line in self:
line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id)
+4 -1
View File
@@ -214,6 +214,7 @@ class ProductProduct(models.Model):
for product in self:
product.is_product_variant = True
@api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra')
def _compute_product_price(self):
prices = {}
pricelist_id_or_name = self._context.get('pricelist')
@@ -256,12 +257,12 @@ class ProductProduct(models.Model):
value -= product.price_extra
product.write({'list_price': value})
@api.depends('product_template_attribute_value_ids.price_extra')
def _compute_product_price_extra(self):
for product in self:
product.price_extra = sum(product.mapped('product_template_attribute_value_ids.price_extra'))
@api.depends('list_price', 'price_extra')
@api.depends_context('uom')
def _compute_product_lst_price(self):
to_uom = None
if 'uom' in self._context:
@@ -274,6 +275,7 @@ class ProductProduct(models.Model):
list_price = product.list_price
product.lst_price = list_price + product.price_extra
@api.depends_context('partner_id')
def _compute_product_code(self):
for product in self:
for supplier_info in product.seller_ids:
@@ -283,6 +285,7 @@ class ProductProduct(models.Model):
else:
product.code = product.default_code
@api.depends_context('partner_id')
def _compute_partner_ref(self):
for product in self:
for supplier_info in product.seller_ids:
@@ -64,6 +64,7 @@ class ProductAttribute(models.Model):
if invalidate_cache:
# prefetched o2m have to be resequenced
# (eg. product.template: attribute_line_ids)
self.flush()
self.invalidate_cache()
return res
@@ -132,6 +133,7 @@ class ProductAttributeValue(models.Model):
if invalidate_cache:
# prefetched o2m have to be resequenced
# (eg. product.template.attribute.line: value_ids)
self.flush()
self.invalidate_cache()
return res
@@ -92,6 +92,7 @@ class Pricelist(models.Model):
def _compute_price_rule_get_items(self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids):
self.ensure_one()
# Load all rules
self.env['product.pricelist.item'].flush(['price', 'currency_id', 'company_id'])
self.env.cr.execute(
"""
SELECT
@@ -336,6 +336,8 @@ class ProductTemplate(models.Model):
for p in self:
if len(p.product_variant_ids) == 1:
p.packaging_ids = p.product_variant_ids.packaging_ids
else:
p.packaging_ids = False
def _set_packaging_ids(self):
for p in self:
@@ -524,6 +526,7 @@ class ProductTemplate(models.Model):
return prices
def create_variant_ids(self):
self.flush()
Product = self.env["product.product"]
variants_to_create = []
@@ -611,6 +614,7 @@ class ProductTemplate(models.Model):
self.ensure_one()
return any(a.create_variant == 'dynamic' for a in self.valid_product_attribute_ids)
@api.depends('attribute_line_ids', 'attribute_line_ids.value_ids')
def _compute_valid_attributes(self):
"""A product template attribute line is considered valid if it has at
least one possible value.
+1
View File
@@ -15,6 +15,7 @@ class Partner(models.Model):
help="This pricelist will be used, instead of the default one, for sales to the current partner")
@api.depends('country_id')
@api.depends_context('force_company')
def _compute_product_pricelist(self):
company = self.env.context.get('force_company', False)
res = self.env['product.pricelist']._get_partner_pricelist_multi(self.ids, company_id=company)
@@ -21,8 +21,10 @@ class StockProductionLot(models.Model):
@api.depends('alert_date')
def _compute_product_expiry_alert(self):
current_date = fields.Datetime.now()
for lot in self.filtered(lambda l: l.alert_date):
lots = self.filtered(lambda l: l.alert_date)
for lot in lots:
lot.product_expiry_alert = lot.alert_date <= current_date
(self - lots).product_expiry_alert = False
def _get_dates(self, product_id=None):
"""Returns dates based on number of days configured in current lot's product."""
@@ -106,6 +106,9 @@ class ProductProduct(models.Model):
company_id = self.env.company.id
#Cost price is calculated afterwards as it is a property
self.env['account.move.line'].flush(['price_unit', 'quantity', 'balance', 'product_id', 'display_type'])
self.env['account.move'].flush(['state', 'invoice_payment_state', 'type', 'invoice_date', 'company_id'])
self.env['product.template'].flush(['list_price'])
sqlstr = """
WITH currency_rate AS ({})
SELECT
+7 -1
View File
@@ -546,7 +546,7 @@ class Task(models.Model):
for task in self:
attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids
message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread
task.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids))
task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))]
@api.depends('create_date', 'date_end', 'date_assign')
def _compute_elapsed(self):
@@ -561,12 +561,18 @@ class Task(models.Model):
duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_assign, compute_leaves=True)
task.working_hours_open = duration_data['hours']
task.working_days_open = duration_data['days']
else:
task.working_hours_open = 0.0
task.working_days_open = 0.0
if task.date_end:
dt_date_end = fields.Datetime.from_string(task.date_end)
duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_end, compute_leaves=True)
task.working_hours_close = duration_data['hours']
task.working_days_close = duration_data['days']
else:
task.working_hours_close = 0.0
task.working_days_close = 0.0
(self - task_linked_to_calendar).update(dict.fromkeys(
['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0))
+1 -1
View File
@@ -65,7 +65,7 @@ class TestPortalProject(TestPortalProjectBase):
def test_followers_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'followers'})
pigs.flush(['privacy_visibility'])
# Do: Alfred reads project -> ko (employee ko followers)
self.assertRaises(AccessError, pigs.with_user(self.user_projectuser).read, ['user_id'])
# Test: no project task visible
+4
View File
@@ -518,12 +518,16 @@ class PurchaseOrderLine(models.Model):
for line in self:
if line.product_id.type in ['consu', 'service']:
line.qty_received_method = 'manual'
else:
line.qty_received_method = False
@api.depends('qty_received_method', 'qty_received_manual')
def _compute_qty_received(self):
for line in self:
if line.qty_received_method == 'manual':
line.qty_received = line.qty_received_manual or 0.0
else:
line.qty_received = 0.0
@api.onchange('qty_received')
def _inverse_qty_received(self):
+6
View File
@@ -18,12 +18,15 @@ class res_partner(models.Model):
domain=[('partner_id', 'in', all_partners.ids)],
fields=['partner_id'], groupby=['partner_id']
)
partners = self.browse()
for group in purchase_order_groups:
partner = self.browse(group['partner_id'][0])
while partner:
if partner in self:
partner.purchase_order_count += group['partner_id_count']
partners |= partner
partner = partner.parent_id
(self - partners).purchase_order_count = 0
def _compute_supplier_invoice_count(self):
# retrieve all children partners and prefetch 'parent_id' on them
@@ -35,12 +38,15 @@ class res_partner(models.Model):
('type', 'in', ('in_invoice', 'in_refund'))],
fields=['partner_id'], groupby=['partner_id']
)
partners = self.browse()
for group in supplier_invoice_groups:
partner = self.browse(group['partner_id'][0])
while partner:
if partner in self:
partner.supplier_invoice_count += group['partner_id_count']
partners |= partner
partner = partner.parent_id
(self - partners).supplier_invoice_count = 0
@api.model
def _commercial_fields(self):
@@ -61,6 +61,7 @@ class TestPurchaseOrderReport(common.TransactionCase):
f.purchase_id = po
invoice = f.save()
invoice.post()
po.flush()
res_product1 = self.PurchaseReport.search([
('order_id', '=', po.id), ('product_id', '=', self.product1.id)])
@@ -35,7 +35,7 @@
<record id="rfq1_line" model="purchase.order.line">
<field name="order_id" ref="rfq1"/>
<field name="name" model="purchase.order.line" eval="obj().env.ref('product.product_product_13').partner_ref"/>
<field name="date_planned" eval="time.strftime('%Y/%m/10')"/>
<field name="date_planned" eval="time.strftime('%Y-%m-10')"/>
<field name="product_id" ref="product.product_product_13"/>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="price_unit">60</field>
@@ -51,7 +51,7 @@
<record id="rfq2_line" model="purchase.order.line">
<field name="order_id" ref="rfq2"/>
<field name="name" model="purchase.order.line" eval="obj().env.ref('product.product_product_13').partner_ref"/>
<field name="date_planned" eval="time.strftime('%Y/%m/15')"/>
<field name="date_planned" eval="time.strftime('%Y-%m-15')"/>
<field name="product_id" ref="product.product_product_13"/>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="price_unit">50</field>
+2
View File
@@ -52,6 +52,8 @@ class PurchaseOrder(models.Model):
for order in self:
if order.picking_ids and all([x.state in ['done', 'cancel'] for x in order.picking_ids]):
order.is_shipped = True
else:
order.is_shipped = False
@api.onchange('picking_type_id')
def _onchange_picking_type_id(self):
@@ -197,13 +197,14 @@ class TestCreatePicking(common.TestProductCommon):
# the move should be 12 units
# note: move.product_qty = computed field, always in the uom of the quant
# move.product_uom_qty = stored field representing the initial demand in move.product_uom
move1 = po.picking_ids.move_lines[0]
move1 = po.picking_ids.move_lines.sorted()[0]
self.assertEqual(move1.product_uom_qty, 12)
self.assertEqual(move1.product_uom.id, uom_unit.id)
self.assertEqual(move1.product_qty, 12)
# edit the so line, sell 2 dozen, the move should now be 24 units
po.order_line.product_qty = 2
move1 = po.picking_ids.move_lines.sorted()[0]
self.assertEqual(move1.product_uom_qty, 24)
self.assertEqual(move1.product_uom.id, uom_unit.id)
self.assertEqual(move1.product_qty, 24)
@@ -23,7 +23,7 @@ class TestFifoPrice(TestPurchase):
'standard_price': 70.0,
'uom_id': self.env.ref('uom.product_uom_kgm').id,
'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
'supplier_taxes_id': '[]',
'supplier_taxes_id': [],
'description': 'FIFO Ice Cream',
})
product_cable_management_box.categ_id.property_cost_method = 'fifo'
@@ -217,7 +217,7 @@ class TestFifoPrice(TestPurchase):
'standard_price': 70.0,
'uom_id': self.env.ref('uom.product_uom_kgm').id,
'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
'supplier_taxes_id': '[]',
'supplier_taxes_id': [],
'description': 'FIFO Ice Cream',
})
product_fifo_negative.categ_id.property_cost_method = 'fifo'
+2 -6
View File
@@ -57,8 +57,7 @@ class RatingMixin(models.AbstractModel):
def _compute_rating_last_value(self):
for record in self:
ratings = self.env['rating.rating'].search([('res_model', '=', self._name), ('res_id', '=', record.id)], limit=1)
if ratings:
record.rating_last_value = ratings.rating
record.rating_last_value = ratings and ratings.rating or 0
@api.depends('rating_ids')
def _compute_rating_stats(self):
@@ -78,13 +77,10 @@ class RatingMixin(models.AbstractModel):
for record in self:
if record._rec_name in values: # set the res_name of ratings to be recomputed
res_name_field = self.env['rating.rating']._fields['res_name']
record.rating_ids._recompute_todo(res_name_field)
self.env.add_to_compute(res_name_field, record.rating_ids)
if record._rating_get_parent_field_name() in values:
record.rating_ids.write({'parent_res_id': record[record._rating_get_parent_field_name()].id})
if self.env.recompute and self._context.get('recompute', True): # trigger the recomputation of all field marked as "to recompute"
self.recompute()
return result
def unlink(self):
@@ -11,6 +11,8 @@ class AccountReconciliation(models.AbstractModel):
if not stl_ids:
return res
# Search if we can find a sale order line that match the statement reference
self.env['sale.order'].flush(['name', 'reference', 'invoice_status', 'company_id', 'state', 'partner_id'])
self.env['account.bank.statement.line'].flush(['name', 'partner_id'])
sql_query = """
SELECT stl.id, array_agg(o.id) AS order_id
FROM sale_order o,
+1 -1
View File
@@ -202,7 +202,7 @@ class ProductTemplate(models.Model):
]
if no_variant_attributes_price_extra:
product = product.with_context(
no_variant_attributes_price_extra=no_variant_attributes_price_extra
no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra)
)
list_price = product.price_compute('list_price')[product.id]
price = product.price if pricelist else list_price
+3
View File
@@ -22,12 +22,15 @@ class ResPartner(models.Model):
domain=[('partner_id', 'in', all_partners.ids)],
fields=['partner_id'], groupby=['partner_id']
)
partners = self.browse()
for group in sale_order_groups:
partner = self.browse(group['partner_id'][0])
while partner:
if partner in self:
partner.sale_order_count += group['partner_id_count']
partners |= partner
partner = partner.parent_id
(self - partners).sale_order_count = 0
def can_edit_vat(self):
''' Can't edit `vat` if there is (non draft) issued SO. '''
+3 -1
View File
@@ -241,6 +241,8 @@ class SaleOrder(models.Model):
dates_list.append(dt)
if dates_list:
order.expected_date = fields.Datetime.to_string(min(dates_list))
else:
order.expected_date = False
def _compute_remaining_validity_days(self):
for record in self:
@@ -1357,7 +1359,7 @@ class SaleOrderLine(models.Model):
]
if no_variant_attributes_price_extra:
product = product.with_context(
no_variant_attributes_price_extra=no_variant_attributes_price_extra
no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra)
)
if self.order_id.pricelist_id.discount_policy == 'with_discount':
+6
View File
@@ -21,6 +21,8 @@ class UtmCampaign(models.Model):
campaign.quotation_count = data_map.get(campaign.id, 0)
def _compute_sale_invoiced_amount(self):
self.env['account.move.line'].flush(['balance', 'move_id', 'account_id', 'exclude_from_invoice_tab'])
self.env['account.move'].flush(['state', 'campaign_id', 'type'])
query = """SELECT move.campaign_id, -SUM(line.balance) as price_subtotal
FROM account_move_line line
INNER JOIN account_move move ON line.move_id = move.id
@@ -35,9 +37,13 @@ class UtmCampaign(models.Model):
self._cr.execute(query, [tuple(self.ids)])
query_res = self._cr.dictfetchall()
campaigns = self.browse()
for datum in query_res:
campaign = self.browse(datum['campaign_id'])
campaign.invoiced_amount = datum['price_subtotal']
campaigns |= campaign
for campaign in (self - campaigns):
campaign.invoiced_amount = 0
def action_redirect_to_quotations(self):
action = self.env.ref('sale.action_quotations_with_onboarding').read()[0]
+1 -2
View File
@@ -103,8 +103,7 @@ class TestAccessRights(TestCommonSaleNoChart):
# Salesperson can change a Sales Team of SO
self.order.with_user(self.user_salesperson_1).write({'team_id': self.sales_channel.id})
# Salesperson can't create the SO of other salesperson
# raise ValidationError instead of AccessError due to constraint
with self.assertRaises(ValidationError):
with self.assertRaises(AccessError):
self.env['sale.order'].with_user(self.user_salesperson_1).create({
'partner_id': self.partner_customer_usd.id,
'user_id': self.user_salesperson.id
+12
View File
@@ -122,6 +122,18 @@ class TestSaleOrder(TestCommonSaleNoChart):
# upsell and invoice
self.sol_serv_order.write({'product_uom_qty': 10})
# There is a bug with `new` and `_origin`
# If you create a first new from a record, then change a value on the origin record, than create another new,
# this other new wont have the updated value of the origin record, but the one from the previous new
# Here the problem lies in the use of `new` in `move = self_ctx.new(new_vals)`,
# and the fact this method is called multiple times in the same transaction test case.
# Here, we update `qty_delivered` on the origin record, but the `new` records which are in cache with this order line
# as origin are not updated, nor the fields that depends on it.
self.sol_serv_order.flush()
for field in self.env['sale.order.line']._fields.values():
for res_id in list(self.env.cache._data[field]):
if not res_id:
self.env.cache._data[field].pop(res_id)
invoice3 = self.sale_order._create_invoices()
self.assertEqual(len(invoice3.invoice_line_ids), 1, 'Sale: third invoice is missing lines')
+1 -1
View File
@@ -99,7 +99,7 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
self.assertEquals(len(self.sale_order.invoice_ids), 2, 'Invoice should be created for the SO')
invoice = self.sale_order.invoice_ids[0]
invoice = self.sale_order.invoice_ids.sorted()[0]
self.assertEquals(len(invoice.invoice_line_ids), len(self.sale_order.order_line), 'All lines should be invoiced')
self.assertEquals(invoice.amount_total, self.sale_order.amount_total - downpayment_line.price_unit, 'Downpayment should be applied')
@@ -47,7 +47,6 @@ class SaleCouponProgram(models.Model):
coupon_ids = fields.One2many('sale.coupon', 'program_id', string="Generated Coupons", copy=False)
coupon_count = fields.Integer(compute='_compute_coupon_count')
order_count = fields.Integer(compute='_compute_order_count')
order_line_ids = fields.Many2many('sale.order.line', store=False, search='_search_order_line_ids')
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company)
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
validity_duration = fields.Integer(default=1,
@@ -61,11 +60,7 @@ class SaleCouponProgram(models.Model):
if self.search(domain):
raise ValidationError(_('The program code must be unique!'))
def _search_order_line_ids(self, operator, arg):
# just a hack to enable the invalidation of 'order_count'
return []
@api.depends('order_line_ids.product_id')
# The api.depends is handled in `def modified` of `sale_coupon/models/sale_order.py`
def _compute_order_count(self):
product_data = self.env['sale.order.line'].read_group([('product_id', 'in', self.mapped('discount_line_product_id').ids)], ['product_id'], ['product_id'])
mapped_data = dict([(m['product_id'][0], m['product_id_count']) for m in product_data])
+19
View File
@@ -425,3 +425,22 @@ class SaleOrderLine(models.Model):
# If company_id is set, always filter taxes by the company
taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes
# Invalidation of `sale.coupon.program.order_count`
# `test_program_rules_validity_dates_and_uses`,
# Overriding modified is quite hardcore as you need to know how works the cache and the invalidation system,
# but at least the below works and should be efficient.
# Another possibility is to add on product.product a one2many to sale.order.line 'order_line_ids',
# and then add the depends @api.depends('discount_line_product_id.order_line_ids'),
# but I am not sure this will as efficient as the below.
def modified(self, fnames, modified=None, create=False):
super(SaleOrderLine, self).modified(fnames, modified=modified)
if 'product_id' in fnames:
Program = self.env['sale.coupon.program']
field_order_count = Program._fields['order_count']
programs = self.env.cache.get_records(Program, field_order_count)
if programs:
products = self.filtered('is_reward_line').mapped('product_id')
for program in programs:
if program.discount_line_product_id in products:
self.env.cache.invalidate([(field_order_count, program.ids)])
@@ -167,7 +167,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# large cabinet |
# --------------------------------------------------------------------------------
# TOTAL | 256.00 | 294.40 | 38.40
self.assertEqual(order.amount_total, 294.4, "Check discount has been applied correctly (eg: on taxes aswell)")
self.assertAlmostEqual(order.amount_total, 294.4, 2, "Check discount has been applied correctly (eg: on taxes aswell)")
# test coupon with code works the same as auto applied_programs
p_specific_product.write({'promo_code_usage': 'code_needed', 'promo_code': '20pc'})
@@ -208,7 +208,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# large cabinet |
# --------------------------------------------------------------------------------
# TOTAL | 2298.00 | 2605.20 | 305.20
self.assertEqual(order.amount_total, 2605.20, "Changing cabinet quantity should change discount amount correctly")
self.assertAlmostEqual(order.amount_total, 2605.20, 2, "Changing cabinet quantity should change discount amount correctly")
p_specific_product.discount_max_amount = 200
order.recompute_coupon_lines()
@@ -337,7 +337,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# --------------------------------------------------------------------------------
# TOTAL AFTER APPLYING FREE PRODUCT PROGRAMS | 1594.95 | 1901.11 | 306.16
self.assertEqual(order.amount_total, 1901.11, "The order total with programs should be 1901.11")
self.assertAlmostEqual(order.amount_total, 1901.11, 2, "The order total with programs should be 1901.11")
self.assertEqual(order.amount_untaxed, 1594.95, "The order untaxed total with programs should be 1594.95")
self.assertEqual(len(order.order_line.ids), 8, "Order should contains 5 regular product lines and 3 free product lines")
@@ -438,7 +438,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
# 20% on Large Cabinet | 1 | -80.00 | 15% excl | -80.00 | -92.00 | -12.00
# --------------------------------------------------------------------------------
# TOTAL | 1242.22 | 1509.11 | 266.89
self.assertEqual(order.amount_total, 1509.11, "The order total with programs should be 1509.11")
self.assertAlmostEqual(order.amount_total, 1509.11, 2, "The order total with programs should be 1509.11")
self.assertEqual(order.amount_untaxed, 1242.22, "The order untaxed total with programs should be 1242.22")
self.assertEqual(len(order.order_line.ids), 7, "Order should contains 7 lines: 4 products lines, 2 free products lines and a 20% discount line")
@@ -224,4 +224,4 @@ class TestSaleCouponProgramRules(TestSaleCouponCommon):
p_specific_product.discount_apply_on = 'cheapest_product'
order.recompute_coupon_lines()
# 872.73 - (20% of 1 iPad) = 872.73 - 58.18 = 814.55
self.assertEqual(order.amount_untaxed, 814.55, "One large cabinet should be discounted by 20%")
self.assertAlmostEqual(order.amount_untaxed, 814.55, 2, "One large cabinet should be discounted by 20%")
+2 -2
View File
@@ -540,7 +540,7 @@ class TestSaleMrpFlow(common.SavepointCase):
self.assertEqual(del_qty, 0.0, 'Sale MRP: delivered quantity should be zero after partial delivery of a kit')
# deliver remaining products, check the so's invoice_status and delivered quantities
self.assertEqual(len(so.picking_ids), 2, 'Sale MRP: number of pickings should be 2')
pick_2 = so.picking_ids[0]
pick_2 = so.picking_ids.filtered('backorder_id')
for move in pick_2.move_lines:
if move.product_id.id == self.env.ref('mrp.product_product_computer_desk_bolt').id:
move.write({'quantity_done': 19})
@@ -729,7 +729,7 @@ class TestSaleMrpFlow(common.SavepointCase):
self._assert_quantities(move_lines, expected_quantities)
# Process only x1 of the first component then create a backorder for the missing components
picking_original.move_lines[0].write({'quantity_done': 1})
picking_original.move_lines.sorted()[0].write({'quantity_done': 1})
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, so.picking_ids[0].id)]})
backorder_wizard.process()
+9 -2
View File
@@ -42,7 +42,7 @@ class SaleOrder(models.Model):
for order in self:
pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer')
dates_list = [date for date in pickings.mapped('date_done') if date]
order.effective_date = dates_list and min(dates_list).date()
order.effective_date = min(dates_list).date() if dates_list else False
@api.depends('picking_policy')
def _compute_expected_date(self):
@@ -235,6 +235,7 @@ class SaleOrderLine(models.Model):
date = confirm_date + timedelta(days=line.customer_lead or 0.0)
grouped_lines[(warehouse.id, date)] |= line
treated = self.browse()
for (warehouse, scheduled_date), lines in grouped_lines.items():
product_qties = lines.mapped('product_id').with_context(to_date=scheduled_date, warehouse=warehouse).read([
'qty_available',
@@ -252,6 +253,12 @@ class SaleOrderLine(models.Model):
line.free_qty_today = free_qty_today - qty_processed_per_product[line.product_id.id]
line.virtual_available_at_date = virtual_available_at_date - qty_processed_per_product[line.product_id.id]
qty_processed_per_product[line.product_id.id] += line.product_uom_qty
treated |= lines
remaining = (self - treated)
remaining.virtual_available_at_date = False
remaining.scheduled_date = False
remaining.free_qty_today = False
remaining.qty_available_today = False
@api.depends('product_id', 'route_id', 'order_id.warehouse_id')
def _compute_is_mto(self):
@@ -259,11 +266,11 @@ class SaleOrderLine(models.Model):
set 'is_available' at True if the product availibility in stock does
not need to be verified, which is the case in MTO, Cross-Dock or Drop-Shipping
"""
self.is_mto = False
for line in self:
if not line.display_qty_widget:
continue
product = line.product_id
line.is_mto = False
product_routes = line.route_id or (product.route_ids + product.categ_id.total_route_ids)
# Check MTO

Some files were not shown because too many files have changed in this diff Show More