[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:
committed by
Denis Ledoux
parent
e28d3aa4d3
commit
9920f20e4c
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 [])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. '''
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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%")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user