[MERGE] forward port branch saas-12.2 up to 498b4b4350
This commit is contained in:
@@ -446,6 +446,7 @@ class AccountBankStatementLine(models.Model):
|
||||
""" Prepare the dict of values to balance the move.
|
||||
|
||||
:param recordset move: the account.move to link the move line
|
||||
:param dict move: a dict of vals of a account.move which will be created later
|
||||
:param float amount: the amount of transaction that wasn't already reconciled
|
||||
"""
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
@@ -453,19 +454,23 @@ class AccountBankStatementLine(models.Model):
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
amount_currency = False
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
if isinstance(move, dict):
|
||||
amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
|
||||
else:
|
||||
amount_sum = sum(x.amount_currency for x in move.line_ids)
|
||||
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
|
||||
if st_line_currency != company_currency and st_line_currency == statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
amount_currency = -amount_sum
|
||||
elif st_line_currency != company_currency and statement_currency == company_currency:
|
||||
# company in currency A, statement in currency A and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
amount_currency = -amount_sum
|
||||
elif st_line_currency != company_currency and st_line_currency != statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency C
|
||||
# counterpart line must have currency B and use rate between B and C to compute correct amount
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
|
||||
amount_currency = -amount_sum/st_line_currency_rate
|
||||
elif st_line_currency == company_currency and statement_currency != company_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency A
|
||||
# counterpart line must have currency B and amount is computed using the rate between A and B
|
||||
@@ -474,9 +479,8 @@ class AccountBankStatementLine(models.Model):
|
||||
# last case is company in currency A, statement in currency A and transaction in currency A
|
||||
# and in this case counterpart line does not need any second currency nor amount_currency
|
||||
|
||||
return {
|
||||
aml_dict = {
|
||||
'name': self.name,
|
||||
'move_id': move.id,
|
||||
'partner_id': self.partner_id and self.partner_id.id or False,
|
||||
'account_id': amount >= 0 \
|
||||
and self.statement_id.journal_id.default_credit_account_id.id \
|
||||
@@ -487,22 +491,72 @@ class AccountBankStatementLine(models.Model):
|
||||
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
|
||||
'amount_currency': amount_currency,
|
||||
}
|
||||
if isinstance(move, self.env['account.move'].__class__):
|
||||
aml_dict['move_id'] = move.id
|
||||
return aml_dict
|
||||
|
||||
@api.multi
|
||||
def fast_counterpart_creation(self):
|
||||
"""This function is called when confirming a bank statement and will allow to automatically process lines without
|
||||
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
|
||||
entry using that account to counterpart the bank account
|
||||
"""
|
||||
payment_list = []
|
||||
move_list = []
|
||||
account_type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
|
||||
managed_st_line = []
|
||||
for st_line in self:
|
||||
# Technical functionality to automatically reconcile by creating a new move line
|
||||
if st_line.account_id and not st_line.journal_entry_ids.ids:
|
||||
vals = {
|
||||
if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
|
||||
managed_st_line.append(st_line.id)
|
||||
# Create payment vals
|
||||
total = st_line.amount
|
||||
payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
|
||||
currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
|
||||
partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
|
||||
payment_list.append({
|
||||
'payment_method_id': payment_methods and payment_methods[0].id or False,
|
||||
'payment_type': total > 0 and 'inbound' or 'outbound',
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'partner_type': partner_type,
|
||||
'journal_id': st_line.statement_id.journal_id.id,
|
||||
'payment_date': st_line.date,
|
||||
'state': 'reconciled',
|
||||
'currency_id': currency.id,
|
||||
'amount': abs(total),
|
||||
'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
|
||||
'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
|
||||
})
|
||||
|
||||
# Create move and move line vals
|
||||
move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
|
||||
aml_dict = {
|
||||
'name': st_line.name,
|
||||
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
|
||||
'credit': st_line.amount > 0 and st_line.amount or 0.0,
|
||||
'account_id': st_line.account_id.id,
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'statement_line_id': st_line.id,
|
||||
}
|
||||
st_line.process_reconciliation(new_aml_dicts=[vals])
|
||||
st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
|
||||
move_vals['line_ids'] = [(0, 0, aml_dict)]
|
||||
balance_line = self._prepare_reconciliation_move_line(
|
||||
move_vals, -aml_dict['debit'] if st_line.amount < 0 else aml_dict['credit'])
|
||||
move_vals['line_ids'].append((0, 0, balance_line))
|
||||
move_list.append(move_vals)
|
||||
|
||||
# Creates
|
||||
payment_ids = self.env['account.payment'].create(payment_list)
|
||||
for payment_id, move_vals in zip(payment_ids, move_list):
|
||||
for line in move_vals['line_ids']:
|
||||
line[2]['payment_id'] = payment_id.id
|
||||
move_ids = self.env['account.move'].create(move_list)
|
||||
move_ids.post()
|
||||
|
||||
for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
|
||||
st_line.write({'move_name': move.name})
|
||||
payment.write({'payment_reference': move.name})
|
||||
|
||||
def _get_communication(self, payment_method_id):
|
||||
return self.name or ''
|
||||
@@ -630,8 +684,6 @@ class AccountBankStatementLine(models.Model):
|
||||
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
|
||||
# case we reconcile the existing and the new move lines together, or being a write-off.
|
||||
if counterpart_aml_dicts or new_aml_dicts:
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
|
||||
# Create the move
|
||||
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
|
||||
@@ -654,32 +706,12 @@ class AccountBankStatementLine(models.Model):
|
||||
|
||||
# Complete dicts to create both counterpart move lines and write-offs
|
||||
to_create = (counterpart_aml_dicts + new_aml_dicts)
|
||||
company = self.company_id
|
||||
date = self.date or fields.Date.today()
|
||||
for aml_dict in to_create:
|
||||
aml_dict['move_id'] = move.id
|
||||
aml_dict['partner_id'] = self.partner_id.id
|
||||
aml_dict['statement_line_id'] = self.id
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
self._prepare_move_line_for_currency(aml_dict, date)
|
||||
|
||||
# Create write-offs
|
||||
for aml_dict in new_aml_dicts:
|
||||
@@ -729,6 +761,36 @@ class AccountBankStatementLine(models.Model):
|
||||
counterpart_moves.assert_balanced()
|
||||
return counterpart_moves
|
||||
|
||||
@api.multi
|
||||
def _prepare_move_line_for_currency(self, aml_dict, date):
|
||||
self.ensure_one()
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
statement_currency = self.journal_id.currency_id or company_currency
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
company = self.company_id
|
||||
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
|
||||
def _check_invoice_state(self, invoice):
|
||||
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
|
||||
invoice.write({'state': 'paid'})
|
||||
|
||||
@@ -93,11 +93,12 @@ class AccountInvoice(models.Model):
|
||||
('type', 'in', [TYPE2JOURNAL[ty] for ty in inv_types if ty in TYPE2JOURNAL]),
|
||||
('company_id', '=', company_id),
|
||||
]
|
||||
journal_with_currency = False
|
||||
if self._context.get('default_currency_id'):
|
||||
currency_clause = [('currency_id', '=', self._context.get('default_currency_id'))]
|
||||
journal_with_currency = self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
return journal_with_currency or self.env['account.journal'].search(domain, limit=1)
|
||||
company_currency_id = self.env['res.company'].browse(company_id).currency_id.id
|
||||
currency_id = self._context.get('default_currency_id') or company_currency_id
|
||||
currency_clause = [('currency_id', '=', currency_id)]
|
||||
if currency_id == company_currency_id:
|
||||
currency_clause = ['|', ('currency_id', '=', False)] + currency_clause
|
||||
return self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
|
||||
@api.model
|
||||
def _default_currency(self):
|
||||
|
||||
@@ -87,12 +87,14 @@ class AccountMove(models.Model):
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
amount = abs(line.debit - line.credit)
|
||||
total_amount += amount
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
precision_currency = move.currency_id or move.company_id.currency_id
|
||||
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
|
||||
move.matched_percentage = 1.0
|
||||
else:
|
||||
for line in move.line_ids:
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
move.matched_percentage = total_reconciled / total_amount
|
||||
|
||||
@api.multi
|
||||
@@ -432,6 +434,7 @@ class AccountMove(models.Model):
|
||||
@api.multi
|
||||
def _reverse_move(self, date=None, journal_id=None):
|
||||
self.ensure_one()
|
||||
date = date or fields.Date.today()
|
||||
with self.env.norecompute():
|
||||
reversed_move = self.copy(default={
|
||||
'date': date,
|
||||
@@ -692,8 +695,8 @@ class AccountMoveLine(models.Model):
|
||||
tax_audit = fields.Char(string="Tax Audit String", compute="_compute_tax_audit", store=True, help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.")
|
||||
|
||||
_sql_constraints = [
|
||||
('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'),
|
||||
('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
|
||||
('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry! Credit or debit should be zero.'),
|
||||
('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry! Credit and debit should be positive.'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
@@ -827,22 +830,21 @@ class AccountMoveLine(models.Model):
|
||||
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
|
||||
"""
|
||||
# Get first all aml involved
|
||||
part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
|
||||
amls = self
|
||||
todo = set(part_recs)
|
||||
todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
|
||||
amls = set(self.ids)
|
||||
seen = set()
|
||||
while todo:
|
||||
partial_rec = todo.pop()
|
||||
seen.add(partial_rec)
|
||||
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
|
||||
if aml not in amls:
|
||||
amls += aml
|
||||
for x in aml.matched_debit_ids | aml.matched_credit_ids:
|
||||
if x not in seen:
|
||||
todo.add(x)
|
||||
partial_rec_ids = [x.id for x in seen]
|
||||
aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
|
||||
amls |= set(aml_ids)
|
||||
seen |= set([rec['id'] for rec in todo])
|
||||
todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
|
||||
|
||||
partial_rec_ids = list(seen)
|
||||
if not amls:
|
||||
return
|
||||
else:
|
||||
amls = self.browse(list(amls))
|
||||
|
||||
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
|
||||
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
|
||||
multiple_currency = False
|
||||
|
||||
@@ -96,7 +96,7 @@ class AccountInvoiceReport(models.Model):
|
||||
sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id,
|
||||
sub.fiscal_position_id, sub.user_id, sub.company_id, sub.nbr, sub.invoice_id, sub.type, sub.state,
|
||||
sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
|
||||
sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total as amount_total,
|
||||
sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total / COALESCE(cr.rate, 1) as amount_total,
|
||||
COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id
|
||||
"""
|
||||
return select_str
|
||||
@@ -114,8 +114,7 @@ class AccountInvoiceReport(models.Model):
|
||||
ai.partner_bank_id,
|
||||
SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty,
|
||||
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
|
||||
ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
|
||||
count(*) * invoice_type.sign AS amount_total,
|
||||
SUM(ail.price_total * invoice_type.sign_qty) AS amount_total,
|
||||
SUM(ABS(ail.price_subtotal_signed)) / CASE
|
||||
WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric
|
||||
THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1))
|
||||
|
||||
@@ -281,7 +281,6 @@ var StatementModel = BasicModel.extend({
|
||||
if (last && !this._isValid(last)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
prop = this._formatQuickCreate(line);
|
||||
line.reconciliation_proposition.push(prop);
|
||||
line.createForm = _.pick(prop, this.quickCreateFields);
|
||||
@@ -493,13 +492,6 @@ var StatementModel = BasicModel.extend({
|
||||
this._blurProposition(handle);
|
||||
var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields));
|
||||
focus.reconcileModelId = reconcileModelId;
|
||||
if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.to_check;})) {
|
||||
new CrashManager().show_warning({data: {
|
||||
exception_type: _t("Incorrect Operation"),
|
||||
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
|
||||
}});
|
||||
return Promise.resolve();
|
||||
}
|
||||
line.reconciliation_proposition.push(focus);
|
||||
if (reconcileModel.has_second_line) {
|
||||
var second = {};
|
||||
@@ -619,19 +611,16 @@ var StatementModel = BasicModel.extend({
|
||||
var self = this;
|
||||
var line = this.getLine(handle);
|
||||
var prop = _.last(_.filter(line.reconciliation_proposition, '__focus'));
|
||||
if ('to_check' in values && values.to_check === false) {
|
||||
// check if we have another line with to_check and if yes don't change value of this proposition
|
||||
prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) {
|
||||
return rec_prop.id !== prop.id && rec_prop.to_check;
|
||||
});
|
||||
}
|
||||
if (!prop) {
|
||||
prop = this._formatQuickCreate(line);
|
||||
line.reconciliation_proposition.push(prop);
|
||||
}
|
||||
if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
|
||||
new CrashManager().show_warning({data: {
|
||||
exception_type: _t("Incorrect Operation"),
|
||||
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
|
||||
}});
|
||||
// FIXME: model should not be tied to the DOM !
|
||||
$('.create_to_check input:visible').prop('checked', !values.to_check).change();
|
||||
return Promise.resolve();
|
||||
}
|
||||
_.each(values, function (value, fieldName) {
|
||||
if (fieldName === 'analytic_tag_ids') {
|
||||
switch (value.operation) {
|
||||
@@ -683,7 +672,6 @@ var StatementModel = BasicModel.extend({
|
||||
prop.__tax_to_recompute = true;
|
||||
}
|
||||
line.createForm = _.pick(prop, this.quickCreateFields);
|
||||
|
||||
// If you check/uncheck the force_tax_included box, reset the createForm amount.
|
||||
if(prop.base_amount)
|
||||
line.createForm.amount = prop.base_amount;
|
||||
@@ -744,16 +732,8 @@ var StatementModel = BasicModel.extend({
|
||||
"new_aml_dicts": _.map(_.filter(props, function (prop) {
|
||||
return isNaN(prop.id) && prop.display;
|
||||
}), self._formatToProcessReconciliation.bind(self, line)),
|
||||
"to_check": line.to_check,
|
||||
};
|
||||
line.reconciliation_proposition.some(function(prop) {
|
||||
if (prop.to_check) {
|
||||
values_dict['to_check'] = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (line.reconciliation_proposition[0].to_check) {
|
||||
values_dict['to_check'] = true;
|
||||
}
|
||||
|
||||
// If the lines are not fully balanced, create an unreconciled amount.
|
||||
// line.st_line.currency_id is never false here because its equivalent to
|
||||
@@ -870,7 +850,12 @@ var StatementModel = BasicModel.extend({
|
||||
var formatOptions = {
|
||||
currency_id: line.st_line.currency_id,
|
||||
};
|
||||
line.to_check = false;
|
||||
_.each(line.reconciliation_proposition, function (prop) {
|
||||
if (prop.to_check) {
|
||||
// If one of the proposition is to_check, set the global to_check flag to true
|
||||
line.to_check = true;
|
||||
}
|
||||
if (prop.is_tax) {
|
||||
if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) {
|
||||
reconciliation_proposition.push(prop);
|
||||
@@ -1025,6 +1010,7 @@ var StatementModel = BasicModel.extend({
|
||||
prop.label = prop.name;
|
||||
prop.account_id = self._formatNameGet(prop.account_id || line.account_id);
|
||||
prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str;
|
||||
prop.to_check = !!prop.to_check;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1160,7 +1146,7 @@ var StatementModel = BasicModel.extend({
|
||||
'link': values.link,
|
||||
'display': true,
|
||||
'invalid': true,
|
||||
'to_check': values.to_check,
|
||||
'to_check': !!values.to_check,
|
||||
'__tax_to_recompute': true,
|
||||
'is_tax': values.is_tax,
|
||||
'__focus': '__focus' in values ? values.__focus : true,
|
||||
@@ -1406,11 +1392,8 @@ var ManualModel = StatementModel.extend({
|
||||
return self.loadData(lines);
|
||||
});
|
||||
default:
|
||||
var partner_ids = context.partner_ids;
|
||||
var account_ids = context.account_ids || self.account_ids;
|
||||
if (partner_ids && !account_ids) account_ids = [];
|
||||
if (!partner_ids && account_ids) partner_ids = [];
|
||||
account_ids = null; // TOFIX: REMOVE ME
|
||||
var partner_ids = context.partner_ids || null;
|
||||
var account_ids = context.account_ids || self.account_ids || null;
|
||||
return self._rpc({
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_all_data_for_manual_reconciliation',
|
||||
@@ -1627,6 +1610,7 @@ var ManualModel = StatementModel.extend({
|
||||
prop.debit = prop.debit !== 0 ? 0 : tmp_value;
|
||||
prop.amount = -prop.amount;
|
||||
prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id);
|
||||
prop.to_check = !!prop.to_check;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -346,7 +346,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
update: function (state) {
|
||||
var self = this;
|
||||
// isValid
|
||||
var to_check_checked = !!(state.reconciliation_proposition[0] && state.reconciliation_proposition[0].to_check);
|
||||
var to_check_checked = !!(state.to_check);
|
||||
this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked);
|
||||
this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked);
|
||||
this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0);
|
||||
@@ -476,9 +476,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (state.to_check) {
|
||||
// Set the to_check field to true if global to_check is set
|
||||
this.$('.create_to_check input').prop('checked', state.to_check).change();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
|
||||
},
|
||||
|
||||
@@ -477,7 +477,7 @@ var auto_reconciliation = {
|
||||
};
|
||||
|
||||
var data_for_manual_reconciliation_widget = {
|
||||
'[[],null]': {
|
||||
'[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]': {
|
||||
'customers': [
|
||||
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'mode': "customers"},
|
||||
{'account_id': 7, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'mode': "customers"}
|
||||
@@ -744,7 +744,7 @@ QUnit.module('account', {
|
||||
"name": "INV/2017/0003",
|
||||
"analytic_tag_ids": [[6, null, []]]
|
||||
}],
|
||||
payment_aml_ids: [], new_aml_dicts: []}]],
|
||||
payment_aml_ids: [], new_aml_dicts: [], to_check: false}]],
|
||||
"Should call process_bank_statement_line with ids");
|
||||
var def = testUtils.makeTestPromise();
|
||||
def.abort = function () {};
|
||||
@@ -782,8 +782,10 @@ QUnit.module('account', {
|
||||
// line is send back to server.
|
||||
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
|
||||
assert.deepEqual(event.data.args[1].args,
|
||||
[[5],[{partner_id: 8, counterpart_aml_dicts: [],
|
||||
payment_aml_ids: [], new_aml_dicts: [{
|
||||
[[5],[{partner_id: 8, to_check: false, counterpart_aml_dicts: [],
|
||||
payment_aml_ids: [],
|
||||
to_check: false,
|
||||
new_aml_dicts: [{
|
||||
account_id: 287,
|
||||
credit: 1175,
|
||||
debit: 0,
|
||||
@@ -834,7 +836,7 @@ QUnit.module('account', {
|
||||
// We also create a line which is the open balance.
|
||||
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
|
||||
assert.deepEqual(event.data.args[1].args,
|
||||
[[5],[{partner_id: 8,
|
||||
[[5],[{partner_id: 8, to_check: false,
|
||||
counterpart_aml_dicts: [{
|
||||
counterpart_aml_id: 109,
|
||||
credit: 650,
|
||||
@@ -884,6 +886,7 @@ QUnit.module('account', {
|
||||
}],
|
||||
payment_aml_ids: [],
|
||||
new_aml_dicts: [],
|
||||
to_check: false,
|
||||
}]
|
||||
], "should call process_bank_statement_line with partial reconcile values");
|
||||
}
|
||||
@@ -1408,6 +1411,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: []}]
|
||||
], "should call process_bank_statement_line with partial reconcile values");
|
||||
}
|
||||
@@ -1500,6 +1504,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: [
|
||||
{account_id: 282,
|
||||
credit: 0,
|
||||
@@ -1609,6 +1614,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: []
|
||||
}]
|
||||
], "should call process_bank_statement_line with correct counterpart_aml_dicts");
|
||||
@@ -1662,7 +1668,7 @@ QUnit.module('account', {
|
||||
assert.expect(5);
|
||||
|
||||
// tweak the data to fit our needs
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[[],null]']);
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]']);
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [
|
||||
{account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16",
|
||||
total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait",
|
||||
@@ -1772,7 +1778,7 @@ QUnit.module('account', {
|
||||
QUnit.test('Tax on account receivable', async function(assert){
|
||||
assert.expect(21);
|
||||
|
||||
this.params.data_for_manual_reconciliation_widget['[[],null]'].accounts = [];
|
||||
this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]'].accounts = [];
|
||||
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
|
||||
testUtils.mock.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
|
||||
@@ -13,6 +13,8 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
self.bsl_model = self.env['account.bank.statement.line']
|
||||
self.reconciliation_widget = self.env['account.reconciliation.widget']
|
||||
self.partner = self.env['res.partner'].create({'name': 'test'})
|
||||
self.currency_usd_id = self.env.ref("base.USD").id
|
||||
self.currency_euro_id = self.env.ref("base.EUR").id
|
||||
|
||||
def test_reconciliation_proposition(self):
|
||||
rcv_mv_line = self.create_invoice(100)
|
||||
@@ -108,3 +110,31 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
})
|
||||
|
||||
return bank_stmt_line
|
||||
|
||||
def test_confirm_statement_usd(self):
|
||||
company = self.env.ref('base.main_company')
|
||||
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_usd_id,
|
||||
'rate': 2.0,
|
||||
'name': '2001-01-01',
|
||||
})
|
||||
bank_journal_usd = self.env['account.journal'].create({
|
||||
'name': 'Bank US',
|
||||
'type': 'bank',
|
||||
'code': 'BNK68',
|
||||
'currency_id': self.currency_usd_id,
|
||||
})
|
||||
statement = self.bs_model.create({
|
||||
'journal_id': bank_journal_usd.id,
|
||||
'balance_end_real': 100,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': '_',
|
||||
'partner_id': self.partner.id,
|
||||
'amount': 100,
|
||||
'account_id': bank_journal_usd.default_debit_account_id.id,
|
||||
})],
|
||||
})
|
||||
statement.button_open()
|
||||
statement.button_confirm_bank()
|
||||
|
||||
@@ -11,6 +11,7 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
_description = 'Account Invoice Send'
|
||||
|
||||
is_email = fields.Boolean('Email', default=lambda self: self.env.user.company_id.invoice_is_email)
|
||||
invoice_without_email = fields.Text(compute='_compute_invoice_without_email', string='invoice(s) that will not be sent')
|
||||
is_print = fields.Boolean('Print', default=lambda self: self.env.user.company_id.invoice_is_print)
|
||||
printed = fields.Boolean('Is Printed', default=False)
|
||||
invoice_ids = fields.Many2many('account.invoice', 'account_invoice_account_invoice_send_rel', string='Invoices')
|
||||
@@ -45,6 +46,22 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
self.composer_id.template_id = self.template_id.id
|
||||
self.composer_id.onchange_template_id_wrapper()
|
||||
|
||||
@api.onchange('is_email')
|
||||
def _compute_invoice_without_email(self):
|
||||
for wizard in self:
|
||||
if wizard.is_email and len(wizard.invoice_ids) > 1:
|
||||
invoices = self.env['account.invoice'].search([
|
||||
('id', 'in', self.env.context.get('active_ids')),
|
||||
('partner_id.email', '=', False)
|
||||
])
|
||||
if invoices:
|
||||
wizard.invoice_without_email = "%s\n%s" % (
|
||||
_("The following invoice(s) will not be sent by email, because the customers don't have email address."),
|
||||
"\n".join([i.reference for i in invoices])
|
||||
)
|
||||
else:
|
||||
wizard.invoice_without_email = False
|
||||
|
||||
@api.multi
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
<field name="is_email" />
|
||||
<b><label for="is_email"/></b>
|
||||
</div>
|
||||
<div class="text-left d-inline-block mr8" attrs="{'invisible': ['|', ('is_email','=', False), ('invoice_without_email', '=', False)]}">
|
||||
<field name="invoice_without_email" class="mr4"/>
|
||||
</div>
|
||||
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
|
||||
<!-- visible wizard -->
|
||||
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
<div class="o_row">
|
||||
<field name="end_type" attrs="{'required': [('recurrency', '=', True)]}"/>
|
||||
<field name="count" attrs="{'invisible': [('end_type', '!=', 'count')], 'required': [('recurrency', '=', True)]}"/>
|
||||
<field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date')]}"/>
|
||||
<field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date'), ('recurrency', '=', True)]}"/>
|
||||
</div>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}" name="weekdays">
|
||||
@@ -182,10 +182,10 @@
|
||||
attrs="{'required': [('month_by', '=', 'date'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'date')]}"/>
|
||||
<field name="byday" string="The"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'day')]}"/>
|
||||
<field name="week_list" nolabel="1"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'day')]}"/>
|
||||
</div>
|
||||
</group>
|
||||
|
||||
@@ -253,7 +253,7 @@ class Lead(models.Model):
|
||||
return {}
|
||||
if user_id and self._context.get('team_id'):
|
||||
team = self.env['crm.team'].browse(self._context['team_id'])
|
||||
if user_id in team.member_ids.ids:
|
||||
if user_id in team.member_ids.ids or user_id == team.user_id.id:
|
||||
return {}
|
||||
team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
|
||||
return {'team_id': team_id}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<record id="rank_doctor" model="gamification.karma.rank">
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank and gain a week-end with a unicorn !</div>
|
||||
<div class="media-body">You have reached the last rank. Congratulations!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.png"/>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -81,7 +81,7 @@ class SyncEvent(object):
|
||||
self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
|
||||
'The event has been deleted from one side, we delete on other side !')
|
||||
#If event is not deleted !
|
||||
elif self.OE.status and (self.GG.status or not is_owner):
|
||||
elif self.OE.status and (self.GG.status or (not is_owner and self.GG.update)):
|
||||
if abs(self.OE.update - self.GG.update) > timedelta(seconds=1):
|
||||
if self.OE.update < self.GG.update:
|
||||
tmpSrc = 'GG'
|
||||
|
||||
@@ -35,7 +35,7 @@ class GoogleDrive(models.Model):
|
||||
if not modifiers.get('invisible') and not modifiers.get('column_invisible'):
|
||||
display_fields.append(node.get('name'))
|
||||
fields = " ".join(display_fields)
|
||||
domain = domain.replace("'", r"\'").replace('"', "'")
|
||||
domain = domain.replace("'", r"\'").replace('"', "'").replace('True', 'true').replace('False', 'false')
|
||||
if groupbys:
|
||||
fields = "%s %s" % (groupbys, fields)
|
||||
formula = '=oe_read_group("%s";"%s";"%s";"%s")' % (model, fields, groupbys, domain)
|
||||
|
||||
@@ -21,7 +21,7 @@ class User(models.Model):
|
||||
department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False)
|
||||
address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False)
|
||||
work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False)
|
||||
parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
|
||||
employee_parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
|
||||
coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False)
|
||||
address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False)
|
||||
is_address_home_a_company = fields.Boolean(related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False)
|
||||
@@ -59,7 +59,7 @@ class User(models.Model):
|
||||
'child_ids',
|
||||
'employee_id',
|
||||
'employee_ids',
|
||||
'parent_id',
|
||||
'employee_parent_id',
|
||||
]
|
||||
|
||||
hr_writable_fields = [
|
||||
@@ -87,7 +87,7 @@ class User(models.Model):
|
||||
'marital',
|
||||
'mobile_phone',
|
||||
'notes',
|
||||
'parent_id',
|
||||
'employee_parent_id',
|
||||
'passport_id',
|
||||
'permit_no',
|
||||
'employee_phone',
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<field name="work_location"/>
|
||||
</group>
|
||||
<group name="managers" string="Managers">
|
||||
<field name="parent_id"/>
|
||||
<field name="employee_parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
</group>
|
||||
</div>
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
<field name="res_model">hr.employee</field>
|
||||
<field name="view_mode">kanban,tree,activity</field>
|
||||
<field name="view_id" ref="hr_employees_view_kanban"/>
|
||||
<field name="target">fullscreen</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new employee
|
||||
|
||||
@@ -800,12 +800,12 @@ class HrExpenseSheet(models.Model):
|
||||
return self.employee_id.parent_id.user_id
|
||||
elif self.employee_id.department_id.manager_id.user_id:
|
||||
return self.employee_id.department_id.manager_id.user_id
|
||||
return self.env.user
|
||||
return self.env['res.users']
|
||||
|
||||
def activity_update(self):
|
||||
for expense_report in self.filtered(lambda hol: hol.state == 'submit'):
|
||||
self.activity_schedule(
|
||||
'hr_expense.mail_act_expense_approval',
|
||||
user_id=expense_report.sudo()._get_responsible_for_approval().id)
|
||||
user_id=expense_report.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
self.filtered(lambda hol: hol.state == 'approve').activity_feedback(['hr_expense.mail_act_expense_approval'])
|
||||
self.filtered(lambda hol: hol.state == 'cancel').activity_unlink(['hr_expense.mail_act_expense_approval'])
|
||||
|
||||
@@ -266,6 +266,11 @@ class HolidaysRequest(models.Model):
|
||||
self.request_unit_hours = False
|
||||
self.request_unit_custom = False
|
||||
|
||||
@api.onchange('user_id')
|
||||
def _onchange_user_id(self):
|
||||
if not self.out_of_office_message:
|
||||
self.out_of_office_message = self.user_id.out_of_office_message
|
||||
|
||||
@api.onchange('request_date_from_period', 'request_hour_from', 'request_hour_to',
|
||||
'request_date_from', 'request_date_to',
|
||||
'employee_id')
|
||||
@@ -340,7 +345,7 @@ class HolidaysRequest(models.Model):
|
||||
def _onchange_type(self):
|
||||
if self.holiday_type == 'employee':
|
||||
if not self.employee_id:
|
||||
self.employee_id = self.env.user.employee_ids[:1].id
|
||||
self.employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1).id
|
||||
self.mode_company_id = False
|
||||
self.category_id = False
|
||||
elif self.holiday_type == 'company':
|
||||
@@ -353,7 +358,7 @@ class HolidaysRequest(models.Model):
|
||||
self.mode_company_id = False
|
||||
self.category_id = False
|
||||
if not self.department_id:
|
||||
self.department_id = self.env.user.employee_ids[:1].department_id.id
|
||||
self.department_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1).department_id.id
|
||||
elif self.holiday_type == 'category':
|
||||
self.employee_id = False
|
||||
self.mode_company_id = False
|
||||
@@ -384,11 +389,15 @@ class HolidaysRequest(models.Model):
|
||||
for holiday in self:
|
||||
holiday.number_of_days_display = holiday.number_of_days
|
||||
|
||||
def _get_calendar(self):
|
||||
self.ensure_one()
|
||||
return self.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id
|
||||
|
||||
@api.multi
|
||||
@api.depends('number_of_days')
|
||||
def _compute_number_of_hours_display(self):
|
||||
for holiday in self:
|
||||
calendar = holiday.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id
|
||||
calendar = holiday._get_calendar()
|
||||
if holiday.date_from and holiday.date_to:
|
||||
number_of_hours = calendar.get_work_hours_count(holiday.date_from, holiday.date_to)
|
||||
holiday.number_of_hours_display = number_of_hours or (holiday.number_of_days * HOURS_PER_DAY)
|
||||
@@ -572,6 +581,10 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
# Allow an employee to always write his own out of office message
|
||||
if len(self) == 1 and values.keys() == {'out_of_office_message'} and self.employee_id.user_id == self.env.user:
|
||||
return super(HolidaysRequest, self.sudo()).write(values)
|
||||
|
||||
employee_id = values.get('employee_id', False)
|
||||
if not self.env.context.get('leave_fast_create') and values.get('state'):
|
||||
self._check_approval_update(values['state'])
|
||||
@@ -823,7 +836,7 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
self.ensure_one()
|
||||
responsible = self.env.user
|
||||
responsible = self.env['res.users']
|
||||
|
||||
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
|
||||
if self.holiday_status_id.responsible_id:
|
||||
@@ -846,12 +859,12 @@ class HolidaysRequest(models.Model):
|
||||
elif holiday.state == 'confirm':
|
||||
holiday.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_approval',
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id)
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif holiday.state == 'validate1':
|
||||
holiday.activity_feedback(['hr_holidays.mail_act_leave_approval'])
|
||||
holiday.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_second_approval',
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id)
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif holiday.state == 'validate':
|
||||
to_do |= holiday
|
||||
elif holiday.state == 'refuse':
|
||||
|
||||
@@ -550,7 +550,7 @@ class HolidaysAllocation(models.Model):
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
self.ensure_one()
|
||||
responsible = self.env.user
|
||||
responsible = self.env['res.users']
|
||||
|
||||
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
|
||||
if self.holiday_status_id.responsible_id:
|
||||
@@ -570,12 +570,12 @@ class HolidaysAllocation(models.Model):
|
||||
elif allocation.state == 'confirm':
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_approval',
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id)
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate1':
|
||||
allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_second_approval',
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id)
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate':
|
||||
to_do |= allocation
|
||||
elif allocation.state == 'refuse':
|
||||
|
||||
@@ -4,26 +4,29 @@
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.tests.common import tagged, users, warmup
|
||||
from odoo.tests.common import tagged, users, warmup, Form
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
@tagged('out_of_office')
|
||||
class TestOutOfOffice(TestHrHolidaysBase):
|
||||
|
||||
def test_leave_ooo(self):
|
||||
leave_type = self.env['hr.leave.type'].create({
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
})
|
||||
|
||||
def test_leave_ooo(self):
|
||||
self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
|
||||
self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
|
||||
leave_date_end = (datetime.today() + relativedelta(days=3))
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': 'Christmas',
|
||||
'employee_id': self.employee_hruser.id,
|
||||
'holiday_status_id': leave_type.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': (datetime.today() - relativedelta(days=1)),
|
||||
'date_to': leave_date_end,
|
||||
'out_of_office_message': 'contact tde in case of problems',
|
||||
@@ -51,6 +54,29 @@ class TestOutOfOffice(TestHrHolidaysBase):
|
||||
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_date_end'], leave_date_end)
|
||||
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'contact tde in case of problems')
|
||||
|
||||
def test_leave_ooo_use_default(self):
|
||||
""" Out of office message from Preferences should be used as default value """
|
||||
self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
|
||||
|
||||
leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
|
||||
leave_form.date_from = datetime.today() - relativedelta(days=1)
|
||||
leave_form.date_to = datetime.today()
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave = leave_form.save()
|
||||
self.assertEqual(leave.out_of_office_message, 'contact xdo in case of problems')
|
||||
|
||||
def test_leave_ooo_overwrite_default(self):
|
||||
""" Out of office message default value should be overwrittable """
|
||||
self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
|
||||
|
||||
leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
|
||||
leave_form.date_from = datetime.today() - relativedelta(days=1)
|
||||
leave_form.date_to = datetime.today()
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.out_of_office_message = 'contact tde in case of problems'
|
||||
leave = leave_form.save()
|
||||
self.assertEqual(leave.out_of_office_message, 'contact tde in case of problems')
|
||||
|
||||
|
||||
@tagged('out_of_office')
|
||||
class TestOutOfOfficePerformance(TestHrHolidaysBase):
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
</div>
|
||||
|
||||
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="out_of_office_message"/>
|
||||
</group>
|
||||
<group name="col_right">
|
||||
|
||||
@@ -44,7 +44,7 @@ def _guess_mimetype(ext=False, default='text/html'):
|
||||
return ext is not False and exts.get(ext, default) or exts
|
||||
|
||||
|
||||
def slugify_one(s, max_length=None):
|
||||
def slugify_one(s, max_length=0):
|
||||
""" Transform a string to a slug that can be used in a url path.
|
||||
This method will first try to do the job with python-slugify if present.
|
||||
Otherwise it will process string by stripping leading and ending spaces,
|
||||
@@ -62,13 +62,12 @@ def slugify_one(s, max_length=None):
|
||||
except TypeError:
|
||||
pass
|
||||
uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
|
||||
slug_str = re.sub('[\W_]', ' ', uni).strip().lower()
|
||||
slug_str = re.sub('[-\s]+', '-', slug_str)
|
||||
|
||||
return slug_str[:max_length]
|
||||
slug_str = re.sub(r'[\W_]', ' ', uni).strip().lower()
|
||||
slug_str = re.sub(r'[-\s]+', '-', slug_str)
|
||||
return slug_str[:max_length] if max_length > 0 else slug_str
|
||||
|
||||
|
||||
def slugify(s, max_length=None, path=False):
|
||||
def slugify(s, max_length=0, path=False):
|
||||
if not path:
|
||||
return slugify_one(s, max_length=max_length)
|
||||
else:
|
||||
|
||||
@@ -2077,212 +2077,230 @@
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_04_new' model='account.account.tag'>
|
||||
<field name='name'>401.04 Ventas y/o servicios gravados realizados en zona fronteriza norte</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_05_new' model='account.account.tag'>
|
||||
<field name='name'>401.05 Ventas y/o servicios gravados realizados en zona fronteriza norte de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_06_new' model='account.account.tag'>
|
||||
<field name='name'>401.06 Ventas y/o servicios gravados realizados en zona fronteriza norte a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_04' model='account.account.tag'>
|
||||
<field name='name'>401.04 Ventas y/o servicios gravados al 0%</field>
|
||||
<field name='name'>401.07 Ventas y/o servicios gravados al 0%</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_05' model='account.account.tag'>
|
||||
<field name='name'>401.05 Ventas y/o servicios gravados al 0% de contado</field>
|
||||
<field name='name'>401.08 Ventas y/o servicios gravados al 0% de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_06' model='account.account.tag'>
|
||||
<field name='name'>401.06 Ventas y/o servicios gravados al 0% a crédito</field>
|
||||
<field name='name'>401.09 Ventas y/o servicios gravados al 0% a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_07' model='account.account.tag'>
|
||||
<field name='name'>401.07 Ventas y/o servicios exentos</field>
|
||||
<field name='name'>401.10 Ventas y/o servicios exentos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_08' model='account.account.tag'>
|
||||
<field name='name'>401.08 Ventas y/o servicios exentos de contado</field>
|
||||
<field name='name'>401.11 Ventas y/o servicios exentos de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_09' model='account.account.tag'>
|
||||
<field name='name'>401.09 Ventas y/o servicios exentos a crédito</field>
|
||||
<field name='name'>401.12 Ventas y/o servicios exentos a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_10' model='account.account.tag'>
|
||||
<field name='name'>401.10 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
|
||||
<field name='name'>401.13 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_11' model='account.account.tag'>
|
||||
<field name='name'>401.11 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.14 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_12' model='account.account.tag'>
|
||||
<field name='name'>401.12 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
|
||||
<field name='name'>401.15 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_13' model='account.account.tag'>
|
||||
<field name='name'>401.13 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.16 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_14' model='account.account.tag'>
|
||||
<field name='name'>401.14 Ventas y/o servicios exentos nacionales partes relacionadas</field>
|
||||
<field name='name'>401.17 Ventas y/o servicios exentos nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_15' model='account.account.tag'>
|
||||
<field name='name'>401.15 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.18 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_16' model='account.account.tag'>
|
||||
<field name='name'>401.16 Ingresos por servicios administrativos</field>
|
||||
<field name='name'>401.19 Ingresos por servicios administrativos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_17' model='account.account.tag'>
|
||||
<field name='name'>401.17 Ingresos por servicios administrativos nacionales partes relacionadas</field>
|
||||
<field name='name'>401.20 Ingresos por servicios administrativos nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_18' model='account.account.tag'>
|
||||
<field name='name'>401.18 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.21 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_19' model='account.account.tag'>
|
||||
<field name='name'>401.19 Ingresos por servicios profesionales</field>
|
||||
<field name='name'>401.22 Ingresos por servicios profesionales</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_20' model='account.account.tag'>
|
||||
<field name='name'>401.20 Ingresos por servicios profesionales nacionales partes relacionadas</field>
|
||||
<field name='name'>401.23 Ingresos por servicios profesionales nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_21' model='account.account.tag'>
|
||||
<field name='name'>401.21 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.24 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_22' model='account.account.tag'>
|
||||
<field name='name'>401.22 Ingresos por arrendamiento</field>
|
||||
<field name='name'>401.25 Ingresos por arrendamiento</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_23' model='account.account.tag'>
|
||||
<field name='name'>401.23 Ingresos por arrendamiento nacionales partes relacionadas</field>
|
||||
<field name='name'>401.26 Ingresos por arrendamiento nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_24' model='account.account.tag'>
|
||||
<field name='name'>401.24 Ingresos por arrendamiento extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.27 Ingresos por arrendamiento extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_25' model='account.account.tag'>
|
||||
<field name='name'>401.25 Ingresos por exportación</field>
|
||||
<field name='name'>401.28 Ingresos por exportación</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_26' model='account.account.tag'>
|
||||
<field name='name'>401.26 Ingresos por comisiones</field>
|
||||
<field name='name'>401.29 Ingresos por comisiones</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_27' model='account.account.tag'>
|
||||
<field name='name'>401.27 Ingresos por maquila</field>
|
||||
<field name='name'>401.30 Ingresos por maquila</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_28' model='account.account.tag'>
|
||||
<field name='name'>401.28 Ingresos por coordinados</field>
|
||||
<field name='name'>401.31 Ingresos por coordinados</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_29' model='account.account.tag'>
|
||||
<field name='name'>401.29 Ingresos por regalías</field>
|
||||
<field name='name'>401.32 Ingresos por regalías</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_30' model='account.account.tag'>
|
||||
<field name='name'>401.30 Ingresos por asistencia técnica</field>
|
||||
<field name='name'>401.33 Ingresos por asistencia técnica</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_31' model='account.account.tag'>
|
||||
<field name='name'>401.31 Ingresos por donativos</field>
|
||||
<field name='name'>401.34 Ingresos por donativos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_32' model='account.account.tag'>
|
||||
<field name='name'>401.32 Ingresos por intereses (actividad propia)</field>
|
||||
<field name='name'>401.35 Ingresos por intereses (actividad propia)</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_33' model='account.account.tag'>
|
||||
<field name='name'>401.33 Ingresos de copropiedad</field>
|
||||
<field name='name'>401.36 Ingresos de copropiedad</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_34' model='account.account.tag'>
|
||||
<field name='name'>401.34 Ingresos por fideicomisos</field>
|
||||
<field name='name'>401.37 Ingresos por fideicomisos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_35' model='account.account.tag'>
|
||||
<field name='name'>401.35 Ingresos por factoraje financiero</field>
|
||||
<field name='name'>401.38 Ingresos por factoraje financiero</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_36' model='account.account.tag'>
|
||||
<field name='name'>401.36 Ingresos por arrendamiento financiero</field>
|
||||
<field name='name'>401.39 Ingresos por arrendamiento financiero</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_37' model='account.account.tag'>
|
||||
<field name='name'>401.37 Ingresos de extranjeros con establecimiento en el país</field>
|
||||
<field name='name'>401.40 Ingresos de extranjeros con establecimiento en el país</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_38' model='account.account.tag'>
|
||||
<field name='name'>401.38 Otros ingresos propios</field>
|
||||
<field name='name'>401.41 Otros ingresos propios</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
@@ -2293,20 +2311,26 @@
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_02_new' model='account.account.tag'>
|
||||
<field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios en zona fronteriza norte</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_02' model='account.account.tag'>
|
||||
<field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
|
||||
<field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_03' model='account.account.tag'>
|
||||
<field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
|
||||
<field name='name'>402.04 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_04' model='account.account.tag'>
|
||||
<field name='name'>402.04 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
|
||||
<field name='name'>402.05 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
|
||||
@@ -69,7 +69,7 @@ class LunchProductReport(models.Model):
|
||||
self._cr.execute("""
|
||||
CREATE or REPLACE view %s AS (
|
||||
SELECT
|
||||
row_number() over () AS id,
|
||||
row_number() over (ORDER BY user_id,product.id) AS id,
|
||||
product.id AS product_id,
|
||||
product.name,
|
||||
product.category_id,
|
||||
|
||||
@@ -40,7 +40,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
});
|
||||
},
|
||||
reload: function (id, options) {
|
||||
var domain = options.domain || this.localData[id].domain;
|
||||
var domain = options && options.domain || this.localData[id].domain;
|
||||
|
||||
this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
|
||||
options = _.extend(options, {domain: domain});
|
||||
|
||||
@@ -172,6 +172,8 @@ class MailMail(models.Model):
|
||||
messages._notify_failure_update() # notify user that we have a failure
|
||||
(notifications - failed).sudo().write({
|
||||
'email_status': 'sent',
|
||||
'failure_type': '',
|
||||
'failure_reason': '',
|
||||
})
|
||||
if not failure_type or failure_type == 'RECIPIENT': # if we have another error, we want to keep the mail.
|
||||
mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
|
||||
|
||||
@@ -730,8 +730,9 @@ class Message(models.Model):
|
||||
WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
|
||||
if self._cr.fetchall():
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
|
||||
(self._description, operation))
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
|
||||
# Read mail_message.ids to have their values
|
||||
message_values = dict((res_id, {}) for res_id in self.ids)
|
||||
@@ -900,8 +901,9 @@ class Message(models.Model):
|
||||
if not (other_ids and self.browse(other_ids).exists()):
|
||||
return
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
|
||||
(self._description, operation))
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), list(other_ids)[:6], _('User:'), self._uid)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_record_name(self, values):
|
||||
|
||||
@@ -537,6 +537,7 @@ var BasicComposer = Widget.extend({
|
||||
files: ev.currentTarget.files,
|
||||
submitForm: true
|
||||
});
|
||||
ev.target.value = "";
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -44,12 +44,19 @@ class Partner(models.Model):
|
||||
'member_lines.account_invoice_line.invoice_id.partner_id',
|
||||
'free_member',
|
||||
'member_lines.date_to', 'member_lines.date_from',
|
||||
'associate_member.membership_state')
|
||||
'associate_member')
|
||||
def _compute_membership_state(self):
|
||||
values = self._membership_state()
|
||||
for partner in self:
|
||||
partner.membership_state = values[partner.id]
|
||||
|
||||
# Do not depend directly on "associate_member.membership_state" or we might end up in an
|
||||
# infinite loop. Since we still need this dependency somehow, we explicitly search for the
|
||||
# "parent members" and trigger a recompute.
|
||||
parent_members = self.search([('associate_member', 'in', self.ids)]) - self
|
||||
if parent_members:
|
||||
parent_members._recompute_todo(self._fields['membership_state'])
|
||||
|
||||
@api.depends('member_lines.account_invoice_line.invoice_id.state',
|
||||
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
|
||||
'member_lines.account_invoice_line.invoice_id.payment_ids',
|
||||
|
||||
@@ -247,6 +247,7 @@ class MrpBomLine(models.Model):
|
||||
'mrp.bom', 'Parent BoM',
|
||||
index=True, ondelete='cascade', required=True)
|
||||
parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id')
|
||||
valid_product_attribute_value_ids = fields.Many2many('product.attribute.value', related='bom_id.product_tmpl_id.valid_product_attribute_value_ids')
|
||||
attribute_value_ids = fields.Many2many(
|
||||
'product.attribute.value', string='Apply on Variants',
|
||||
help="BOM Product Variants needed form apply this line.")
|
||||
|
||||
@@ -597,7 +597,7 @@ class MrpProduction(models.Model):
|
||||
move_raw.write({
|
||||
'group_id': production.procurement_group_id.id,
|
||||
'unit_factor': move_raw.product_uom_qty / production.product_qty,
|
||||
'reference': self.name, # set reference when MO name is different than 'New'
|
||||
'reference': production.name, # set reference when MO name is different than 'New'
|
||||
})
|
||||
production._generate_finished_moves()
|
||||
production.move_raw_ids._adjust_procure_method()
|
||||
@@ -849,8 +849,10 @@ class MrpProduction(models.Model):
|
||||
raise UserError(_('Work order %s is still running') % wo.name)
|
||||
self._check_lots()
|
||||
self.post_inventory()
|
||||
moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves_to_cancel._action_cancel()
|
||||
# Moves without quantity done are not posted => set them as done instead of canceling. In
|
||||
# case the user edits the MO later on and sets some consumed quantity on those, we do not
|
||||
# want the move lines to be canceled.
|
||||
(self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({'state': 'done'})
|
||||
self.write({'date_finished': fields.Datetime.now()})
|
||||
return True
|
||||
|
||||
|
||||
@@ -63,8 +63,9 @@
|
||||
string=" "/>
|
||||
<field name="product_qty"/>
|
||||
<field name="parent_product_tmpl_id" invisible="1" />
|
||||
<field name="valid_product_attribute_value_ids" invisible="1"/>
|
||||
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" domain="[('id', 'in', valid_product_attribute_value_ids)]" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<field name="finished_lots_exist" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_locked', '=', True)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': ['|', ('is_locked', '=', True), ('state', '=', 'cancel')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<tree editable="bottom" decoration-success="product_uom_qty==qty_done" decoration-danger="(product_uom_qty > 0) and (qty_done>product_uom_qty)">
|
||||
<field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
|
||||
<field name="lot_produced_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
<input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/>
|
||||
Save my payment data
|
||||
</label>
|
||||
<t t-if="acq.save_token == 'always'">
|
||||
<input type="checkbox" name="o_payment_form_save_token" checked="'checked'" class="o_hidden" data-remove-me=""/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="acq.payment_flow == 's2s'">
|
||||
|
||||
@@ -170,7 +170,7 @@ class AcquirerAdyen(models.Model):
|
||||
'sessionValidity': tmp_date.isoformat('T')[:19] + "Z",
|
||||
'resURL': urls.url_join(base_url, AdyenController._return_url),
|
||||
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False,
|
||||
'shopperEmail': values.get('partner_email', ''),
|
||||
'shopperEmail': values.get('partner_email') or values.get('billing_partner_email') or '',
|
||||
})
|
||||
values['merchantSig'] = self._adyen_generate_merchant_sig_sha256('in', values)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class AcquirerPaypal(models.Model):
|
||||
else:
|
||||
percentage = self.fees_int_var
|
||||
fixed = self.fees_int_fixed
|
||||
fees = (percentage / 100.0 * amount + fixed) / (1 - percentage / 100.0)
|
||||
fees = (percentage / 100.0 * amount) + fixed / (1 - percentage / 100.0)
|
||||
return fees
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -214,7 +214,8 @@ class PosSession(models.Model):
|
||||
st_values = {
|
||||
'journal_id': journal.id,
|
||||
'user_id': self.env.user.id,
|
||||
'name': pos_name
|
||||
'name': pos_name,
|
||||
'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id)
|
||||
}
|
||||
|
||||
statements.append(ABS.with_context(ctx).sudo(uid).create(st_values).id)
|
||||
|
||||
@@ -38,3 +38,16 @@
|
||||
margin-top: 6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.o_form_project_tasks.o_form_editable .oe_title {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.o_form_project_tasks.o_form_editable .oe_title .o_task_name {
|
||||
/*
|
||||
* should be (coming from addons/web/static/src/scss/form_view.scss):
|
||||
* max-width: map-get($container-max-widths, md) - (2 * $o-horizontal-padding);
|
||||
*/
|
||||
max-width: 688px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
<div class="row">
|
||||
<div t-attf-class="col {{'col-lg-6' if not task.description else 'col-lg-12'}}">
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}" target="_blank" data-no-post-process="">
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank" data-no-post-process="">
|
||||
<div class='oe_attachment_embedded o_image o_image_small mr-2 mr-lg-3' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
|
||||
<div class='oe_attachment_name text-truncate'><t t-esc='attachment.name'/></div>
|
||||
</a>
|
||||
|
||||
@@ -95,9 +95,9 @@ class AccountInvoice(models.Model):
|
||||
self.partner_id = self.purchase_id.partner_id.id
|
||||
|
||||
vendor_ref = self.purchase_id.partner_ref
|
||||
if vendor_ref:
|
||||
self.reference = ", ".join([self.reference, vendor_ref]) if (
|
||||
self.reference and vendor_ref not in self.reference) else vendor_ref
|
||||
if vendor_ref and (not self.reference or (
|
||||
vendor_ref + ", " not in self.reference and not self.reference.endswith(vendor_ref))):
|
||||
self.reference = ", ".join([self.reference, vendor_ref]) if self.reference else vendor_ref
|
||||
|
||||
if not self.invoice_line_ids:
|
||||
#as there's no invoice line yet, we keep the currency of the PO
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
<field name="amount_total" sum="Total amount" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="state"/>
|
||||
<field name="invoice_status" attrs="{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))]}"/>
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -185,11 +185,15 @@ class StockRule(models.Model):
|
||||
price_unit = seller.currency_id._convert(
|
||||
price_unit, line.order_id.currency_id, line.order_id.company_id, fields.Date.today())
|
||||
|
||||
return {
|
||||
res = {
|
||||
'product_qty': line.product_qty + procurement_uom_po_qty,
|
||||
'price_unit': price_unit,
|
||||
'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
|
||||
}
|
||||
orderpoint_id = values.get('orderpoint_id')
|
||||
if orderpoint_id:
|
||||
res['orderpoint_id'] = orderpoint_id.id
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, company_id, values, po):
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<page string="Repair Line">
|
||||
<group>
|
||||
<group>
|
||||
<field name="type"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<label for="product_uom_qty"/>
|
||||
@@ -86,7 +87,6 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
</div>
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
|
||||
<field name="invoiced" invisible="1"/>
|
||||
@@ -137,7 +137,7 @@
|
||||
<field name="fees_lines">
|
||||
<form string="Fees">
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" domain="[('type','=','service')]" required="True"/>
|
||||
<field name="name"/>
|
||||
<label for="product_uom_qty"/>
|
||||
<div class="o_row">
|
||||
|
||||
@@ -1242,6 +1242,7 @@ class SaleOrderLine(models.Model):
|
||||
:param additional_domain: domain to restrict AAL to include in computation (required since timesheet is an AAL with a project ...)
|
||||
"""
|
||||
result = {}
|
||||
|
||||
# avoid recomputation if no SO lines concerned
|
||||
if not self:
|
||||
return result
|
||||
|
||||
@@ -447,6 +447,7 @@
|
||||
name="analytic_tag_ids"
|
||||
groups="analytic.group_analytic_tags"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"
|
||||
/>
|
||||
<field
|
||||
name="price_unit"
|
||||
|
||||
@@ -92,10 +92,10 @@ class ProductTemplate(models.Model):
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
res = super(ProductTemplate, self)._onchange_type()
|
||||
if self.type == 'service':
|
||||
if self.type == 'service' and not self.invoice_policy:
|
||||
self.invoice_policy = 'order'
|
||||
self.service_type = 'timesheet'
|
||||
elif self.type == 'consu' and self.service_policy == 'ordered_timesheet':
|
||||
elif self.type == 'consu' and not self.invoice_policy and self.service_policy == 'ordered_timesheet':
|
||||
self.invoice_policy = 'order'
|
||||
return res
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@ class ProjectTask(models.Model):
|
||||
def _get_default_partner(self):
|
||||
partner = False
|
||||
if 'default_project_id' in self.env.context: # partner from SO line is prior on one from project
|
||||
project = self.env['project.project'].browse(self.env.context['default_project_id'])
|
||||
partner = project.sale_line_id.order_partner_id
|
||||
project_sudo = self.env['project.project'].browse(self.env.context['default_project_id']).sudo()
|
||||
partner = project_sudo.sale_line_id.order_partner_id
|
||||
if not partner:
|
||||
partner = super(ProjectTask, self)._get_default_partner()
|
||||
return partner
|
||||
@@ -213,7 +213,7 @@ class ProjectTask(models.Model):
|
||||
@api.multi
|
||||
@api.constrains('sale_line_id')
|
||||
def _check_sale_line_type(self):
|
||||
for task in self:
|
||||
for task in self.sudo():
|
||||
if task.sale_line_id:
|
||||
if not task.sale_line_id.is_service or task.sale_line_id.is_expense:
|
||||
raise ValidationError(_('You cannot link the order item %s - %s to this task because it is a re-invoiced expense.' % (task.sale_line_id.order_id.id, task.sale_line_id.product_id.name)))
|
||||
|
||||
@@ -82,12 +82,16 @@ class Project(models.Model):
|
||||
#
|
||||
# Time Repartition (per employee per billable types)
|
||||
#
|
||||
user_ids = self.env['project.task'].sudo().search_read([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'])
|
||||
user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id'])
|
||||
user_ids = [user_id['user_id'][0] for user_id in user_ids]
|
||||
employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids'])
|
||||
# flatten the list of list
|
||||
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
|
||||
employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id')
|
||||
|
||||
aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
|
||||
employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
|
||||
|
||||
employees = self.env['hr.employee'].sudo().browse(employee_ids)
|
||||
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
|
||||
|
||||
@@ -332,8 +336,14 @@ class Project(models.Model):
|
||||
def _plan_prepare_actions(self, values):
|
||||
actions = []
|
||||
if len(self) == 1:
|
||||
task_order_line_ids = []
|
||||
# retrieve all the sale order line that we will need later below
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id'])
|
||||
task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids]
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman'):
|
||||
if not self.sale_line_id and not self.tasks.mapped('sale_line_id'):
|
||||
if not self.sale_line_id and not task_order_line_ids:
|
||||
actions.append({
|
||||
'label': _("Create a Sales Order"),
|
||||
'type': 'action',
|
||||
@@ -342,14 +352,19 @@ class Project(models.Model):
|
||||
})
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account
|
||||
sale_orders = self.tasks.mapped('sale_line_id.order_id').filtered(lambda so: so.invoice_status == 'to invoice')
|
||||
if to_invoice_amount and sale_orders:
|
||||
if len(sale_orders) == 1:
|
||||
|
||||
sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id'])
|
||||
sale_order_ids = [s['order_id'][0] for s in sale_order_ids]
|
||||
sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id'])
|
||||
sale_order_ids = list(map(lambda x: x['id'], sale_order_ids))
|
||||
|
||||
if to_invoice_amount and sale_order_ids:
|
||||
if len(sale_order_ids) == 1:
|
||||
actions.append({
|
||||
'label': _("Create Invoice"),
|
||||
'type': 'action',
|
||||
'action_id': 'sale.action_view_sale_advance_payment_inv',
|
||||
'context': json.dumps({'active_ids': sale_orders.ids, 'active_model': 'project.project'}),
|
||||
'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}),
|
||||
})
|
||||
else:
|
||||
actions.append({
|
||||
@@ -385,9 +400,13 @@ class Project(models.Model):
|
||||
# if only one project, add it in the context as default value
|
||||
tasks_domain = [('project_id', 'in', self.ids)]
|
||||
tasks_context = self.env.context
|
||||
tasks_projects = self.env['project.task'].sudo().search(tasks_domain).mapped('project_id')
|
||||
if len(tasks_projects) == 1:
|
||||
tasks_context = {**tasks_context, 'default_project_id': tasks_projects.id}
|
||||
|
||||
# filter out all the projects that have no tasks
|
||||
task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id'])
|
||||
task_projects_ids = [p['project_id'][0] for p in task_projects_ids]
|
||||
|
||||
if len(task_projects_ids) == 1:
|
||||
tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]}
|
||||
stat_buttons.append({
|
||||
'name': _('Tasks'),
|
||||
'count': sum(self.mapped('task_count')),
|
||||
@@ -400,8 +419,13 @@ class Project(models.Model):
|
||||
})
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
sale_orders = self.mapped('sale_line_id.order_id') | self.mapped(
|
||||
'tasks.sale_order_id')
|
||||
# read all the sale orders linked to the projects' tasks
|
||||
task_so_ids = self.env['project.task'].search_read([
|
||||
('project_id', 'in', self.ids), ('sale_order_id', '!=', False)
|
||||
], ['sale_order_id'])
|
||||
task_so_ids = [o['sale_order_id'][0] for o in task_so_ids]
|
||||
|
||||
sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids)
|
||||
if sale_orders:
|
||||
stat_buttons.append({
|
||||
'name': _('Sales Orders'),
|
||||
@@ -413,15 +437,20 @@ class Project(models.Model):
|
||||
context={'create': False, 'edit': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
invoices = sale_orders.mapped('invoice_ids').filtered(lambda inv: inv.type == 'out_invoice')
|
||||
if invoices:
|
||||
|
||||
invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids'])
|
||||
invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids]))
|
||||
invoice_ids = self.env['account.invoice'].search_read([('id', 'in', invoice_ids), ('type', '=', 'out_invoice')], ['id'])
|
||||
invoice_ids = list(map(lambda x: x['id'], invoice_ids))
|
||||
|
||||
if invoice_ids:
|
||||
stat_buttons.append({
|
||||
'name': _('Invoices'),
|
||||
'count': len(invoices),
|
||||
'count': len(invoice_ids),
|
||||
'icon': 'fa fa-pencil-square-o',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('account.action_invoice_tree1'),
|
||||
domain=[('id', 'in', invoices.ids), ('type', '=', 'out_invoice')],
|
||||
domain=[('id', 'in', invoice_ids), ('type', '=', 'out_invoice')],
|
||||
context={'create': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -365,6 +365,12 @@ class SaleOrderLine(models.Model):
|
||||
map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project
|
||||
else:
|
||||
map_so_project[so_line.order_id.id] = project
|
||||
elif not project:
|
||||
# Attach subsequent SO lines to the created project
|
||||
so_line.project_id = (
|
||||
map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id))
|
||||
or map_so_project.get(so_line.order_id.id)
|
||||
)
|
||||
if so_line.product_id.service_tracking == 'task_in_project':
|
||||
if not project:
|
||||
if so_line.product_id.project_template_id:
|
||||
|
||||
@@ -363,8 +363,8 @@ class TestSaleService(TestCommonSaleTimesheetNoChart):
|
||||
# check each line has or no generate something
|
||||
self.assertTrue(so_line1.project_id, "Line1 should have create a project based on template A")
|
||||
self.assertTrue(so_line2.project_id, "Line2 should have create an empty project")
|
||||
self.assertFalse(so_line3.project_id, "Line3 should not have create a project, since line1 already create a project based on template A")
|
||||
self.assertFalse(so_line4.project_id, "Line4 should not have create a project, since line1 already create an empty project")
|
||||
self.assertEqual(so_line3.project_id, so_line1.project_id, "Line3 should reuse project of line1")
|
||||
self.assertEqual(so_line4.project_id, so_line2.project_id, "Line4 should reuse project of line2")
|
||||
self.assertTrue(so_line4.task_id, "Line4 should have create a new task, even if no project created.")
|
||||
self.assertTrue(so_line5.project_id, "Line5 should have create a project based on template B")
|
||||
|
||||
|
||||
@@ -18,12 +18,28 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_view_search_sale_timesheet" model="ir.ui.view">
|
||||
<field name="name">product.template.search.timesheet</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_search_view"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='consumable']" position="after">
|
||||
<separator/>
|
||||
<filter string="Time-based services" name="product_time_based" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')]"/>
|
||||
<filter string="Fixed price services" name="product_service_fixed" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'order'), ('service_type', '=', 'timesheet')]"/>
|
||||
<filter string="Milestone services" name="product_service_milestone" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'manual')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_time_based" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet', 'default_service_policy': 'delivered_timesheet'}</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_fixed" model="ir.actions.act_window">
|
||||
@@ -31,7 +47,8 @@
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet', 'default_service_policy': 'ordered_timesheet'}</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_milestone" model="ir.actions.act_window">
|
||||
@@ -39,7 +56,8 @@
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual', 'default_service_policy': 'delivered_manual'}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -61,20 +61,42 @@
|
||||
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button type="object" name="action_view_so"
|
||||
class="oe_stat_button" icon="fa-dollar"
|
||||
attrs="{'invisible': [('sale_order_id', '=', False)]}"
|
||||
string="Sales Order"/>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
</xpath>
|
||||
<field name="partner_id" position="after">
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_create": True}' context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="billable_type" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_inherit_sale_line_editable" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.sale.line.editable.salesman</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="view_sale_service_inherit_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='sale_line_id']" position="attributes">
|
||||
<attribute name="options">{"no_create": True}</attribute>
|
||||
<attribute name="readonly">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_sale_order" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.sale.order</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button type="object" name="action_view_so"
|
||||
class="oe_stat_button" icon="fa-dollar"
|
||||
attrs="{'invisible': [('sale_order_id', '=', False)]}"
|
||||
string="Sales Order"/>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_search" model="ir.ui.view">
|
||||
<field name="name">project.task.search.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
|
||||
@@ -6,6 +6,14 @@ from odoo import models, fields, api, _
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = 'ir.actions.report'
|
||||
|
||||
@api.multi
|
||||
def retrieve_attachment(self, record):
|
||||
# Override this method in order to force to re-render the pdf in case of
|
||||
# using snailmail
|
||||
if self.env.context.get('snailmail_layout'):
|
||||
return False
|
||||
return super(IrActionsReport, self).retrieve_attachment(record)
|
||||
|
||||
@api.model
|
||||
def get_paperformat(self):
|
||||
# force the right format (euro/A4) when sending letters, only if we are not using the l10n_DE layout
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestPingenSend(AccountingTestCase):
|
||||
self.data = {
|
||||
'data': json.dumps({
|
||||
'speed': 1,
|
||||
'color': 2,
|
||||
'color': 1,
|
||||
'duplex': 0,
|
||||
'send': True,
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<!--
|
||||
Resource: stock.location
|
||||
-->
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="stock_location_locations" model="stock.location">
|
||||
<field name="name">Physical Locations</field>
|
||||
<field name="usage">view</field>
|
||||
|
||||
@@ -200,7 +200,7 @@ class Route(models.Model):
|
||||
@api.onchange('warehouse_selectable')
|
||||
def _onchange_warehouse_selectable(self):
|
||||
if not self.warehouse_selectable:
|
||||
self.warehouse_ids = []
|
||||
self.warehouse_ids = [(5, 0, 0)]
|
||||
|
||||
def toggle_active(self):
|
||||
for route in self:
|
||||
|
||||
@@ -1156,7 +1156,7 @@ class StockMove(models.Model):
|
||||
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
|
||||
if len(result_package.quant_ids.mapped('location_id')) > 1:
|
||||
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
|
||||
picking = moves_todo and moves_todo[0].picking_id or False
|
||||
picking = moves_todo.mapped('picking_id')
|
||||
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
|
||||
moves_todo.mapped('move_dest_ids')._action_assign()
|
||||
|
||||
|
||||
@@ -433,5 +433,5 @@ class QuantPackage(models.Model):
|
||||
for quant in self._get_contained_quants():
|
||||
if quant.product_id not in res:
|
||||
res[quant.product_id] = 0
|
||||
res[quant.product_id] += quant.qty
|
||||
res[quant.product_id] += quant.quantity
|
||||
return res
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
<separator/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Pickings that are late on scheduled time"/>
|
||||
<separator/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False)]" help="Remaining parts of picking partially processed"/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Remaining parts of picking partially processed"/>
|
||||
<separator/>
|
||||
<filter string="My Activities" name="activities_my"
|
||||
domain="[('activity_ids.user_id', '=', uid)]"/>
|
||||
@@ -566,7 +566,6 @@ action = action_values
|
||||
'default_picking_type_id': active_id,
|
||||
'contact_display': 'partner_address',
|
||||
'search_default_late': 1,
|
||||
'search_default_confirmed': 1,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_picking_internal_search"/>
|
||||
@@ -589,7 +588,6 @@ action = action_values
|
||||
'default_picking_type_id': active_id,
|
||||
'contact_display': 'partner_address',
|
||||
'search_default_backorder': 1,
|
||||
'search_default_confirmed': 1,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_picking_internal_search"/>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
|
||||
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
|
||||
</template>
|
||||
|
||||
<template id="survey_web_assets" name="Survey Web Assets">
|
||||
@@ -32,7 +33,6 @@
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/scss/fonts.scss" />
|
||||
<!-- Some helper classes (like mt- mb-) are required for survey templates -->
|
||||
<link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
|
||||
</template>
|
||||
|
||||
<template id="survey_report_assets_pdf" inherit_id="web.report_assets_pdf">
|
||||
|
||||
@@ -83,9 +83,8 @@ class UoM(models.Model):
|
||||
self._cr.execute("""
|
||||
SELECT C.id AS category_id, count(U.id) AS uom_count
|
||||
FROM uom_category C
|
||||
LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference'
|
||||
LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't'
|
||||
WHERE C.id IN %s
|
||||
AND U.active = 't'
|
||||
GROUP BY C.id
|
||||
""", (tuple(category_ids),))
|
||||
for uom_data in self._cr.dictfetchall():
|
||||
|
||||
@@ -68,6 +68,16 @@ class TestUom(TransactionCase):
|
||||
'name': 'Custom UoM category',
|
||||
})
|
||||
|
||||
# at first we can not create a non reference in custom category
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['uom.uom'].create({
|
||||
'name': 'Bigger UoM of my category',
|
||||
'factor_inv': 42,
|
||||
'uom_type': 'bigger',
|
||||
'rounding': 0.5,
|
||||
'category_id': category.id
|
||||
})
|
||||
|
||||
# create the reference
|
||||
self.env['uom.uom'].create({
|
||||
'name': 'Reference UoM of my category',
|
||||
|
||||
@@ -372,7 +372,7 @@ ActionManager.include({
|
||||
Widget: View,
|
||||
});
|
||||
} else if (config.debug === 'assets') {
|
||||
console.error("View type '" + viewType + "' is not present in the view registry.");
|
||||
console.log("View type '" + viewType + "' is not present in the view registry.");
|
||||
}
|
||||
});
|
||||
return views;
|
||||
|
||||
@@ -1510,6 +1510,7 @@ var AbstractFieldBinary = AbstractField.extend({
|
||||
*/
|
||||
_clearFile: function (){
|
||||
var self = this;
|
||||
this.$('.o_input_file').val('');
|
||||
this.set_filename('');
|
||||
if (!this.isDestroyed()) {
|
||||
this._setValue(false).then(function() {
|
||||
|
||||
@@ -65,8 +65,9 @@ var Model = AbstractModel.extend({
|
||||
/**
|
||||
* reload
|
||||
*/
|
||||
reload: function (_id, _params) {
|
||||
// FIXME: update this._state or something?
|
||||
reload: function (_id, params) {
|
||||
_.extend(this._state, _.pick(params, ['domain', 'context']));
|
||||
|
||||
return this._fetch();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
|
||||
font-family: 'Lato' 'Noto';
|
||||
font-family: 'Lato', 'Noto';
|
||||
color: gray('700');
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1998,6 +1998,36 @@ QUnit.module('basic_fields', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("binary fields input value is empty whean clearing after uploading", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="document" filename="foo"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
// // We need to convert the input type since we can't programmatically set the value of a file input
|
||||
form.$('.o_input_file').attr('type', 'text').val('coucou.txt');
|
||||
|
||||
assert.strictEqual(form.$('.o_input_file').val(), 'coucou.txt',
|
||||
"input value should be changed to \"coucou.txt\"");
|
||||
|
||||
await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button'));
|
||||
|
||||
assert.strictEqual(form.$('.o_input_file').val(), '',
|
||||
"input value should be empty");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('field text in editable list view', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class Website(Home):
|
||||
if request.env['res.users'].browse(request.uid).has_group('base.group_user'):
|
||||
redirect = b'/web?' + request.httprequest.query_string
|
||||
else:
|
||||
redirect = '/'
|
||||
redirect = '/my'
|
||||
return http.redirect_with_hash(redirect)
|
||||
return response
|
||||
|
||||
@@ -467,3 +467,8 @@ class WebsiteBinary(http.Controller):
|
||||
if unique:
|
||||
kw['unique'] = unique
|
||||
return Binary().content_image(**kw)
|
||||
|
||||
@http.route(['/favicon.ico'], type='http', auth='public', website=True)
|
||||
def favicon(self, **kw):
|
||||
# when opening a pdf in chrome, chrome tries to open the default favicon url
|
||||
return self.content_image(model='website', id=str(request.website.id), field='favicon', **kw)
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Default Menu to store module menus for new website -->
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Default Main Menu</field>
|
||||
<field name="url">/default-main-menu</field>
|
||||
</record>
|
||||
<record id="menu_home" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
</record>
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
</record>
|
||||
|
||||
<data>
|
||||
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
|
||||
|
||||
<!-- Homepage -->
|
||||
<record id="homepage" model="ir.ui.view">
|
||||
<field name="name">Home</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -35,13 +15,7 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="homepage"/>
|
||||
</record>
|
||||
|
||||
<!-- Contact Us page -->
|
||||
<record id="contactus" model="ir.ui.view">
|
||||
<field name="name">Contact Us</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -93,13 +67,7 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="contactus_page" model="website.page">
|
||||
<field name="url">/contactus</field>
|
||||
<field name="website_published">True</field>
|
||||
<field name="view_id" ref="contactus"/>
|
||||
</record>
|
||||
|
||||
<!-- About Us page -->
|
||||
<record id="aboutus" model="ir.ui.view">
|
||||
<field name="name">About us</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -145,12 +113,43 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="homepage"/>
|
||||
</record>
|
||||
<record id="contactus_page" model="website.page">
|
||||
<field name="url">/contactus</field>
|
||||
<field name="website_published">True</field>
|
||||
<field name="view_id" ref="contactus"/>
|
||||
</record>
|
||||
<record id="aboutus_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/aboutus</field>
|
||||
<field name="view_id" ref="aboutus"/>
|
||||
</record>
|
||||
|
||||
<!-- Default Menu to store module menus for new website -->
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Default Main Menu</field>
|
||||
<field name="url">/default-main-menu</field>
|
||||
</record>
|
||||
<record id="menu_home" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
</record>
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
</record>
|
||||
|
||||
<record id="default_website" model="website">
|
||||
<field name="name">My Website</field>
|
||||
<field name="domain"></field>
|
||||
|
||||
@@ -82,7 +82,8 @@ class View(models.Model):
|
||||
# original tree. Indeed, the order of children 'id' fields
|
||||
# must remain the same so that the inheritance is applied
|
||||
# in the same order in the copied tree.
|
||||
inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
|
||||
child = inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
|
||||
inherit_child.inherit_children_ids.write({'inherit_id': child.id})
|
||||
inherit_child.unlink()
|
||||
else:
|
||||
# Trigger COW on inheriting views
|
||||
|
||||
@@ -141,7 +141,7 @@ class Website(models.Model):
|
||||
public_user_to_change_websites = self.env['website']
|
||||
self._get_languages.clear_cache(self)
|
||||
if 'company_id' in values and 'user_id' not in values:
|
||||
public_user_to_change_websites = self.filtered(lambda w: w.user_id.company_id.id != values['company_id'])
|
||||
public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id'])
|
||||
if public_user_to_change_websites:
|
||||
company = self.env['res.company'].browse(values['company_id'])
|
||||
super(Website, public_user_to_change_websites).write(dict(values, user_id=company._get_public_user().id))
|
||||
|
||||
@@ -797,6 +797,54 @@ class TestCowViewSaving(common.TransactionCase):
|
||||
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
|
||||
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
|
||||
|
||||
def test_multiple_inherit_level(self):
|
||||
""" Test multi-level inheritance:
|
||||
Base
|
||||
|
|
||||
---> Extension (Website-specific)
|
||||
|
|
||||
---> Extension 2 (Website-specific)
|
||||
"""
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
self.inherit_view.website_id = 1
|
||||
inherit_view_2 = View.create({
|
||||
'name': 'Extension 2',
|
||||
'mode': 'extension',
|
||||
'inherit_id': self.inherit_view.id,
|
||||
'arch': '<div position="inside">, extended content 2</div>',
|
||||
'key': 'website.extension_view_2',
|
||||
'website_id': 1,
|
||||
})
|
||||
|
||||
total_views = View.search_count([])
|
||||
|
||||
# id | name | content | website_id | inherit | key
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# 1 | Base | base content | / | / | website.base_view
|
||||
# 2 | Extension | , extended content | 1 | 1 | website.extension_view
|
||||
# 3 | Extension 2 | , extended content 2 | 1 | 2 | website.extension_view_2
|
||||
|
||||
self.base_view.with_context(website_id=1).write({'arch': '<div>modified content</div>'})
|
||||
|
||||
# 2 views are created, one is deleted
|
||||
self.assertEqual(View.search_count([]), total_views + 1)
|
||||
self.assertFalse(self.inherit_view.exists())
|
||||
self.assertTrue(inherit_view_2.exists())
|
||||
|
||||
# Verify the inheritance
|
||||
base_specific = View.search([('key', '=', self.base_view.key), ('website_id', '=', 1)]).with_context(load_all_views=True)
|
||||
extend_specific = View.search([('key', '=', 'website.extension_view'), ('website_id', '=', 1)])
|
||||
self.assertEqual(extend_specific.inherit_id, base_specific)
|
||||
self.assertEqual(inherit_view_2.inherit_id, extend_specific)
|
||||
|
||||
# id | name | content | website_id | inherit | key
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# 1 | Base | base content | / | / | website.base_view
|
||||
# 4 | Base | modified content | 1 | / | website.base_view
|
||||
# 5 | Extension | , extended content | 1 | 4 | website.extension_view
|
||||
# 3 | Extension 2 | , extended content 2 | 1 | 5 | website.extension_view_2
|
||||
|
||||
|
||||
class Crawler(HttpCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -218,6 +218,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<t t-raw="0"/>
|
||||
<div class="alert alert-info alert-dismissable mt16 css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Following content will appear on all events
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_website_event_layout_1"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -59,7 +59,10 @@ class MailMessage(models.Model):
|
||||
if self.user_has_groups('base.group_public'):
|
||||
self.env.cr.execute('SELECT id FROM "%s" WHERE website_published IS FALSE AND id = ANY (%%s)' % (self._table), (self.ids,))
|
||||
if self.env.cr.fetchall():
|
||||
raise AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation))
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
return super(MailMessage, self).check_access_rule(operation=operation)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -258,8 +258,8 @@
|
||||
<button class="btn btn-sm d-md-none bg-white border" type="button" data-toggle="collapse" data-target="#o_wprofile_sidebar_collapse" aria-expanded="false" aria-controls="o_wprofile_sidebar_collapse">More info</button>
|
||||
</div>
|
||||
<div class="collapse d-md-block" id="o_wprofile_sidebar_collapse">
|
||||
<div t-if="user.next_rank_id" class="mb-3">
|
||||
<small class="font-weight-bold mt-1">Next rank:</small>
|
||||
<div t-if="user.rank_id" class="mb-3">
|
||||
<small t-if="user.next_rank_id" class="font-weight-bold mt-1">Next rank:</small>
|
||||
<t t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="img_max_width">40%</t>
|
||||
</t>
|
||||
@@ -313,13 +313,18 @@
|
||||
</template>
|
||||
|
||||
<template id="profile_next_rank_card" name="Profile Next Rank Card">
|
||||
<div t-if="user.next_rank_id" class="o_wprofile_progress_circle">
|
||||
<div class="o_wprofile_progress_circle">
|
||||
<svg viewBox="0 0 36 36" class="o_pc_circular_chart">
|
||||
<t t-if="(user.next_rank_id.karma_min - user.rank_id.karma_min) > 0">
|
||||
<t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))"/>
|
||||
<t t-if="user.next_rank_id">
|
||||
<t t-if="(user.next_rank_id.karma_min - user.rank_id.karma_min) > 0">
|
||||
<t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="user_points" t-value="0"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="user_points" t-value="0"/>
|
||||
<t t-set="user_points" t-value="100"/>
|
||||
</t>
|
||||
<path class="o_pc_circle_bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
<path class="o_pc_circle" t-attf-stroke-dasharray="#{user_points}, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" stroke="url(#gradient)" mask="url(#mask)"/>
|
||||
@@ -333,12 +338,20 @@
|
||||
</svg>
|
||||
<div class="o_pc_overlay d-flex flex-column align-items-center justify-content-center">
|
||||
<img class="img-fluid"
|
||||
t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id}/image_medium"
|
||||
t-att-alt="user.next_rank_id.name + ' badge'"
|
||||
t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id if user.next_rank_id else user.rank_id.id}/image_medium"
|
||||
t-att-alt="(user.next_rank_id.name if user.next_rank_id else user.rank_id.name) + ' badge'"
|
||||
t-att-style="'max-width: ' + (img_max_width if img_max_width else '50%;')"/>
|
||||
<h4 class=" mb-0">"<span t-field="user.next_rank_id.name"/>"</h4>
|
||||
<h4 class=" mb-0">
|
||||
"
|
||||
<span t-if="user.next_rank_id" t-field="user.next_rank_id.name"/>
|
||||
<span t-else="" t-field="user.rank_id.name"/>
|
||||
"
|
||||
</h4>
|
||||
<small>
|
||||
<span class="font-weight-bold text-primary" t-field="user.karma"/>/<span class="font-weight-bold" t-field="user.next_rank_id.karma_min"/> xp
|
||||
<span class="font-weight-bold text-primary" t-field="user.karma"/>/
|
||||
<span t-if="user.next_rank_id" class="font-weight-bold" t-field="user.next_rank_id.karma_min"/>
|
||||
<span t-else="" class="font-weight-bold" t-field="user.rank_id.karma_min"/>
|
||||
xp
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
odoo.define('website_sale_delivery.tour', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require("web_tour.tour");
|
||||
|
||||
tour.register('check_free_delivery', {
|
||||
test: true,
|
||||
url: '/shop?search=conference chair',
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "select conference chair",
|
||||
trigger: '.oe_product_cart:first a:contains("Conference Chair")',
|
||||
},
|
||||
{
|
||||
content: "click on add to cart",
|
||||
extra_trigger: 'label:contains(Steel) input:propChecked',
|
||||
trigger: '#product_detail form[action^="/shop/cart/update"] .btn-primary',
|
||||
},
|
||||
{
|
||||
content: "click in modal on 'Proceed to checkout' button",
|
||||
trigger: 'button:contains("Proceed to Checkout")',
|
||||
},
|
||||
{
|
||||
content: "go to checkout",
|
||||
extra_trigger: '#cart_products input.js_quantity:propValue(1)',
|
||||
trigger: 'a[href*="/shop/checkout"]',
|
||||
},
|
||||
{
|
||||
content: "Check Free Delivery value to be zero",
|
||||
trigger: "#delivery_carrier span:contains('0.0')"
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_ui
|
||||
@@ -0,0 +1,14 @@
|
||||
import odoo.tests
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
|
||||
def test_01_free_delivery_when_exceed_threshold(self):
|
||||
self.env.ref("delivery.free_delivery_carrier").write({
|
||||
'fixed_price': 2,
|
||||
'free_over': True,
|
||||
'amount': 10,
|
||||
})
|
||||
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('check_free_delivery')", "odoo.__DEBUG__.services['web_tour.tour'].tours.check_free_delivery.ready", login="admin")
|
||||
@@ -19,6 +19,7 @@
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_sale_delivery/static/src/js/website_sale_delivery.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/website_sale_delivery/static/src/scss/website_sale_delivery.scss"/>
|
||||
<script type="text/javascript" src="/website_sale_delivery/static/src/js/website_free_delivery_tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -28,11 +29,9 @@
|
||||
<t t-set='badge_class' t-value="(delivery_nb != 1 and 'float-right ' or '') + 'badge badge-secondary'" />
|
||||
<t t-if="delivery.delivery_type == 'fixed'">
|
||||
<span t-if="delivery.fixed_price > 0.0" t-att-class="badge_class">
|
||||
<t t-set="monetary_options" t-value='{"widget": "monetary",
|
||||
"from_currency": delivery.product_id.company_id.currency_id,
|
||||
"display_currency": website_sale_order.currency_id}'/>
|
||||
<t t-if="delivery.free_over" t-esc="delivery.rate_shipment(website_sale_order)['price']" t-options='monetary_options'/>
|
||||
<t t-else="" t-esc="delivery.fixed_price" t-options='monetary_options'/>
|
||||
<t t-esc="delivery.rate_shipment(website_sale_order)['price'] if delivery.free_over else delivery.fixed_price"
|
||||
t-options='{"widget": "monetary", "from_currency": delivery.product_id.company_id.currency_id,
|
||||
"display_currency": website_sale_order.currency_id}'/>
|
||||
</span>
|
||||
<span t-else="" t-att-class="badge_class">Free</span>
|
||||
</t>
|
||||
|
||||
@@ -40,11 +40,20 @@ publicWidget.registry.WebsiteSale.include({
|
||||
* @param {integer} quantity
|
||||
*/
|
||||
_onOptionsUpdateQuantity: function (quantity) {
|
||||
this.$form
|
||||
var $qtyInput = this.$form
|
||||
.find('.js_main_product input[name="add_qty"]')
|
||||
.first()
|
||||
.val(quantity)
|
||||
.trigger('change');
|
||||
.first();
|
||||
|
||||
if ($qtyInput.length) {
|
||||
$qtyInput.val(quantity).trigger('change');
|
||||
} else {
|
||||
// This handles the case when the "Select Quantity" customize show
|
||||
// is disabled, and therefore the above selector does not find an
|
||||
// element.
|
||||
// To avoid duplicating all RPC, only trigger the variant change if
|
||||
// it is not already done from the above trigger.
|
||||
this.optionalProductsModal.triggerVariantChange(this.optionalProductsModal.$el);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ from odoo.http import request
|
||||
class ProductWishlist(models.Model):
|
||||
_name = 'product.wishlist'
|
||||
_description = 'Product Wishlist'
|
||||
_sql_constrains = [
|
||||
_sql_constraints = [
|
||||
("product_unique_partner_id",
|
||||
"UNIQUE(product_id, partner_id)",
|
||||
"Duplicated wishlisted product for this partner."),
|
||||
|
||||
@@ -268,6 +268,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
'channels_newest': channels_newest,
|
||||
'achievements': achievements,
|
||||
'users': users,
|
||||
'top3_users': self._get_top3_users(),
|
||||
'challenges': challenges,
|
||||
'challenges_done': challenges_done,
|
||||
})
|
||||
@@ -311,6 +312,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
'search_my': my,
|
||||
'search_tags': search_tags,
|
||||
'search_channel_tag_id': post.get('channel_tag_id'),
|
||||
'top3_users': self._get_top3_users(),
|
||||
})
|
||||
|
||||
return request.render('website_slides.courses_all', values)
|
||||
@@ -318,6 +320,11 @@ class WebsiteSlides(WebsiteProfile):
|
||||
def _prepare_additional_channel_values(self, values, **kwargs):
|
||||
return values
|
||||
|
||||
def _get_top3_users(self):
|
||||
return request.env['res.users'].sudo().search_read([
|
||||
('karma', '>', 0),
|
||||
('image', '!=', False)], ['id'], limit=3, order='karma desc')
|
||||
|
||||
@http.route([
|
||||
'/slides/<model("slide.channel"):channel>',
|
||||
'/slides/<model("slide.channel"):channel>/page/<int:page>',
|
||||
@@ -374,6 +381,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
|
||||
values = {
|
||||
'channel': channel,
|
||||
'active_tab': kw.get('active_tab', 'home'),
|
||||
# search
|
||||
'search_category': category,
|
||||
'search_tag': tag,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<div t-foreach="widget.quiz.questions" t-as="question"
|
||||
t-attf-class="o_wslides_js_lesson_quiz_question my-3 #{widget.readonly ? 'disabled' : ''} #{widget.slide.completed ? 'completed-disabled' : ''}"
|
||||
t-attf-class="o_wslides_js_lesson_quiz_question mt-3 mb-4 #{widget.readonly ? 'disabled' : ''} #{widget.slide.completed ? 'completed-disabled' : ''}"
|
||||
t-att-data-question-id="question.id" t-att-data-title="question.question">
|
||||
<div class="h4">
|
||||
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/>
|
||||
|
||||
@@ -163,20 +163,20 @@
|
||||
<div class="col-12 col-md-8 col-lg-9">
|
||||
<ul class="nav nav-tabs o_wslides_nav_tabs flex-nowrap" role="tablist" id="profile_extra_info_tablist">
|
||||
<li class="nav-item o_wslides_course_header_nav_home">
|
||||
<a t-att-class="'nav-link %s' % ('' if search_slide_type else 'active')" id="home-tab" data-toggle="pill" href="#home" role="tab" aria-controls="home" aria-selected="true">
|
||||
<a t-att-class="'nav-link %s' % ('active' if active_tab == 'home' else '')" id="home-tab" data-toggle="pill" href="#home" role="tab" aria-controls="home" t-att-aria-selected="'true' if active_tab == 'home' else 'false'">
|
||||
<i class="fa fa-home"/> Course
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="channel.allow_comment" class="nav-item">
|
||||
<a class="nav-link" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" aria-selected="false">Review</a>
|
||||
<a t-att-class="'nav-link %s' % ('active' if active_tab == 'review' else '')" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" t-att-aria-selected="'true' if active_tab == 'review' else 'false'">Review</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content py-4 o_wslides_tabs_content mb-4" id="courseMainTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<div t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'home' else '')" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<t t-if="channel.channel_type == 'training'" t-call="website_slides.course_slides_list"/>
|
||||
</div>
|
||||
<div t-if="channel.allow_comment" class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab">
|
||||
<div t-if="channel.allow_comment" t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'review' else '')" id="review" role="tabpanel" aria-labelledby="review-tab">
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="channel"/>
|
||||
<t t-set="hash" t-value="message_post_hash"/>
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo><data>
|
||||
|
||||
<template id='courses_footer'>
|
||||
<!-- 'users': 3 top users with picture (skip users without picture) -->
|
||||
<section class="s_banner bg-900" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center">
|
||||
<div t-attf-class="col-12 #{'col-md' if top3_users else 'text-center'}">
|
||||
<h2 class="my-4 text-white">Meet The Heros Who Transformed Their Company!</h2>
|
||||
<a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-5" t-if="top3_users">
|
||||
<div class="ml-4">
|
||||
<t t-foreach="top3_users" t-as="user">
|
||||
<img class="rounded-circle img-fluid shadow ml-n4"
|
||||
style="border: 6px solid white"
|
||||
t-attf-src="/web/image/res.users/#{user['id']}/image_medium"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- Channels home template -->
|
||||
<template id='courses_home' name="Odoo Courses Homepage">
|
||||
<t t-set="body_classname" t-value="'o_wslides_body'"/>
|
||||
<t t-call="website.layout">
|
||||
<div class="wrap o_wslides_wrap">
|
||||
<div id="wrap" class="wrap o_wslides_wrap">
|
||||
<section class="s_banner overflow-hidden bg-900" style="background-image: url("/website_slides/static/src/img/banner_default.svg"); background-size: cover; background-position: 55% 65%">
|
||||
<div class="container align-items-center d-flex mb-5 mt-lg-5 pt-lg-4 pb-lg-1">
|
||||
<div>
|
||||
@@ -146,18 +169,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="s_banner" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-12 col-md">
|
||||
<h2 class="my-4 text-white">Meet the people who transformed their company</h2>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<t t-call="website_slides.courses_footer"></t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
@@ -264,18 +276,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="s_banner bg-900" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-12 col-md">
|
||||
<h2 class="my-4">Meet the people who transformed their company</h2>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<t t-call="website_slides.courses_footer"></t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
@@ -395,13 +396,13 @@
|
||||
<img t-attf-src="/web/image/gamification.karma.rank/#{user.rank_id.id}/image_small" height="16" alt="" class="mr-1"/>
|
||||
<a href="/profile/ranks_badges" t-field="user.rank_id"/>
|
||||
</div>
|
||||
<t t-if="user.next_rank_id">
|
||||
<div class="font-weight-bold text-muted mt-1">Next rank:</div>
|
||||
<t t-if="user.rank_id">
|
||||
<div t-if="user.next_rank_id" class="font-weight-bold text-muted mt-1">Next rank:</div>
|
||||
<t t-call="website_profile.profile_next_rank_card">
|
||||
<t t-set="bg_class">bg-200</t>
|
||||
<t t-set="img_max_width">50%</t>
|
||||
</t>
|
||||
<div t-field="user.next_rank_id.description_motivational"/>
|
||||
<div t-field="user.rank_id.description_motivational"/>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<t t-set="body_classname" t-value="'o_wslides_body'"/>
|
||||
<t t-call="website.layout">
|
||||
<t t-set="main_object" t-value="slide"/>
|
||||
<div class="wrap o_wslides_wrap">
|
||||
<div id="wrap" class="wrap o_wslides_wrap">
|
||||
<div class="o_wslides_lesson_header o_wslides_gradient position-relative text-white pb-0 pt-2 pt-md-5">
|
||||
<t t-call="website_slides.course_nav">
|
||||
<t t-set="channel" t-value="slide.channel_id"/>
|
||||
@@ -185,12 +185,12 @@
|
||||
<div class="row align-items-center my-3">
|
||||
<div class="col-12 col-md order-2 order-md-1 d-flex">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="my-0">
|
||||
<h1 class="h5 my-0">
|
||||
<t t-call="website_slides.slide_icon">
|
||||
<t t-set="icon_class">mr-1</t>
|
||||
</t>
|
||||
<span t-field="slide.name"/>
|
||||
</h4>
|
||||
</h1>
|
||||
<span t-if="slide.question_ids"
|
||||
t-att-class="'ml-2 badge %s' % ('badge-success' if channel_progress[slide.id].get('completed') else 'badge-info')">
|
||||
<span t-if="channel_progress[slide.id].get('completed')">
|
||||
@@ -427,7 +427,7 @@
|
||||
t-att-data-has-next="1 if next_slide else 0"
|
||||
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
|
||||
<div t-foreach="slide_questions" t-as="question"
|
||||
t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
|
||||
t-att-class="'o_wslides_js_lesson_quiz_question mt-3 mb-4 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
|
||||
<div class="h4">
|
||||
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<li class="nav-item">
|
||||
<a t-att-href="'/slides/%s%s?%s' % (slug(forum.slide_channel_id), '/%s' % slug(category) if category else '', keep_query('debug'))"
|
||||
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">
|
||||
<i class="fa fa-home"/> Home</a>
|
||||
<i class="fa fa-home"/> Course</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" t-att-class="'nav-link active o_wprofile_navlink'" style="border-left: 0px">Forum</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-att-href="'/slides/%s' % (slug(forum.slide_channel_id))"
|
||||
<a t-att-href="'/slides/%s?active_tab=review' % (slug(forum.slide_channel_id))"
|
||||
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">Review</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
Nepal, 21.05.2019
|
||||
|
||||
JankariTech Pvt. Ltd. agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Artur Neumann artur@jankaritech.com https://github.com/individual-it
|
||||
|
||||
List of contributors:
|
||||
|
||||
Artur Neumann artur@jankaritech.com https://github.com/individual-it
|
||||
|
||||
Phil Davis phil@jankaritech.com https://github.com/phil-davis
|
||||
|
||||
Paurakh Sharma Humagain paurakh011@gmail.com https://github.com/paurakhsharma
|
||||
|
||||
Saugat Pachhai suagatchhetri@outlook.com https://github.com/skshetry
|
||||
|
||||
Dipak Acharya dpakach@gmail.com https://github.com/dpakach
|
||||
@@ -0,0 +1,11 @@
|
||||
Mexico, 2018-12-05
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Luis Arturo Íñiguez Hernández luis.iniguezh@gmail.com https://github.com/luisiniguezh
|
||||
@@ -0,0 +1,11 @@
|
||||
Italy, 2019-06-04
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Michele Zaccheddu michelezak93@gmail.com https://github.com/MicheleZak
|
||||
@@ -787,3 +787,11 @@ class IrActionsActClient(models.Model):
|
||||
for record in self:
|
||||
params = record.params
|
||||
record.params_store = repr(params) if isinstance(params, dict) else params
|
||||
|
||||
def _get_default_form_view(self):
|
||||
doc = super(IrActionsActClient, self)._get_default_form_view()
|
||||
params = doc.find(".//field[@name='params']")
|
||||
params.getparent().remove(params)
|
||||
params_store = doc.find(".//field[@name='params_store']")
|
||||
params_store.getparent().remove(params_store)
|
||||
return doc
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class IrFilters(models.Model):
|
||||
@@ -41,7 +42,10 @@ class IrFilters(models.Model):
|
||||
@api.multi
|
||||
def _get_eval_domain(self):
|
||||
self.ensure_one()
|
||||
return ast.literal_eval(self.domain)
|
||||
return safe_eval(self.domain, {
|
||||
'datetime': datetime,
|
||||
'context_today': datetime.datetime.now,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _get_action_domain(self, action_id=None):
|
||||
|
||||
@@ -751,7 +751,7 @@ class IrModelFields(models.Model):
|
||||
else:
|
||||
if column_rename:
|
||||
raise UserError(_('Can only rename one field at a time!'))
|
||||
column_rename = (obj._table, item.name, vals['name'], item.index)
|
||||
column_rename = (obj._table, item.name, vals['name'], item.index, item.store)
|
||||
|
||||
# We don't check the 'state', because it might come from the context
|
||||
# (thus be set for multiple fields) and will be ignored anyway.
|
||||
@@ -769,10 +769,11 @@ class IrModelFields(models.Model):
|
||||
|
||||
if column_rename:
|
||||
# rename column in database, and its corresponding index if present
|
||||
table, oldname, newname, index = column_rename
|
||||
self._cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, oldname, newname))
|
||||
if index:
|
||||
self._cr.execute('ALTER INDEX "%s_%s_index" RENAME TO "%s_%s_index"' % (table, oldname, table, newname))
|
||||
table, oldname, newname, index, stored = column_rename
|
||||
if stored:
|
||||
self._cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, oldname, newname))
|
||||
if index:
|
||||
self._cr.execute('ALTER INDEX "%s_%s_index" RENAME TO "%s_%s_index"' % (table, oldname, table, newname))
|
||||
|
||||
if column_rename or patched_models:
|
||||
# setup models, this will reload all manual fields in registry
|
||||
@@ -1201,13 +1202,17 @@ class IrModelAccess(models.Model):
|
||||
:rtype: list
|
||||
"""
|
||||
assert access_mode in ('read', 'write', 'create', 'unlink'), 'Invalid access mode'
|
||||
self._cr.execute("""SELECT c.name, g.name
|
||||
FROM ir_model_access a
|
||||
JOIN ir_model m ON (a.model_id=m.id)
|
||||
JOIN res_groups g ON (a.group_id=g.id)
|
||||
LEFT JOIN ir_module_category c ON (c.id=g.category_id)
|
||||
WHERE m.model=%s AND a.active IS TRUE AND a.perm_""" + access_mode,
|
||||
(model_name,))
|
||||
self._cr.execute("""
|
||||
SELECT c.name, g.name
|
||||
FROM ir_model_access a
|
||||
JOIN ir_model m ON (a.model_id = m.id)
|
||||
JOIN res_groups g ON (a.group_id = g.id)
|
||||
LEFT JOIN ir_module_category c ON (c.id = g.category_id)
|
||||
WHERE m.model = %%s
|
||||
AND a.active = TRUE
|
||||
AND a.perm_%s = TRUE
|
||||
ORDER BY c.name, g.name NULLS LAST
|
||||
""" % access_mode, [model_name])
|
||||
return [('%s/%s' % x) if x[0] else x[1] for x in self._cr.fetchall()]
|
||||
|
||||
# The context parameter is useful when the method translates error messages.
|
||||
|
||||
@@ -209,7 +209,7 @@ class IrRule(models.Model):
|
||||
'document_model': model,
|
||||
'rules_list': '\n'.join('- %s' % rule.name for rule in rules),
|
||||
'multi_company_warning': ('\n' + _('Note: this might be a multi-company issue.') + '\n') if any(
|
||||
'company_id' in r.domain_force for r in rules) else '',
|
||||
'company_id' in (r.domain_force or []) for r in rules) else '',
|
||||
'example_records': list(records.ids[:6]),
|
||||
'user_id': self.env.user.id,
|
||||
})
|
||||
|
||||
@@ -246,7 +246,13 @@ class IrTranslation(models.Model):
|
||||
continue
|
||||
if not callable(field.translate):
|
||||
# Pass context without lang, need to read real stored field, not translation
|
||||
result = model.browse(record.res_id).with_context(lang=None).read([field_name])
|
||||
try:
|
||||
result = model.browse(record.res_id).with_context(lang=None).read([field_name])
|
||||
except AccessError:
|
||||
# because we can read self but not the record,
|
||||
# that means we would get an access error when accessing the translations
|
||||
# so instead we defer the access right to the "check" method
|
||||
result = [{field_name: _("Cannot be translated; record not accessible.")}]
|
||||
record.source = result[0][field_name] if result else False
|
||||
|
||||
def _inverse_source(self):
|
||||
@@ -538,7 +544,7 @@ class IrTranslation(models.Model):
|
||||
""" Check access rights of operation ``mode`` on ``self`` for the
|
||||
current user. Raise an AccessError in case conditions are not met.
|
||||
"""
|
||||
if self.env.user._is_admin():
|
||||
if self.env.user._is_superuser():
|
||||
return
|
||||
|
||||
# collect translated field records (model_ids) and other translations
|
||||
|
||||
@@ -38,13 +38,6 @@
|
||||
</rng:optional>
|
||||
|
||||
<rng:optional><rng:attribute name="precision"/></rng:optional>
|
||||
<rng:optional>
|
||||
<rng:element name="templates">
|
||||
<rng:oneOrMore>
|
||||
<rng:ref name="any"/>
|
||||
</rng:oneOrMore>
|
||||
</rng:element>
|
||||
</rng:optional>
|
||||
|
||||
<rng:optional><rng:attribute name="color"/></rng:optional>
|
||||
|
||||
@@ -54,11 +47,17 @@
|
||||
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="decoration-danger"/></rng:optional>
|
||||
|
||||
<rng:zeroOrMore>
|
||||
<rng:choice>
|
||||
<rng:ref name="field"/>
|
||||
</rng:choice>
|
||||
</rng:zeroOrMore>
|
||||
<rng:interleave>
|
||||
<rng:zeroOrMore><rng:ref name="field"/></rng:zeroOrMore>
|
||||
<rng:optional>
|
||||
<rng:element name="templates">
|
||||
<rng:oneOrMore>
|
||||
<rng:ref name="any"/>
|
||||
</rng:oneOrMore>
|
||||
</rng:element>
|
||||
</rng:optional>
|
||||
</rng:interleave>
|
||||
|
||||
<rng:empty/>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
</group>
|
||||
<group string="Advanced Properties" groups="base.group_no_one">
|
||||
<field name="related"/>
|
||||
<field name="depends"/>
|
||||
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
|
||||
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
||||
</group>
|
||||
<div groups="base.group_no_one">
|
||||
@@ -295,7 +295,7 @@
|
||||
</group>
|
||||
<group string="Advanced Properties" groups="base.group_no_one">
|
||||
<field name="related"/>
|
||||
<field name="depends" attrs="{'required': [('compute', '!=', False)] }"/>
|
||||
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
|
||||
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
||||
</group>
|
||||
<div groups="base.group_no_one">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user