[MERGE] forward port branch 12.0 up to cb8fefa899

This commit is contained in:
Christophe Simonis
2019-01-31 16:59:58 +01:00
158 changed files with 2063 additions and 375 deletions
+5 -9
View File
@@ -67,12 +67,11 @@ class AccountAccount(models.Model):
@api.constrains('user_type_id')
def _check_user_type_id(self):
data_unaffected_earnings = self.env.ref('account.data_unaffected_earnings')
for company in self.mapped('company_id'):
account_unaffected_earnings = self.search([
('company_id', '=', company.id),
('user_type_id', '=', data_unaffected_earnings.id),
])
if len(account_unaffected_earnings) >= 2:
result = self.read_group([('user_type_id', '=', data_unaffected_earnings.id)], ['company_id'], ['company_id'])
for res in result:
if res.get('company_id_count', 0) >= 2:
account_unaffected_earnings = self.search([('company_id', '=', res['company_id'][0]),
('user_type_id', '=', data_unaffected_earnings.id)])
raise ValidationError(_('You cannot have more than one account with "Current Year Earnings" as type. (accounts: %s)') % [a.code for a in account_unaffected_earnings])
name = fields.Char(required=True, index=True)
@@ -228,7 +227,6 @@ class AccountAccount(models.Model):
self.group_id = group
@api.multi
@api.depends('name', 'code')
def name_get(self):
result = []
for account in self:
@@ -801,7 +799,6 @@ class AccountJournal(models.Model):
}).id
@api.multi
@api.depends('name', 'currency_id', 'company_id', 'company_id.currency_id')
def name_get(self):
res = []
for journal in self:
@@ -946,7 +943,6 @@ class AccountTax(models.Model):
default = dict(default or {}, name=_("%s (Copy)") % self.name)
return super(AccountTax, self).copy(default=default)
@api.depends('name', 'type_tax_use')
def name_get(self):
if not self._context.get('append_type_to_tax_name'):
return super(AccountTax, self).name_get()
@@ -550,6 +550,8 @@ class AccountBankStatementLine(models.Model):
:returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains
the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*).
"""
payable_account_type = self.env.ref('account.data_account_type_payable')
receivable_account_type = self.env.ref('account.data_account_type_receivable')
counterpart_aml_dicts = counterpart_aml_dicts or []
payment_aml_rec = payment_aml_rec or self.env['account.move.line']
new_aml_dicts = new_aml_dicts or []
@@ -570,10 +572,16 @@ class AccountBankStatementLine(models.Model):
raise UserError(_('A selected move line was already reconciled.'))
if isinstance(aml_dict['move_line'], int):
aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line'])
account_types = self.env['account.account.type']
for aml_dict in (counterpart_aml_dicts + new_aml_dicts):
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], int):
# Transform the value in the format required for One2many and Many2many fields
aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']]
user_type_id = self.env['account.account'].browse(aml_dict.get('account_id')).user_type_id
if user_type_id in [payable_account_type, receivable_account_type] and user_type_id not in account_types:
account_types |= user_type_id
if any(line.journal_entry_ids for line in self):
raise UserError(_('A selected statement line was already reconciled with an account move.'))
@@ -609,6 +617,8 @@ class AccountBankStatementLine(models.Model):
payment = self.env['account.payment']
if abs(total)>0.00001:
payment_vals = self._prepare_payment_vals(total)
if self.partner_id and len(account_types) == 1:
payment_vals['partner_type'] = 'customer' if account_types == receivable_account_type else 'supplier'
payment = payment.create(payment_vals)
# Complete dicts to create both counterpart move lines and write-offs
+25 -1
View File
@@ -147,8 +147,13 @@ class AccountInvoice(models.Model):
amount_to_show = currency._convert(abs(line.amount_residual), self.currency_id, self.company_id, line.date or fields.Date.today())
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
if line.ref :
title = '%s : %s' % (line.move_id.name, line.ref)
else:
title = line.move_id.name
info['content'].append({
'journal_name': line.ref or line.move_id.name,
'title': title,
'amount': amount_to_show,
'currency': currency_id.symbol,
'id': line.id,
@@ -1402,6 +1407,20 @@ class AccountInvoice(models.Model):
return result
@api.model
def _refund_tax_lines_account_change(self, lines, taxes_to_change):
# Let's change the account on tax lines when
# @param {list} lines: a list of orm commands
# @param {dict} taxes_to_change
# key: tax ID, value: refund account
if not taxes_to_change:
return lines
for line in lines:
if isinstance(line[2], dict) and line[2]['tax_id'] in taxes_to_change:
line[2]['account_id'] = taxes_to_change[line[2]['tax_id']]
return lines
def _get_refund_common_fields(self):
return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id']
@@ -1447,7 +1466,12 @@ class AccountInvoice(models.Model):
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
tax_lines = invoice.tax_line_ids
values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines)
taxes_to_change = {
line.tax_id.id: line.tax_id.refund_account_id.id
for line in tax_lines.filtered(lambda l: l.tax_id.refund_account_id != l.tax_id.account_id)
}
cleaned_tax_lines = self._refund_cleanup_lines(tax_lines)
values['tax_line_ids'] = self._refund_tax_lines_account_change(cleaned_tax_lines, taxes_to_change)
if journal_id:
journal = self.env['account.journal'].browse(journal_id)
@@ -185,13 +185,14 @@ class account_journal(models.Model):
self.env.cr.execute(query, query_args)
query_results_drafts = self.env.cr.dictfetchall()
today = datetime.today()
query = """SELECT amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';"""
today = fields.Date.today()
query = """SELECT residual_signed as amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';"""
self.env.cr.execute(query, (self.id, today))
late_query_results = self.env.cr.dictfetchall()
(number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency)
(number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency)
(number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency)
curr_cache = {}
(number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency, curr_cache=curr_cache)
(number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency, curr_cache=curr_cache)
(number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency, curr_cache=curr_cache)
difference = currency.round(last_balance-account_sum) + 0.0
return {
@@ -226,23 +227,45 @@ class account_journal(models.Model):
gather the bills in draft state data, and the arguments
dictionary to use to run it as its second.
"""
return ("""SELECT state, amount_total, currency_id AS currency, type, date_invoice, company_id
FROM account_invoice
# there is no account_move_lines for draft invoices, so no relevant residual_signed value
return ("""SELECT state,
(CASE WHEN inv.type in ('out_invoice', 'in_invoice')
THEN inv.amount_total
ELSE (-1 * inv.amount_total)
END) AS amount_total,
inv.currency_id AS currency,
inv.type,
inv.date_invoice,
inv.company_id
FROM account_invoice inv
WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id})
def _count_results_and_sum_amounts(self, results_dict, target_currency):
def _count_results_and_sum_amounts(self, results_dict, target_currency, curr_cache=None):
""" Loops on a query result to count the total number of invoices and sum
their amount_total field (expressed in the given target currency).
amount_total must be signed !
"""
rslt_count = 0
rslt_sum = 0.0
# Create a cache with currency rates to avoid unnecessary SQL requests. Do not copy
# curr_cache on purpose, so the dictionary is modified and can be re-used for subsequent
# calls of the method.
curr_cache = {} if curr_cache is None else curr_cache
for result in results_dict:
cur = self.env['res.currency'].browse(result.get('currency'))
company = self.env['res.company'].browse(result.get('company_id')) or self.env.user.company_id
rslt_count += 1
type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1
rslt_sum += type_factor * cur._convert(
result.get('amount_total'), target_currency, company, result.get('date_invoice') or fields.Date.today())
date = result.get('date_invoice') or fields.Date.today()
amount = result.get('amount_total', 0)
if cur != target_currency:
key = (cur, target_currency, company, date)
# Using setdefault will call _get_conversion_rate, so we explicitly check the
# existence of the key in the cache instead.
if key not in curr_cache:
curr_cache[key] = self.env['res.currency']._get_conversion_rate(*key)
amount *= curr_cache[key]
rslt_sum += target_currency.round(amount)
return (rslt_count, rslt_sum)
@api.multi
@@ -360,7 +383,7 @@ class account_journal(models.Model):
action['view_id'] = False
if self.type == 'purchase':
new_help = self.env['account.invoice'].with_context(ctx).complete_empty_list_help()
action.update({'help': action.get('help', '') + new_help})
action.update({'help': (action.get('help') or '') + new_help})
return action
@api.multi
+26 -17
View File
@@ -410,9 +410,6 @@ class AccountMove(models.Model):
journal_id=journal_id,
auto=auto)
reversed_moves |= reversed_move
#unreconcile all lines reversed
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
aml.remove_move_reconcile()
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
for account in set([x.account_id for x in aml]):
to_rec = aml.filtered(lambda y: y.account_id == account)
@@ -429,6 +426,18 @@ class AccountMove(models.Model):
def open_reconcile_view(self):
return self.line_ids.open_reconcile_view()
# FIXME: Clarify me and change me in master
@api.multi
def action_duplicate(self):
self.ensure_one()
action = self.env.ref('account.action_move_journal_line').read()[0]
action['target'] = 'inline'
action['context'] = dict(self.env.context)
action['context']['view_no_maturity'] = False
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
action['res_id'] = self.copy().id
return action
@api.model
def _run_reverses_entries(self):
''' This method is called from a cron job. '''
@@ -618,7 +627,7 @@ class AccountMoveLine(models.Model):
index=True, store=True, copy=False) # related is required
blocked = fields.Boolean(string='No Follow-up', default=False,
help="You can check this box to mark this journal item as a litigation with the associated partner")
date_maturity = fields.Date(string='Due date', index=True, required=True,
date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False,
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required
analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines")
@@ -877,22 +886,13 @@ class AccountMoveLine(models.Model):
ret = self._reconcile_lines(debit_moves, credit_moves, field)
return ret
@api.multi
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
# Empty self can happen if the user tries to reconcile entries which are already reconciled.
# The calling method might have filtered out reconciled lines.
if not self:
return True
def _check_reconcile_validity(self):
#Perform all checks on lines
company_ids = set()
all_accounts = []
partners = set()
for line in self:
company_ids.add(line.company_id.id)
all_accounts.append(line.account_id)
if (line.account_id.internal_type in ('receivable', 'payable')):
partners.add(line.partner_id.id)
if line.reconciled:
raise UserError(_('You are trying to reconcile some entries that are already reconciled.'))
if len(company_ids) > 1:
@@ -902,6 +902,14 @@ class AccountMoveLine(models.Model):
if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
raise UserError(_('Account %s (%s) does not allow reconciliation. First change the configuration of this account to allow it.') % (all_accounts[0].name, all_accounts[0].code))
@api.multi
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
# Empty self can happen if the user tries to reconcile entries which are already reconciled.
# The calling method might have filtered out reconciled lines.
if not self:
return True
self._check_reconcile_validity()
#reconcile everything that can be
remaining_moves = self.auto_reconcile_lines()
@@ -919,7 +927,7 @@ class AccountMoveLine(models.Model):
#add writeoff line to reconcile algorithm and finish the reconciliation
remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines()
# Check if reconciliation is total or needs an exchange rate entry to be created
(self+writeoff_to_reconcile).check_full_reconcile()
(self + writeoff_to_reconcile).check_full_reconcile()
return True
def _create_writeoff(self, writeoff_vals):
@@ -963,8 +971,9 @@ class AccountMoveLine(models.Model):
raise UserError(_("Either pass both debit and credit or none."))
if 'date' not in vals:
vals['date'] = self._context.get('date_p') or fields.Date.today()
if vals['date'] < date:
date = vals['date']
vals['date'] = fields.Date.to_date(vals['date'])
if vals['date'] and vals['date'] < date:
date = vals['date']
if 'name' not in vals:
vals['name'] = self._context.get('comment') or _('Write-Off')
if 'analytic_account_id' not in vals:
+8 -1
View File
@@ -324,7 +324,7 @@ class account_register_payments(models.TransientModel):
pmt_communication = self.show_communication_field and self.communication \
or self.group_invoices and ' '.join([inv.reference or inv.number for inv in invoices]) \
or invoices[0].reference # in this case, invoices contains only one element, since group_invoices is False
return {
values = {
'journal_id': self.journal_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_date': self.payment_date,
@@ -337,8 +337,13 @@ class account_register_payments(models.TransientModel):
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
'partner_bank_account_id': bank_account.id,
'multi': False,
'payment_difference_handling': self.payment_difference_handling,
'writeoff_account_id': self.writeoff_account_id.id,
'writeoff_label': self.writeoff_label,
}
return values
@api.multi
def get_payments_vals(self):
'''Compute the values for payments.
@@ -429,6 +434,8 @@ class account_payment(models.Model):
if move_line.account_id.reconcile:
move_line_id = move_line.id
break;
if not self.partner_id:
raise UserError(_("Payments without a customer can't be matched"))
action_context = {'company_ids': [self.company_id.id], 'partner_ids': [self.partner_id.commercial_partner_id.id]}
if self.partner_type == 'customer':
action_context.update({'mode': 'customers'})
+1 -1
View File
@@ -173,7 +173,7 @@ class AccountChartTemplate(models.Model):
# (won't work well for multi-company)
company = self.env.user.company_id
# If we don't have any chart of account on this company, install this chart of account
if not company.chart_template_id:
if not self.existing_accounting(company):
self.load_for_current_company(15.0, 15.0)
def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
+6 -1
View File
@@ -381,8 +381,13 @@ Best Regards,'''))
('user_type_id', '=', unaffected_earnings_type.id)])
if account:
return account[0]
# Do not assume '999999' doesn't exist since the user might have created such an account
# manually.
code = 999999
while self.env['account.account'].search([('code', '=', str(code)), ('company_id', '=', self.id)]):
code -= 1
return self.env['account.account'].create({
'code': '999999',
'code': str(code),
'name': _('Undistributed Profits/Losses'),
'user_type_id': unaffected_earnings_type.id,
'company_id': self.id,
+1 -1
View File
@@ -204,7 +204,7 @@ class ResPartner(models.Model):
@api.multi
def _credit_debit_get(self):
tables, where_clause, where_params = self.env['account.move.line']._query_get()
tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get()
where_params = [tuple(self.ids)] + where_params
if where_clause:
where_clause = 'AND ' + where_clause
+21 -1
View File
@@ -224,6 +224,27 @@ class AccountReconciliation(models.AbstractModel):
""" Returns the data required for the invoices & payments matching of partners/accounts.
If an argument is None, fetch all related reconciliations. Use [] to fetch nothing.
"""
MoveLine = self.env['account.move.line']
aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids'))
if aml_ids:
aml = MoveLine.browse(aml_ids)
aml._check_reconcile_validity()
account = aml[0].account_id
currency = account.currency_id or account.company_id.currency_id
return {
'accounts': [{
'reconciliation_proposition': self._prepare_move_lines(aml, target_currency=currency),
'company_id': account.company_id.id,
'currency_id': currency.id,
'mode': 'accounts',
'account_id': account.id,
'account_name': account.name,
'account_code': account.code,
}],
'customers': [],
'suppliers': [],
}
return {
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'),
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'),
@@ -344,7 +365,6 @@ class AccountReconciliation(models.AbstractModel):
# be reconciled.
return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']]
@api.model
def process_move_lines(self, data):
""" Used to validate a batch of reconciliations in a single call
@@ -142,6 +142,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
if not self.env.user.company_id.currency_id.is_zero(line_amount):
partners_amount[partner_id] += line_amount
lines.setdefault(partner_id, [])
lines[partner_id].append({
'line': line,
'amount': line_amount,
@@ -179,6 +180,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, user_company, date_from)
if not self.env.user.company_id.currency_id.is_zero(line_amount):
undue_amounts[partner_id] += line_amount
lines.setdefault(partner_id, [])
lines[partner_id].append({
'line': line,
'amount': line_amount,
@@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({
field_registry.add('payment', ShowPaymentLineWidget);
return {
ShowPaymentLineWidget: ShowPaymentLineWidget
};
});
@@ -858,6 +858,7 @@ var StatementModel = BasicModel.extend({
'tax_id': [tax.id, null],
'amount': tax.amount,
'label': tax.name,
'date': prop.date,
'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,
'analytic': tax.analytic,
'is_tax': true,
@@ -1072,6 +1073,7 @@ var StatementModel = BasicModel.extend({
*/
_formatQuickCreate: function (line, values) {
values = values || {};
var today = new moment().utc().format();
var account = this._formatNameGet(values.account_id);
var formatOptions = {
currency_id: line.st_line.currency_id,
@@ -1088,6 +1090,7 @@ var StatementModel = BasicModel.extend({
'tax_id': this._formatNameGet(values.tax_id),
'debit': 0,
'credit': 0,
'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}),
'base_amount': values.amount_type !== "percentage" ?
(amount) : line.balance.amount * values.amount / 100,
'percent': values.amount_type === "percentage" ? values.amount : null,
@@ -1238,7 +1241,7 @@ var StatementModel = BasicModel.extend({
* datas allowing manual reconciliation
*/
var ManualModel = StatementModel.extend({
quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'],
quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'date'],
//--------------------------------------------------------------------------
// Public
@@ -1636,6 +1639,12 @@ var ManualModel = StatementModel.extend({
})
.then(this._formatMoveLine.bind(this, handle));
},
_formatToProcessReconciliation: function (line, prop) {
var result = this._super(line, prop);
result['date'] = prop.date;
return result;
},
});
return {
@@ -542,10 +542,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}, {
type: 'float',
name: 'amount',
}, {
type: 'char', //TODO is it a bug or a feature when type date exists ?
name: 'date',
}], {
account_id: {string: _t("Account")},
label: {string: _t("Label")},
amount: {string: _t("Account")}
amount: {string: _t("Account")},
}).then(function (recordID) {
self.handleCreateRecord = recordID;
var record = self.model.get(self.handleCreateRecord);
@@ -573,6 +576,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
self.fields.amount = new basic_fields.FieldFloat(self,
'amount', record, {mode: 'edit'});
self.fields.date = new basic_fields.FieldDate(self,
'date', record, {mode: 'edit'});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'))
@@ -586,6 +592,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.then(addRequiredStyle.bind(self, self.fields.label));
self.fields.amount.appendTo($create.find('.create_amount .o_td_field'))
.then(addRequiredStyle.bind(self, self.fields.amount));
self.fields.date.appendTo($create.find('.create_date .o_td_field'))
self.$('.create').append($create);
function addRequiredStyle(widget) {
@@ -893,6 +900,7 @@ var ManualLineRenderer = LineRenderer.extend({
_renderCreate: function (state) {
this._super(state);
this.$('.create .create_journal_id').show();
this.$('.create .create_date').removeClass('d-none')
this.$('.create .create_journal_id .o_input').addClass('o_required_modifier');
},
@@ -16,8 +16,8 @@
<td>
<a title="assign to invoice" role="button" class="oe_form_field btn btn-link outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;" href="#">Add</a>
</td>
<td>
<span class="oe_form_field" style="margin-right: 30px;"><t t-esc="line.journal_name"></t></span>
<td style="max-width: 10em;">
<div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.title"><t t-esc="line.journal_name"></t></div>
</td>
</t>
<t t-if="!outstanding">
@@ -212,6 +212,10 @@
<td class="o_td_label">Tax Included in Price</td>
<td class="o_td_field"></td>
</tr>
<tr class="create_date d-none">
<td class="o_td_label">Writeoff Date</td>
<td class="o_td_field"></td>
</tr>
</tbody>
</table>
<div class="add_line_container">
@@ -649,8 +649,8 @@ QUnit.module('account', {
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait");
assert.strictEqual(clientAction.widgets[2].$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp");
widget.$('.accounting_view tfoot td:first').trigger('click');
assert.containsN(widget, '.create input.o_input', 7,
"create panel should contain 7 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount)");
assert.containsN(widget, '.create input.o_input', 8,
"create panel should contain 8 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount, date)");
assert.containsN(widget, '.create .create_account_id .o_required_modifier, .create .create_label .o_required_modifier, .create .create_amount .o_required_modifier', 3,
"account_id, label and amount should be required fields");
assert.strictEqual(widget.$('.create .create_label input').val(), 'SAJ/2014/002 and SAJ/2014/003',
@@ -1,3 +1,5 @@
from unittest.mock import patch
from odoo.addons.account.tests.account_test_users import AccountTestUsers
import datetime
from odoo.tests import tagged
@@ -165,3 +167,174 @@ class TestAccountCustomerInvoice(AccountTestUsers):
))
self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids]))
def test_customer_invoice_tax_refund(self):
company = self.env.user.company_id
tax_account = self.env['account.account'].create({
'name': 'TAX',
'code': 'TAX',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
'company_id': company.id,
})
tax_refund_account = self.env['account.account'].create({
'name': 'TAX_REFUND',
'code': 'TAX_R',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
'company_id': company.id,
})
journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0]
partner3 = self.env.ref('base.res_partner_3')
account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id
tax = self.env['account.tax'].create({
'name': 'Tax 15.0',
'amount': 15.0,
'amount_type': 'percent',
'type_tax_use': 'sale',
'account_id': tax_account.id,
'refund_account_id': tax_refund_account.id
})
invoice_line_data = [
(0, 0,
{
'product_id': self.env.ref('product.product_product_1').id,
'quantity': 40.0,
'account_id': account_id,
'name': 'product test 1',
'discount': 10.00,
'price_unit': 2.27,
'invoice_line_tax_ids': [(6, 0, [tax.id])],
}
)]
invoice = self.env['account.invoice'].create(dict(
name="Test Customer Invoice",
journal_id=journalrec.id,
partner_id=partner3.id,
invoice_line_ids=invoice_line_data
))
invoice.action_invoice_open()
refund = invoice.refund()
self.assertEqual(invoice.tax_line_ids.mapped('account_id'), tax_account)
self.assertEqual(refund.tax_line_ids.mapped('account_id'), tax_refund_account)
def test_customer_invoice_dashboard(self):
def patched_today(*args, **kwargs):
return '2019-01-22'
date_invoice = '2019-01-21'
partner3 = self.env.ref('base.res_partner_3')
account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id
journal = self.env['account.journal'].create({
'name': 'sale_0',
'code': 'SALE0',
'type': 'sale',
})
invoice_line_data = [
(0, 0,
{
'product_id': self.env.ref('product.product_product_1').id,
'quantity': 40.0,
'account_id': account_id,
'name': 'product test 1',
'discount': 10.00,
'price_unit': 2.27,
}
)
]
invoice = self.env['account.invoice'].create(dict(
name="Test Customer Invoice",
journal_id=journal.id,
partner_id=partner3.id,
invoice_line_ids=invoice_line_data,
date_invoice=date_invoice,
))
refund_line_data = [
(0, 0,
{
'product_id': self.env.ref('product.product_product_1').id,
'quantity': 1.0,
'account_id': account_id,
'name': 'product test 1',
'price_unit': 13.3,
}
)]
refund = self.env['account.invoice'].create(dict(
name="Test Customer Refund",
type='out_refund',
journal_id=journal.id,
partner_id=partner3.id,
invoice_line_ids=refund_line_data,
date_invoice=date_invoice,
))
# Check Draft
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 2)
self.assertEquals(dashboard_data['sum_draft'], '$ 68.42')
self.assertEquals(dashboard_data['number_waiting'], 0)
self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00')
# Check Both
invoice.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 1)
self.assertEquals(dashboard_data['sum_draft'], '$ -13.30')
self.assertEquals(dashboard_data['number_waiting'], 1)
self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72')
# Check waiting payment
refund.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
self.assertEquals(dashboard_data['number_waiting'], 2)
self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42')
# Check partial
receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable')
payment_move = self.env['account.move'].create({
'journal_id': journal.id,
})
payment_move_line = self.env['account.move.line'].with_context(check_move_validity=False).create({
'move_id': payment_move.id,
'account_id': receivable_account.id,
'debit': 10.00,
})
self.env['account.move.line'].with_context(check_move_validity=False).create({
'move_id': payment_move.id,
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_liquidity').id)], limit=1).id,
'credit': 10.00,
})
payment_move.post()
refund.register_payment(payment_move_line)
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
self.assertEquals(dashboard_data['number_waiting'], 2)
self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42')
with patch('odoo.fields.Date.today', patched_today):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_late'], 2)
self.assertEquals(dashboard_data['sum_late'], '$ 78.42')
+34
View File
@@ -595,3 +595,37 @@ class TestPayment(AccountingTestCase):
# The invoice should now be paid
self.assertEqual(invoice.state, 'paid', "Invoice should be in 'paid' state after having reconciled the two payments with a bank statement")
def test_register_payments_multi_invoices(self):
inv1 = self.create_invoice(amount=40)
inv2 = inv1.copy()
inv2.action_invoice_open()
batch_payment = self.env['account.register.payments'].with_context(active_ids=(inv1 + inv2).ids).create({
'amount': 70,
'partner_id': inv1.partner_id.id,
'journal_id': self.bank_journal_usd.id,
'invoice_ids': [(6, False, (inv1 + inv2).ids)],
'partner_type': 'customer',
'payment_difference_handling': 'reconcile',
'payment_type': 'inbound',
'payment_method_id': self.payment_method_manual_in.id,
'writeoff_account_id': self.account_revenue.id,
'writeoff_label': "why can't we live together"})
payment_id = batch_payment.create_payments()['res_id']
payment_id = self.env['account.payment'].browse(payment_id)
self.assertEqual(inv1.state, 'paid')
self.assertEqual(inv2.state, 'paid')
receivable_inv1 = inv1.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
receivable_inv2 = inv2.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
receivable_payment = payment_id.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(
receivable_inv1.full_reconcile_id.reconciled_line_ids,
receivable_inv1 + receivable_inv2 + receivable_payment
)
write_off_line = payment_id.move_line_ids.filtered(lambda l: l.account_id == payment_id.writeoff_account_id)
self.assertEqual(write_off_line.debit, 10)
+2 -1
View File
@@ -1556,10 +1556,11 @@
<field name="name">account.move.form</field>
<field name="model">account.move</field>
<field name="arch" type="xml">
<form string="Account Entry">
<form string="Account Entry" duplicate="false">
<header>
<button name="action_post" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/>
<button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/>
<button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
+10 -2
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo import api, fields, models, _
from odoo.addons.mail.wizard.mail_compose_message import _reopen
class AccountInvoiceSend(models.TransientModel):
@@ -67,3 +67,11 @@ class AccountInvoiceSend(models.TransientModel):
if self.is_print:
return self._print_document()
return {'type': 'ir.actions.act_window_close'}
@api.multi
def save_as_template(self):
self.ensure_one()
self.composer_id.save_as_template()
action = _reopen(self, self.id, self.model, context=self._context)
action.update({'name': _('Send Invoice')})
return action
@@ -9,7 +9,7 @@ import zipfile
from odoo import api, models
_logger = logging.getLogger(__name__)
FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc', 'pdf']
FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc']
def textToString(element):
buff = u""
@@ -92,6 +92,9 @@ class IrAttachment(models.Model):
def _index_pdf(self, bin_data):
'''Index PDF documents'''
# extractText gives very bad results for indexing, hence we don't index PDF anymore. A
# better alternative is probably PDFMiner.six, but not for stable.
# See POC at https://github.com/odoo/odoo/pull/27568.
buf = u""
if bin_data.startswith(b'%PDF-'):
f = io.BytesIO(bin_data)
@@ -15,9 +15,10 @@ class Partner(models.Model):
@api.onchange('city_id')
def _onchange_city_id(self):
self.city = self.city_id.name
self.zip = self.city_id.zipcode
self.state_id = self.city_id.state_id
if self.city_id:
self.city = self.city_id.name
self.zip = self.city_id.zipcode
self.state_id = self.city_id.state_id
@api.model
def _fields_view_get_address(self, arch):
@@ -37,7 +38,10 @@ class Partner(models.Model):
}"
/>
<field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s"
context="{'default_country_id': country_id}"
context="{'default_country_id': country_id,
'default_name': city,
'default_zipcode': zip,
'default_state_id': state_id}"
domain="[('country_id', '=', country_id)]"
attrs="{
'invisible': [('country_enforce_cities', '=', False)],
@@ -24,6 +24,7 @@ class BaseGengoTranslations(models.TransientModel):
GROUPS = ['base.group_system']
_name = 'base.gengo.translations'
_description = 'Base Gengo Translations'
@api.model
def default_get(self, fields):
+3 -2
View File
@@ -15,13 +15,14 @@ class ResPartner(models.Model):
self.env.cr.execute("""
SELECT
U.partner_id as id,
CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
ELSE 'online'
END as status
FROM bus_presence B
JOIN res_users U ON B.user_id = U.id
WHERE U.partner_id IN %s AND U.active = 't'
GROUP BY U.partner_id
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
for partner in self:
+1
View File
@@ -888,6 +888,7 @@ for rec in records:
name="Team Pipelines"
parent="crm_menu_sales"
action="sales_team.crm_team_salesteams_pipelines_act"
groups="sales_team.group_sale_manager"
sequence="4"/>
<menuitem id="res_partner_menu_customer"
+1 -1
View File
@@ -70,7 +70,7 @@ class DeliveryCarrier(models.Model):
def install_more_provider(self):
return {
'name': 'New Providers',
'view_mode': 'kanban',
'view_mode': 'kanban,form',
'res_model': 'ir.module.module',
'domain': [['name', 'ilike', 'delivery_']],
'type': 'ir.actions.act_window',
@@ -21,6 +21,7 @@ class ChooseDeliveryPackage(models.TransientModel):
string='Shipping Weight',
default=lambda self: self._default_shipping_weight()
)
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name')
def _default_stock_quant_package_id(self):
if self.env.context.get('default_stock_quant_package_id'):
@@ -45,6 +46,12 @@ class ChooseDeliveryPackage(models.TransientModel):
total_weight = sum([po.qty_done * po.product_id.weight for po in move_line_ids])
return total_weight
@api.depends('stock_quant_package_id', 'delivery_packaging_id')
def _compute_weight_uom_name(self):
weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
for package in self:
package.weight_uom_name = weight_uom_id.name
@api.onchange('delivery_packaging_id', 'shipping_weight')
def _onchange_packaging_weight(self):
if self.delivery_packaging_id.max_weight and self.shipping_weight > self.delivery_packaging_id.max_weight:
@@ -11,7 +11,7 @@
<label for="shipping_weight" attrs="{'invisible': [('delivery_packaging_id', '=', False)]}"/>
<div class="o_row" attrs="{'invisible': [('delivery_packaging_id', '=', False)]}">
<field name="shipping_weight"/>
<span>kg</span>
<field name="weight_uom_name"/>
</div>
</group>
<footer>
+3 -7
View File
@@ -23,7 +23,7 @@ class FleetVehicle(models.Model):
license_plate = fields.Char(track_visibility="onchange",
help='License plate number of the vehicle (i = plate number for a car)')
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
driver_id = fields.Many2one('res.partner', 'Driver', track_visibility="onchange", help='Driver of the vehicle', copy=False)
driver_id = fields.Many2one('res.partner', 'Driver', track_visibility="onchange", help='Driver of the vehicle', copy=False, auto_join=True)
model_id = fields.Many2one('fleet.vehicle.model', 'Model',
track_visibility="onchange", required=True, help='Model of the vehicle')
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Brand', related="model_id.brand_id", store=True, readonly=False)
@@ -59,6 +59,7 @@ class FleetVehicle(models.Model):
fuel_type = fields.Selection([
('gasoline', 'Gasoline'),
('diesel', 'Diesel'),
('lpg', 'LPG'),
('electric', 'Electric'),
('hybrid', 'Hybrid')
], 'Fuel Type', help='Fuel Used by the vehicle')
@@ -228,12 +229,7 @@ class FleetVehicle(models.Model):
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
domain = args or []
domain = expression.AND([domain, [('name', operator, name)]])
# we don't want to override the domain's filter on driver_id if present
if not any(['driver_id' in element for element in domain]):
partner_ids = self.env['res.partner']._search([('name', operator, name)], access_rights_uid=name_get_uid)
if partner_ids:
domain = expression.OR([domain, ['|', ('driver_id', 'in', partner_ids), ('driver_id', '=', False)]])
domain = expression.AND([domain, ['|', ('name', operator, name), ('driver_id.name', operator, name)]])
rec = self._search(domain, limit=limit, access_rights_uid=name_get_uid)
return self.browse(rec).name_get()
+115
View File
@@ -0,0 +1,115 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * crm_project
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:17+0000\n"
"PO-Revision-Date: 2018-08-24 09:17+0000\n"
"Language-Team: Filipino (https://www.transifex.com/odoo/teams/41243/fil/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: fil\n"
"Plural-Forms: nplurals=2; plural=(n == 1 || n==2 || n==3) || (n % 10 != 4 || n % 10 != 6 || n % 10 != 9);\n"
#. module: crm_project
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_view_form
msgid ""
"<span class=\"o_stat_text\">Convert To</span>\n"
" <span class=\"o_stat_text\">Task</span>"
msgstr ""
#. module: crm_project
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
msgid "Cancel"
msgstr ""
#. module: crm_project
#: model:ir.actions.act_window,name:crm_project.crm_lead_convert2task_action
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_view_form
msgid "Convert to Task"
msgstr ""
#. module: crm_project
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
msgid "Create Task"
msgstr ""
#. module: crm_project
#: selection:crm.lead.convert2task,action:0
msgid "Create a new customer"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__create_uid
msgid "Created by"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__create_date
msgid "Created on"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__partner_id
msgid "Customer"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__display_name
msgid "Display Name"
msgstr ""
#. module: crm_project
#: selection:crm.lead.convert2task,action:0
msgid "Do not link to a customer"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__id
msgid "ID"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task____last_update
msgid "Last Modified on"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__write_uid
msgid "Last Updated by"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__write_date
msgid "Last Updated on"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__lead_id
msgid "Lead"
msgstr ""
#. module: crm_project
#: model:ir.model,name:crm_project.model_crm_lead_convert2task
msgid "Lead convert to Task"
msgstr ""
#. module: crm_project
#: selection:crm.lead.convert2task,action:0
msgid "Link to an existing customer"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__project_id
msgid "Project"
msgstr ""
#. module: crm_project
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__action
msgid "Related Customer"
msgstr ""
+2 -1
View File
@@ -497,7 +497,8 @@ class HolidaysRequest(models.Model):
holiday.sudo().action_validate()
holiday.message_subscribe(partner_ids=[holiday._get_responsible_for_approval().partner_id.id])
holiday.sudo().message_post(body=_("The leave has been automatically approved"), subtype="mt_comment") # Message from OdooBot (sudo)
holiday.activity_update()
if not self._context.get('import_file'):
holiday.activity_update()
return holiday
def _read_from_database(self, field_names, inherited_field_names=[]):
+20 -11
View File
@@ -45,15 +45,21 @@ class FleetVehicle(models.Model):
elif contract.cost_frequency == "yearly":
car.total_cost += contract.cost_generated / 12.0
def _get_co2_fee(self, co2):
return max((((co2 * 9.0) - 600.0) * 1.2488) / 12.0, 0.0)
def _get_co2_fee(self, co2, fuel_type):
fuel_coefficient = {'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600}
co2_fee = 0
if fuel_type and fuel_type != 'electric':
if not co2:
co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182
co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0
return max(co2_fee, 26.47)
@api.depends('co2')
@api.depends('co2', 'fuel_type')
def _compute_co2_fee(self):
for car in self:
car.co2_fee = self._get_co2_fee(car.co2)
car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)
@api.depends('fuel_type', 'car_value', 'acquisition_date')
@api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
def _compute_car_atn(self):
for car in self:
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
@@ -111,15 +117,18 @@ class FleetVehicle(models.Model):
atn = 0.0
else:
if fuel_type in ['diesel', 'hybrid']:
reference = 87.0
reference = 88.0
else:
reference = 105.0
reference = 107.0
if not co2:
co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205
if co2 <= reference:
atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff
else:
atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff
return max(1280, atn) / 12.0
return max(1310, atn) / 12.0
@api.onchange('model_id')
def _onchange_model_id(self):
@@ -140,7 +149,7 @@ class FleetVehicleModel(models.Model):
default_recurring_cost_amount_depreciated = fields.Float(string="Cost (Depreciated)",
help="Default recurring cost amount that should be applied to a new car from this model")
default_co2 = fields.Float(string="CO2 emissions")
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('lpg', 'LPG'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
default_car_value = fields.Float(string="Catalog Value (VAT Incl.)")
can_be_requested = fields.Boolean(string="Can be requested", help="Can be requested on a contract as a new car")
default_atn = fields.Float(compute='_compute_atn', string="ATN")
@@ -158,7 +167,7 @@ class FleetVehicleModel(models.Model):
for model in self:
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
@api.depends('default_co2')
@api.depends('default_co2', 'default_fuel_type')
def _compute_co2_fee(self):
for model in self:
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2)
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2, model.default_fuel_type)
@@ -239,7 +239,7 @@
<record id="pcg_120" model="account.account.template">
<field name="name">Résultat de l'exercice (bénéfice)</field>
<field name="code">120</field>
<field name="user_type_id" ref="account.data_unaffected_earnings"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
+7 -6
View File
@@ -158,7 +158,7 @@
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_06" model="account.account.tag">
<field name="name">1c. Leveringen/diensten belast met overige tarieven behalve 0% (omzet)</field>
<field name="name">1c. Leveringen/diensten belast met overige tarieven, behalve 0% (omzet)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_07" model="account.account.tag">
@@ -174,7 +174,7 @@
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_10" model="account.account.tag">
<field name="name">2a. Leveringen/diensten waarbij de heffing van omzetbelasting naar u is verlegd (omzet)</field>
<field name="name">2a. Leveringen/diensten waarbij de omzetbelasting naar u is verlegd (omzet)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_11" model="account.account.tag">
@@ -187,11 +187,11 @@
</record>
<!-- Keep tag_nl_40 & tag_nl_41 to avoid migration issue. -->
<record id="tag_nl_40" model="account.account.tag">
<field name="name">3b. Leveringen naar/diensten in landen binnen de EU (omzet) (producten)</field>
<field name="name">3b. Leveringen naar of diensten in landen binnen de EU (omzet) (producten)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_41" model="account.account.tag">
<field name="name">3b. Leveringen naar/diensten in landen binnen de EU (omzet) (diensten)</field>
<field name="name">3b. Leveringen naar of diensten in landen binnen de EU (omzet) (diensten)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_14" model="account.account.tag">
@@ -231,7 +231,7 @@
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_23" model="account.account.tag">
<field name="name">1c. Leveringen/diensten belast met overige tarieven behalve 0% (BTW)</field>
<field name="name">1c. Leveringen/diensten belast met overige tarieven, behalve 0% (BTW)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_24" model="account.account.tag">
@@ -247,7 +247,7 @@
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_27" model="account.account.tag">
<field name="name">2a. Leveringen/diensten waarbij de heffing van Heffing van omzetbelasting naar u is verlegd (BTW)</field>
<field name="name">2a. Leveringen/diensten waarbij de omzetbelasting naar u is verlegd (BTW)</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_nl_28" model="account.account.tag">
@@ -297,3 +297,4 @@
</data>
</odoo>
@@ -331,8 +331,6 @@
<field name="description">21% BTW verlegd</field>
<field eval="21" name="amount"/>
<field name="amount_type">group</field>
<field name="refund_account_id" ref="vat_payable_v"/>
<field name="account_id" ref="vat_payable_v"/>
<field name="type_tax_use">purchase</field>
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_10')])]"/>
<field name="children_tax_ids" eval="[(6, 0, [ref('btw_ink_0_1'), ref('btw_ink_0_2')])]"/>
+2 -2
View File
@@ -264,8 +264,8 @@
Best regards,
</div>
<div>&amp;nbsp;</div>
<div t-if="record.user_id and record.user_id.signature" style="font-size: 13px;">
<div t-raw="record.user_id.signature"/>
<div t-if="record.user_id and record.user_id.sudo().signature" style="font-size: 13px;">
<div t-raw="record.user_id.sudo().signature"/>
</div>
</td>
</tr>
+10
View File
@@ -534,6 +534,16 @@ class Message(models.Model):
failures_infos = []
# for each channel, build the information header and include the logged partner information
for message in self:
# Check if user has access to the record before displaying a notification about it.
# In case the user switches from one company to another, it might happen that he doesn't
# have access to the record related to the notification. In this case, we skip it.
if message.model and message.res_id:
record = self.env[message.model].browse(message.res_id)
try:
record.check_access_rights('read')
record.check_access_rule('read')
except AccessError:
continue
info = {
'message_id': message.id,
'record_name': message.record_name,
+3 -2
View File
@@ -1335,7 +1335,8 @@ class MailThread(models.AbstractModel):
subtype = 'mail.mt_note'
if message_dict.get('parent_id'):
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
partner_ids = [(4, parent_message.author_id.id)]
if parent_message.author_id:
partner_ids = [(4, parent_message.author_id.id)]
else:
subtype = 'mail.mt_comment'
@@ -2062,7 +2063,7 @@ class MailThread(models.AbstractModel):
prioritary_attachments = all_attachments.filtered(lambda x: x.mimetype.endswith('pdf')) \
or all_attachments.filtered(lambda x: x.mimetype.startswith('image')) \
or all_attachments
self.write({'message_main_attachment_id': prioritary_attachments[0].id})
self.sudo().write({'message_main_attachment_id': prioritary_attachments[0].id})
# Notify recipients of the newly-created message (Inbox / Email + channels)
if msg_vals.get('moderation_status') != 'pending_moderation':
message._notify(
+1 -1
View File
@@ -273,7 +273,7 @@ var BasicActivity = AbstractField.extend({
var self = this;
var $markDoneBtn = $(ev.currentTarget);
var activityID = $markDoneBtn.data('activity-id');
var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id');
var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false;
var forceNextActivity = $markDoneBtn.data('force-next-activity');
if ($markDoneBtn.data('toggle') == 'collapse') {
+3 -4
View File
@@ -300,6 +300,7 @@ var Chatter = Widget.extend({
recordName: this.recordName,
defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(),
defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(),
attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [],
});
this._composer.on('input_focused', this, function () {
this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []);
@@ -309,9 +310,7 @@ var Chatter = Widget.extend({
if (oldComposer) {
oldComposer.destroy();
}
if (!config.device.isMobile) {
self._composer.focus();
}
self._composer.focus();
self._composer.on('post_message', self, function (messageData) {
self._discardOnReload(messageData).then(function () {
self._disableComposer();
@@ -321,7 +320,7 @@ var Chatter = Widget.extend({
self.trigger_up('reload');
} else if (messageData.attachment_ids.length) {
self._reloadAttachmentBox();
self.trigger_up('reload', {fieldNames: ['message_attachment_count']});
self.trigger_up('reload', {fieldNames: ['message_attachment_count'], keepChanges: true});
}
}).fail(function () {
self._enableComposer();
@@ -58,7 +58,7 @@ var BasicComposer = Widget.extend({
// Attachments
this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
this.fileuploadID = _.uniqueId('o_chat_fileupload');
this.set('attachment_ids', []);
this.set('attachment_ids', options.attachmentIds || []);
// Mention
this._mentionManager = new MentionManager(this);
@@ -120,6 +120,7 @@ var BasicComposer = Widget.extend({
});
// Attachments
this._renderAttachments();
$(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
this.on('change:attachment_ids', this, this._renderAttachments);
@@ -101,31 +101,37 @@ MailManager.include({
// valid threadID, therefore no check
var thread = this.getThread(threadID);
var threadWindow = this._getThreadWindow(threadID);
var def = $.when();
if (!threadWindow) {
threadWindow = this._makeNewThreadWindow(thread, options);
this._placeNewThreadWindow(threadWindow, options.passively);
threadWindow.appendTo($(this.THREAD_WINDOW_APPENDTO))
.then(function () {
self._repositionThreadWindows();
return thread.fetchMessages();
}).then(function () {
threadWindow.render();
threadWindow.scrollToBottom();
if (
!self._areAllThreadWindowsHidden() &&
!thread.isFolded() &&
!threadWindow.isPassive()
) {
thread.markAsRead();
}
});
def = thread.fetchMessages().then(function () {
threadWindow = self._makeNewThreadWindow(thread, options);
self._placeNewThreadWindow(threadWindow, options.passively);
return threadWindow.appendTo($(self.THREAD_WINDOW_APPENDTO));
}).then(function () {
self._repositionThreadWindows();
threadWindow.render();
threadWindow.scrollToBottom();
if (
!self._areAllThreadWindowsHidden() &&
!thread.isFolded() &&
!threadWindow.isPassive()
) {
thread.markAsRead();
}
}).fail(function () {
// thread window could not be open, which may happen due to
// access error while fetching messages to the document.
// abort opening the thread window in this case.
thread.close();
});
} else if (!options.passively) {
if (threadWindow.isHidden()) {
this._makeThreadWindowVisible(threadWindow);
}
}
threadWindow.updateVisualFoldState();
def.then(function () {
threadWindow.updateVisualFoldState();
});
},
/**
* Called when a thread has its window state that has been changed, so its
@@ -46,7 +46,7 @@ var ActivityMenu = Widget.extend({
kwargs: {context: session.user_context},
}).then(function (data) {
self._activities = data;
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count || 0; }, 0);
self.$('.o_notification_counter').text(self.activityCounter);
self.$el.toggleClass('o_no_notification', !self.activityCounter);
});
@@ -280,8 +280,10 @@ var ThreadWindow = AbstractThreadWindow.extend({
clear_breadcrumbs: false,
active_id: this.hasThread() ? this._getThreadID() : undefined,
on_reverse_breadcrumb: function () {
self.call('mail_service', 'getMailBus')
.trigger('discuss_open', false);
var mailBus = self.call('mail_service', 'getMailBus');
if (mailBus) {
mailBus.trigger('discuss_open', false);
}
},
});
}
@@ -84,6 +84,8 @@
.o_followers_list {
font-size: small;
max-height: 50vh; // ancestor has size 33px
overflow: auto;
.o_partner {
position: relative; // required for old design compatibility
@@ -271,6 +271,37 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as
messagingMenu.destroy();
});
QUnit.test('do not open thread window on fetch message failure', function (assert) {
// this may happen when the user receives a notification from a document
// that he does not have access rights at the moment.
assert.expect(4);
var messagingMenu = new MessagingMenu();
testUtils.addMockEnvironment(messagingMenu, {
services: this.services,
data: this.data,
session: this.session,
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'some.res.model' && args.args[0][0] === 1) {
assert.step('some.res.model/1/read');
return $.Deferred().reject(); // simulate failure
}
return this._super.apply(this, arguments);
},
});
messagingMenu.appendTo($('#qunit-fixture'));
testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
assert.containsOnce(messagingMenu, '.o_mail_preview');
testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
assert.verifySteps(['some.res.model/1/read']);
assert.strictEqual($('.o_thread_window').length, 0,
"should not have open the DocumentThread in a thread window on fetch messages failure");
messagingMenu.destroy();
});
});
});
});
+1
View File
@@ -4,6 +4,7 @@
from . import mass_mailing
from . import mass_mailing_stats
from . import mail_mail
from . import mail_template
from . import mail_thread
from . import res_config_settings
from . import mass_mailing_report
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class MailTemplate(models.Model):
_inherit = "mail.template"
@api.model
def render_post_process(self, html):
# super will transform relative url to absolute
html = super(MailTemplate, self).render_post_process(html)
# apply shortener after
if self.env.context.get('post_convert_links'):
html = self.env['link.tracker'].convert_links(
html,
self.env.context['post_convert_links'],
blacklist=['/unsubscribe_from_list']
)
return html
+39 -13
View File
@@ -779,19 +779,47 @@ class MassMailing(models.Model):
_logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name)
return opt_out
def _get_convert_links(self):
self.ensure_one()
utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
vals = {'mass_mailing_id': self.id}
if self.mass_mailing_campaign_id:
vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
if utm_mixin.campaign_id:
vals['campaign_id'] = utm_mixin.campaign_id.id
if utm_mixin.source_id:
vals['source_id'] = utm_mixin.source_id.id
if utm_mixin.medium_id:
vals['medium_id'] = utm_mixin.medium_id.id
return vals
def _get_seen_list(self):
"""Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
self.ensure_one()
target = self.env[self.mailing_model_real]
mail_field = 'email' if 'email' in target._fields else 'email_from'
# avoid loading a large number of records in memory
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
if set(['email', 'email_from']) & set(target._fields):
mail_field = 'email' if 'email' in target._fields else 'email_from'
# avoid loading a large number of records in memory
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
elif 'partner_id' in target._fields:
mail_field = 'email'
query = """
SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
JOIN res_partner p ON (t.partner_id = p.id)
WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
else:
raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name)
if self.mass_mailing_campaign_id.unique_ab_testing:
query +="""
AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s;
@@ -814,6 +842,7 @@ class MassMailing(models.Model):
return {
'mass_mailing_opt_out_list': self._get_opt_out_list(),
'mass_mailing_seen_list': self._get_seen_list(),
'post_convert_links': self._get_convert_links(),
}
def get_recipients(self):
@@ -855,13 +884,10 @@ class MassMailing(models.Model):
if not res_ids:
raise UserError(_('There is no recipients selected.'))
# Convert links in absolute URLs before the application of the shortener
mailing.body_html = self.env['mail.thread']._replace_local_links(mailing.body_html)
composer_values = {
'author_id': author_id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
'body': mailing.convert_links()[mailing.id],
'body': mailing.body_html,
'subject': mailing.name,
'model': mailing.mailing_model_real,
'email_from': mailing.email_from,
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_mass_mailing_list_merge
from . import test_mass_mailing_shortener
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from lxml import etree
try:
from unittest.mock import patch
except ImportError:
from mock import patch
class TestMassMailingShortener(common.TransactionCase):
def getHrefFor(self, html, id):
return html.xpath("*[@id='%s']" % id)[0].attrib.get('href')
def shorturl_to_link(self, short_url):
return self.env['link.tracker.code'].search([('code', '=', short_url.split('/r/')[-1])]).link_id
def setUp(self):
super(TestMassMailingShortener, self).setUp()
def _get_title_from_url(u):
return "Hello"
def _compute_favicon():
# 1px to avoid real request
return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='
patcher = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._compute_favicon', wraps=_compute_favicon)
patcher2 = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._get_title_from_url', wraps=_get_title_from_url)
patcher.start()
patcher2.start()
self.addCleanup(patcher.stop)
self.addCleanup(patcher2.stop)
def test_00_test_mass_mailing_shortener(self):
mailing_list_A = self.env['mail.mass_mailing.list'].create({
'name': 'A',
})
self.env['mail.mass_mailing.contact'].create({
'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
self.env['mail.mass_mailing.contact'].create({
'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
self.env['mail.mass_mailing.contact'].create({
'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
mass_mailing = self.env['mail.mass_mailing'].create({
"reply_to_mode": "email",
"reply_to": "Administrator <admin@yourcompany.example.com>",
"mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id,
"mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id,
"contact_list_ids": [[6, False, [mailing_list_A.id]]],
"mass_mailing_campaign_id": False,
"name": "sdf",
"body_html": """
Hi,
% set url = "www.odoo.com"
% set httpurl = "https://www.odoo.eu"
Website0: <a id="url0" href="https://www.odoo.tz/my/${object.name}">https://www.odoo.tz/my/${object.name}</h1>
Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</h1>
Website2: <a id="url2" href="https://${url}">https://${url}</h1>
Website3: <a id="url3" href="${httpurl}">${httpurl}</h1>
Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
""",
"schedule_date": False,
"state": "draft",
"keep_archives": True,
})
mass_mailing.put_in_queue()
mass_mailing._process_mass_mailing_queue()
sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
sent_messages = sent_mails.mapped('mail_message_id')
self.assertEqual(mailing_list_A.contact_nbr, len(sent_messages),
'Some message has not been sent')
xbody = etree.fromstring(sent_messages[0].body)
after_url0 = self.getHrefFor(xbody, 'url0')
after_url1 = self.getHrefFor(xbody, 'url1')
after_url2 = self.getHrefFor(xbody, 'url2')
after_url3 = self.getHrefFor(xbody, 'url3')
after_url4 = self.getHrefFor(xbody, 'url4')
self.assertTrue('/r/' in after_url0, 'URL0 should be shortened: %s' % after_url0)
self.assertTrue('/r/' in after_url1, 'URL1 should be shortened: %s' % after_url1)
self.assertTrue('/r/' in after_url2, 'URL2 should be shortened: %s' % after_url2)
self.assertTrue('/r/' in after_url3, 'URL3 should be shortened: %s' % after_url3)
self.assertEqual(after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
short0 = self.shorturl_to_link(after_url0)
short1 = self.shorturl_to_link(after_url1)
short2 = self.shorturl_to_link(after_url2)
short3 = self.shorturl_to_link(after_url3)
self.assertTrue("https://www.odoo.tz/my/User" in short0.url, 'URL mismatch')
self.assertEqual(short1.url, "https://www.odoo.be", 'URL mismatch')
self.assertEqual(short2.url, "https://www.odoo.com", 'URL mismatch')
self.assertEqual(short3.url, "https://www.odoo.eu", 'URL mismatch')
_xbody = etree.fromstring(sent_messages[1].body)
_after_url0 = self.getHrefFor(_xbody, 'url0')
_after_url1 = self.getHrefFor(_xbody, 'url1')
_after_url2 = self.getHrefFor(_xbody, 'url2')
_after_url3 = self.getHrefFor(_xbody, 'url3')
_after_url4 = self.getHrefFor(_xbody, 'url4')
self.assertTrue('/r/' in _after_url0, 'URL0 should be shortened: %s' % _after_url0)
self.assertTrue('/r/' in _after_url1, 'URL1 should be shortened: %s' % _after_url1)
self.assertTrue('/r/' in _after_url2, 'URL2 should be shortened: %s' % _after_url2)
self.assertTrue('/r/' in _after_url3, 'URL3 should be shortened: %s' % _after_url3)
self.assertEqual(_after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
_short0 = self.shorturl_to_link(_after_url0)
_short1 = self.shorturl_to_link(_after_url1)
_short2 = self.shorturl_to_link(_after_url2)
_short3 = self.shorturl_to_link(_after_url3)
self.assertTrue("https://www.odoo.tz/my/User" in _short0.url, 'URL mismatch')
self.assertEqual(_short1.url, "https://www.odoo.be", 'URL mismatch')
self.assertEqual(_short2.url, "https://www.odoo.com", 'URL mismatch')
self.assertEqual(_short3.url, "https://www.odoo.eu", 'URL mismatch')
self.assertNotEqual(short0.url, _short0.url)
self.assertEqual(short1.url, _short1.url)
self.assertEqual(short2.url, _short2.url)
self.assertEqual(short3.url, _short3.url)
+13 -1
View File
@@ -5,7 +5,7 @@ from collections import defaultdict
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare, float_round
class MrpProduction(models.Model):
@@ -30,6 +30,10 @@ class MrpProduction(models.Model):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
if not location:
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
try:
location.check_access_rule('read')
except (AttributeError, AccessError):
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
return location and location.id or False
@api.model
@@ -39,6 +43,10 @@ class MrpProduction(models.Model):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
if not location:
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
try:
location.check_access_rule('read')
except (AttributeError, AccessError):
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
return location and location.id or False
name = fields.Char(
@@ -397,6 +405,10 @@ class MrpProduction(models.Model):
@api.onchange('picking_type_id')
def onchange_picking_type(self):
location = self.env.ref('stock.stock_location_stock')
try:
location.check_access_rule('read')
except (AttributeError, AccessError):
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
self.move_raw_ids.update({'picking_type_id': self.picking_type_id})
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
+13 -3
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare
@@ -13,10 +13,20 @@ class MrpUnbuild(models.Model):
_order = 'id desc'
def _get_default_location_id(self):
return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
try:
stock_location.check_access_rule('read')
return stock_location.id
except (AttributeError, AccessError):
return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
def _get_default_location_dest_id(self):
return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
try:
stock_location.check_access_rule('read')
return stock_location.id
except (AttributeError, AccessError):
return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
product_id = fields.Many2one(
+2 -2
View File
@@ -128,8 +128,8 @@
</group>
<group>
<field name="mo_id" domain="[('state','=','done')]"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
<field name="location_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
<field name="has_tracking" invisible="1"/>
<field name="lot_id" attrs="{'invisible': [('has_tracking', '=', 'none')], 'required': [('has_tracking', '!=', 'none')]}" groups="stock.group_production_lot"/>
</group>
+1 -1
View File
@@ -171,7 +171,7 @@
<div class="col-6 o_kanban_primary_left">
<div class="btn-group" name="o_wo">
<t t-if="record.workorder_count.raw_value &gt; 0">
<button class="btn btn-primary" name="%(action_work_orders)d" type="action" context="{'search_default_ready': 1, 'search_default_progress': 1, 'search_default_pending': 1}">
<button class="btn btn-primary" name="%(action_work_orders)d" type="action" context="{'search_default_ready': 1, 'search_default_progress': 1}">
<span>WORK ORDERS</span>
</button>
</t>
+1
View File
@@ -18,6 +18,7 @@ _logger = logging.getLogger(__name__)
class PadCommon(models.AbstractModel):
_name = 'pad.common'
_description = 'Pad Common'
@api.model
def pad_is_configured(self):
+4 -1
View File
@@ -100,7 +100,10 @@ class PaymentProcessing(http.Controller):
class WebsitePayment(http.Controller):
@http.route(['/my/payment_method'], type='http', auth="user", website=True)
def payment_method(self, **kwargs):
acquirers = list(request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')]))
acquirers = list(request.env['payment.acquirer'].search([
('website_published', '=', True), ('registration_view_template_id', '!=', False),
('payment_flow', '=', 's2s'), ('company_id', '=', request.env.user.company_id.id)
]))
partner = request.env.user.partner_id
payment_tokens = partner.payment_token_ids
payment_tokens |= partner.commercial_partner_id.sudo().payment_token_ids
@@ -34,7 +34,7 @@
<!-- TDE FIXME: was customize_show=True -->
<template id="pay_meth_link" inherit_id="portal.portal_layout">
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')])">
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s'), ('company_id', '=', request.env.user.company_id.id)])">
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
</t>
</xpath>
+6 -6
View File
@@ -583,21 +583,21 @@ class PosOrder(models.Model):
@api.onchange('statement_ids', 'lines')
def _onchange_amount_all(self):
amounts = {order_id: {'paid': 0, 'return': 0, 'untaxed': 0} for order_id in self.ids}
amounts = {order_id: {'paid': 0, 'return': 0, 'taxed': 0, 'taxes': 0} for order_id in self.ids}
for order in self.env['account.bank.statement.line'].read_group([('pos_statement_id', 'in', self.ids)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['paid'] = order['amount']
for order in self.env['account.bank.statement.line'].read_group(['&', ('pos_statement_id', 'in', self.ids), ('amount', '<', 0)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['return'] = order['amount']
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal'], ['order_id']):
amounts[order['order_id'][0]]['untaxed'] = order['price_subtotal']
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal', 'price_subtotal_incl'], ['order_id']):
amounts[order['order_id'][0]]['taxed'] = order['price_subtotal_incl']
amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal']
for order in self:
currency = order.pricelist_id.currency_id
order.amount_paid = amounts[order.id]['paid']
order.amount_return = amounts[order.id]['return']
order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
amount_untaxed = currency.round(amounts[order.id]['untaxed'])
order.amount_total = order.amount_tax + amount_untaxed
order.amount_tax = currency.round(amounts[order.id]['taxes'])
order.amount_total = currency.round(amounts[order.id]['taxed'])
@api.onchange('partner_id')
def _onchange_partner_id(self):
+14 -6
View File
@@ -876,12 +876,20 @@ exports.PosModel = Backbone.Model.extend({
transfer.pipe(function(order_server_id){
// generate the pdf and download it
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}}).done(function () {
invoiced.resolve();
done.resolve();
});
if (order_server_id.length) {
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}}).done(function () {
invoiced.resolve();
done.resolve();
});
} else {
// The order has been pushed separately in batch when
// the connection came back.
// The user has to go to the backend to print the invoice
invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
done.reject();
}
});
return done;
@@ -2062,6 +2062,17 @@ var PaymentScreenWidget = ScreenWidget.extend({
self.gui.show_screen('clientlist');
},
});
} else if (error.message === 'Backend Invoice') {
self.gui.show_popup('confirm',{
'title': _t('Please print the invoice from the backend'),
'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
confirm: function () {
this.gui.show_screen('receipt');
},
cancel: function () {
this.gui.show_screen('receipt');
},
});
} else if (error.code < 0) { // XmlHttpRequest Errors
self.gui.show_popup('error',{
'title': _t('The order could not be sent'),
+13
View File
@@ -162,6 +162,19 @@ hr {
.container-fluid .container-fluid {
@extend %o-double-container-no-padding;
}
#wrap {
> .container, > .container-fluid {
// BS3 used to do this on all containers so that margins and floats are
// cleared inside containers. As lots of current odoo layouts may rely
// on this for some alignments, this is restored (at least for a while)
// here only for main containers of the frontend.
&::before, &::after {
content: "";
display: table;
clear: both;
}
}
}
[class^="col-lg-"] {
min-height: 24px;
}
+1 -1
View File
@@ -439,7 +439,7 @@
<t t-if='prev_record or next_record'>
<div class="record_pager btn-group" role="group">
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not prev_record else '')" t-att-href="prev_record or '#'" ><i class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"></i></a>
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-chevron-right" role="img" aria-label="Next" title="Previous"></i></a>
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"></i></a>
</div>
</t>
</template>
+1
View File
@@ -9,6 +9,7 @@ from odoo import models, fields, api
class pos_cache(models.Model):
_name = 'pos.cache'
_description = 'Point of Sale Cache'
cache = fields.Binary(attachment=True)
product_domain = fields.Text(required=True)
@@ -1,75 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<template id="report_simple_label">
<div class="col-4" style="padding:0;">
<table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;">
<tr>
<th style="text-align: left;">
<strong t-field="product.display_name"/>
</th>
</tr>
<tr>
<td>
<strong>Price:</strong>
<strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
</td>
</tr>
<t t-if="product.barcode">
<tr>
<td style="text-align: center; vertical-align: middle;" class="col-5">
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<span t-field="product.barcode"/>
</td>
</tr>
</t>
</table>
</div>
</template>
<template id="report_productlabel">
<t t-call="web.basic_layout">
<div class="page">
<t t-foreach="docs" t-as="product">
<t t-call="product.report_simple_label">
<t t-set="product" t-value="product"/>
</t>
</t>
</div>
</t>
</template>
<template id="report_simple_barcode">
<div class="col-4" style="padding:0;">
<table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;">
<tr>
<th style="text-align: left;">
<strong t-field="product.display_name"/>
</th>
</tr>
<t t-if="product.barcode">
<tr>
<td style="text-align: center; vertical-align: middle;" class="col-5">
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<span t-field="product.barcode"/>
</td>
</tr>
</t>
</table>
</div>
</template>
<template id="report_productbarcode">
<t t-call="web.basic_layout">
<div class="page">
<t t-foreach="docs" t-as="product">
<t t-call="product.report_simple_barcode">
<t t-set="product" t-value="product"/>
</t>
</t>
</div>
</t>
</template>
</data>
</odoo>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<template id="report_simple_label">
<div style="width: 32%; display: inline-table; height:14rem;">
<table class="table table-bordered mb-0" style="border: 2px solid black;">
<tr>
<th class="table-active text-left" style="height: 4rem;">
<strong t-field="product.display_name"/>
</th>
</tr>
<tr>
<td style="height: 2rem">
<strong>Price:</strong>
<strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
</td>
</tr>
<tr>
<td class="text-center align-middle" style="height: 6rem">
<t t-if="product.barcode">
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
<span t-field="product.barcode"/>
</t>
<t t-else=""><span class="text-muted">No barcode available</span></t>
</td>
</tr>
</table>
</div>
</template>
<template id="report_productlabel">
<t t-call="web.basic_layout">
<div class="page">
<t t-foreach="docs" t-as="product">
<t t-call="product.report_simple_label">
<t t-set="product" t-value="product"/>
</t>
</t>
</div>
</t>
</template>
<template id="report_simple_barcode">
<div style="width: 32%; display: inline-table; height: 10rem;">
<table class="table table-bordered mb-0" style="border: 2px solid black;">
<tr>
<th class="table-active text-left" style="height: 4rem;">
<strong t-field="product.display_name"/>
</th>
</tr>
<tr>
<td class="text-center align-middle" style="height: 6rem;">
<t t-if="product.barcode">
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:4rem;"/>
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:4rem;"/>
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:4rem"/>
<span t-field="product.barcode"/>
</t>
<t t-else=""><span class="text-muted">No barcode available</span></t>
</td>
</tr>
</table>
</div>
</template>
<template id="report_productbarcode">
<t t-call="web.basic_layout">
<div class="page">
<t t-foreach="docs" t-as="product">
<t t-call="product.report_simple_barcode">
<t t-set="product" t-value="product"/>
</t>
</t>
</div>
</t>
</template>
</data>
</odoo>
+5 -1
View File
@@ -8,7 +8,7 @@ class MailMessage(models.Model):
_inherit = 'mail.message'
rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings')
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False)
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value')
@api.multi
@api.depends('rating_ids', 'rating_ids.rating')
@@ -17,3 +17,7 @@ class MailMessage(models.Model):
mapping = dict((r.message_id.id, r.rating) for r in ratings)
for message in self:
message.rating_value = mapping.get(message.id, 0.0)
@api.multi
def _search_rating_value(self, operator, operand):
return [('rating_ids.rating', operator, operand)]
+2 -1
View File
@@ -196,7 +196,8 @@ class ResourceCalendar(models.Model):
hour_count = 0.0
for attendance in attendances:
hour_count += attendance.hour_to - attendance.hour_from
self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2)
if attendances:
self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2)
# --------------------------------------------------
# Computation API
+1 -1
View File
@@ -103,7 +103,7 @@
</div>
</field>
<field name="report_template" ref="action_report_saleorder"/>
<field name="report_name">${(object.name or '').replace('/','_')}${object.state == 'draft' and '_draft' or ''}</field>
<field name="report_name">${(object.name or '').replace('/','_')}</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="user_signature" eval="False"/>
<field name="auto_delete" eval="True"/>
+3 -2
View File
@@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def create(self, values):
result = super(AccountAnalyticLine, self).create(values)
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line
result.sudo()._sale_determine_order_line()
return result
@@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model):
result = super(AccountAnalyticLine, self).write(values)
if 'so_line' not in values: # allow to force a False value for so_line
# only take the AAL from expense or vendor bill, meaning having a negative amount
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line()
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line()
return result
# ----------------------------------------------------------
# Vendor Bill / Expense : determine the Sale Order to reinvoice
+9 -7
View File
@@ -47,8 +47,8 @@ class SaleOrder(models.Model):
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
order.update({
'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
'amount_untaxed': amount_untaxed,
'amount_tax': amount_tax,
'amount_total': amount_untaxed + amount_tax,
})
@@ -66,12 +66,14 @@ class SaleOrder(models.Model):
for possible refunds created directly from existing invoices. This is necessary since such a
refund is not directly linked to the SO.
"""
# Ignore the status of the deposit product
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)]
for order in self:
invoice_ids = order.order_line.mapped('invoice_lines.invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
# Ignore the status of the deposit product
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id]
if order.state not in ('sale', 'done'):
invoice_status = 'no'
@@ -652,7 +654,7 @@ class SaleOrder(models.Model):
# create an analytic account if at least an expense product
for order in self:
if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if not order.analytic_account_id:
order._create_analytic_account()
@@ -1016,7 +1018,7 @@ class SaleOrderLine(models.Model):
msg = _("Extra line with %s ") % (line.product_id.display_name,)
line.order_id.message_post(body=msg)
# create an analytic account if at least an expense product
if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id:
if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id:
self.order_id._create_analytic_account()
return line
+6 -1
View File
@@ -6,7 +6,6 @@
name="Sales"
web_icon="sale,static/description/icon.png"
active="False"
groups="sales_team.group_sale_salesman,sales_team.group_sale_salesman_all_leads,sales_team.group_sale_manager"
sequence="7"/>
<menuitem id="sale_order_menu"
@@ -17,6 +16,7 @@
<menuitem id="report_sales_team"
name="Sales Teams"
parent="sale_order_menu"
groups="sales_team.group_sale_manager"
action="sales_team.crm_team_salesteams_act"
sequence="3"/>
@@ -314,6 +314,7 @@
<field name="qty_delivered_manual" invisible="1"/>
<field name="qty_delivered_method" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_tax" invisible="1"/>
<field name="price_subtotal" invisible="1"/>
<label for="product_uom_qty" string="Ordered Quantity"/>
<div>
@@ -322,6 +323,7 @@
name="product_uom_qty" class="oe_inline"/>
<field
name="product_uom"
force_save="1"
groups="uom.group_uom"
class="oe_inline oe_no_button"
attrs="{
@@ -442,6 +444,7 @@
<field name="qty_to_invoice" invisible="1"/>
<field
name="product_uom"
force_save="1"
attrs="{
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
'required': [('display_type', '=', False)],
@@ -473,6 +476,7 @@
<field name="invoice_status" invisible="1"/>
<field name="customer_lead" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="price_tax" invisible="1"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="name"/>
@@ -480,6 +484,7 @@
<field name="product_uom_qty"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="price_subtotal"/>
<field name="price_tax" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_unit"/>
<field name="display_type"/>
+1 -1
View File
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _sale_determine_order(self):
mapping = super(AccountAnalyticLine, self)._sale_determine_order()
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'):
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']):
if analytic_line.move_id.expense_id.sale_order_id:
mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id
return mapping
+1 -1
View File
@@ -51,7 +51,7 @@ class SaleOrder(models.Model):
def _action_confirm(self):
""" On SO confirmation, some lines should generate a task or a project. """
result = super(SaleOrder, self)._action_confirm()
self.mapped('order_line').sudo()._timesheet_service_generation()
self.mapped('order_line').sudo().with_context(default_company_id=self.company_id.id)._timesheet_service_generation()
return result
@api.multi
+1 -1
View File
@@ -143,7 +143,7 @@ class StockMoveLine(models.Model):
"""
res = {}
if self.qty_done and self.product_id.tracking == 'serial':
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
if float_compare(self.qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
res['warning'] = {'title': _('Warning'), 'message': message}
return res
+2 -1
View File
@@ -131,7 +131,8 @@ class StockScrap(models.Model):
self.package_id,
self.owner_id,
strict=True).mapped('quantity'))
if float_compare(available_qty, self.scrap_qty, precision_digits=precision) >= 0:
scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id)
if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0:
return self.do_scrap()
else:
return {
+1 -1
View File
@@ -971,7 +971,7 @@ class Orderpoint(models.Model):
# These days will be substracted when creating the PO
days += self.product_id._select_seller(
quantity=product_qty,
date=start_date,
date=fields.Date.context_today(self,start_date),
uom_id=self.product_uom).delay or 0.0
date_planned = start_date + relativedelta.relativedelta(days=days)
return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@@ -155,7 +155,7 @@
</tbody>
</table>
<t t-set="no_reserved_product" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty != x.reserved_availability and x.move_line_ids and x.state!='done')"/>
<p t-if="o.state in ['draft', 'waiting', 'confirmed'] or no_reserved_product" class="fa fa-exclamation-triangle">
<p t-if="o.state in ['draft', 'waiting', 'confirmed'] or no_reserved_product"><i class="fa fa-exclamation-triangle" />
All products could not be reserved. Click on the "Check Availability" button to try to reserve products
</p>
<p t-field="o.note"/>
+18
View File
@@ -3569,6 +3569,24 @@ class StockMove(TransactionCase):
self.assertEqual(scrap.state, 'done')
self.assertEqual(move1.reserved_availability, 0.25)
def test_scrap_6(self):
""" Check that scrap correctly handle UoM. """
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
scrap = self.env['stock.scrap'].create({
'product_id': self.product1.id,
'product_uom_id': self.uom_dozen.id,
'scrap_qty': 1,
})
warning_message = scrap.action_validate()
self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
'product_id': self.product1.id,
'location_id': self.stock_location.id,
'scrap_id': scrap.id
})
insufficient_qty_wizard.action_done()
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).quantity, -11)
def test_in_date_1(self):
""" Check that moving a tracked quant keeps the incoming date.
"""
+1 -1
View File
@@ -220,7 +220,7 @@
<button name="%(act_stock_return_picking)d" string="Return" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
<button name="action_cancel" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'confirmed', 'partially_available', 'draft', 'waiting')), ('is_locked', '=', False)]}" string="Cancel" groups="base.group_user" type="object"/>
<field name="picking_type_code" invisible="1"/>
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available')), ('is_locked', '=', False)]}"/>
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', '|', ('picking_type_code', '=', 'incoming'), ('is_locked', '=', False), '&amp;', ('state', 'not in', ('assigned', 'partially_available')), ('move_type', '!=', 'one'), '&amp;', ('state', 'not in', ('assigned', 'partially_available', 'confirmed')), ('move_type', '=', 'one')]}"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="move_line_exist" invisible="1"/>
<field name="has_packages" invisible="1"/>
+34 -21
View File
@@ -138,23 +138,38 @@ class ProductProduct(models.Model):
StockMove = self.env['stock.move']
to_date = self.env.context.get('to_date')
self.env['account.move.line'].check_access_rights('read')
fifo_automated_values = {}
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
FROM account_move_line AS aml
WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
GROUP BY aml.product_id, aml.account_id"""
params = (self.env.user.company_id.id,)
if to_date:
query = query % ('AND aml.date <= %s',)
params = params + (to_date,)
else:
query = query % ('',)
self.env.cr.execute(query, params=params)
real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
if real_time_product_ids:
self.env['account.move.line'].check_access_rights('read')
fifo_automated_values = {}
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
FROM account_move_line AS aml
WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
GROUP BY aml.product_id, aml.account_id"""
params = (tuple(real_time_product_ids), self.env.user.company_id.id)
if to_date:
query = query % ('AND aml.date <= %s',)
params = params + (to_date,)
else:
query = query % ('',)
self.env.cr.execute(query, params=params)
res = self.env.cr.fetchall()
for row in res:
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
res = self.env.cr.fetchall()
for row in res:
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
product_values = {product: 0 for product in self}
product_move_ids = {product: [] for product in self}
if to_date:
domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
for move in StockMove.search(domain).with_context(prefetch_fields=False):
product_values[move.product_id] += move.value
product_move_ids[move.product_id].append(move.id)
else:
domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
for move in StockMove.search(domain).with_context(prefetch_fields=False):
product_values[move.product_id] += move.remaining_value
product_move_ids[move.product_id].append(move.id)
for product in self:
if product.cost_method in ['standard', 'average']:
@@ -170,11 +185,9 @@ class ProductProduct(models.Model):
elif product.cost_method == 'fifo':
if to_date:
if product.product_tmpl_id.valuation == 'manual_periodic':
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
moves = StockMove.search(domain)
product.stock_value = sum(moves.mapped('value'))
product.stock_value = product_values[product]
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product])
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
@@ -182,7 +195,7 @@ class ProductProduct(models.Model):
product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
else:
product.stock_value, moves = product._sum_remaining_values()
product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product])
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_fifo_manual_move_ids = moves
+3 -6
View File
@@ -387,7 +387,7 @@ class StockMove(models.Model):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
rounding = move.product_id.uom_id.rounding
qty_done = 0.0
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
new_std_price = move._get_price_unit()
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
@@ -396,7 +396,6 @@ class StockMove(models.Model):
else:
# Get the standard price
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
qty = forced_qty or qty_done
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
@@ -483,10 +482,8 @@ class StockMove(models.Model):
fifo_valued_products = self.env['product.product']
fifo_valued_categories = self.env['product.category'].search([('property_cost_method', '=', 'fifo')])
fifo_valued_products |= self.env['product.product'].search([('categ_id', 'child_of', fifo_valued_categories.ids)])
moves_to_vacuum = self.env['stock.move']
for product in fifo_valued_products:
moves_to_vacuum |= self.search(
[('product_id', '=', product.id), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
moves_to_vacuum = self.search(
[('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
moves_to_vacuum._fifo_vacuum()
@api.multi
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertAlmostEqual(self.product1.stock_value, 0.0)
def test_average_perpetual_6(self):
self.product1.product_tmpl_id.cost_method = 'average'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
move2 = self.env['stock.move'].create({
'name': 'Receive 1 units at 5',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 5,
})
move2._action_confirm()
move2._action_assign()
move2.move_line_ids.qty_done = 1.0
# Receive both at the same time
(move1 | move2)._action_done()
self.assertAlmostEqual(self.product1.standard_price, 7.5)
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
return False
accounts = self.product_id.product_tmpl_id.get_product_accounts()
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
# If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
if self.move_id._is_dropshipped():
debit_account_id = accounts.get('expense') and accounts['expense'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
+1
View File
@@ -77,6 +77,7 @@ class SurveyUserInput(models.Model):
""" Send again the invitation """
self.ensure_one()
local_context = {
'survey_resent_user_input': self.id,
'survey_resent_token': True,
'default_partner_ids': self.partner_id and [self.partner_id.id] or [],
'default_multi_email': self.email or "",
+4
View File
@@ -205,6 +205,10 @@ class SurveyInvite(models.TransientModel):
Mail.create(values).send()
def create_token(wizard, partner_id, email):
if context.get("survey_resent_user_input"):
survey_user_input = SurveyUserInput.browse(context.get("survey_resent_user_input"))
if survey_user_input.state in ('new', 'skip'):
return survey_user_input.token
if context.get("survey_resent_token"):
survey_user_input = SurveyUserInput.search([('survey_id', '=', wizard.survey_id.id),
('state', 'in', ['new', 'skip']), '|', ('partner_id', '=', partner_id),
+9 -1
View File
@@ -155,7 +155,15 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
""" Posting a message on a mailing list should send one email to all recipients """
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
self.test_channel.write({'email_send': True})
self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner)
# Subscribe an user without email. We shouldn't try to send email to them.
nomail = self.env['res.users'].create({
"login": "nomail",
"name": "No Mail",
"email": False,
"notification_type": "email",
})
self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner | nomail.partner_id)
self.test_channel.message_post(body="Test", message_type='comment', subtype='mt_comment')
self.assertEqual(len(self._mails), 1)
@@ -49,7 +49,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
with self.assertQueryCount(__system__=2435, marketing=3091):
with self.assertQueryCount(__system__=2480, marketing=3136):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
@@ -86,7 +86,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
with self.assertQueryCount(__system__=2807, marketing=3559):
with self.assertQueryCount(__system__=2867, marketing=3619):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
+9 -11
View File
@@ -1063,21 +1063,19 @@ class Binary(http.Controller):
if status != 200 and download:
return request.env['ir.http']._response_by_status(status, headers, content)
if not content:
content = base64.b64encode(self.placeholder(image='placeholder.png'))
headers = self.force_contenttype(headers, contenttype='image/png')
if not (width or height):
suffix = field.split('_')[-1]
if suffix in ('small', 'medium', 'big'):
content = getattr(odoo.tools, 'image_resize_image_%s' % suffix)(content)
content = limited_image_resize(
content, width=width, height=height, crop=crop, upper_limit=upper_limit, avoid_if_small=avoid_if_small)
if content:
image_base64 = base64.b64decode(content)
else:
suffix = field.split('_')[-1]
if suffix in ('small', 'medium', 'big'):
encoded_placeholder = base64.b64encode(self.placeholder(image='placeholder.png'))
image_base64 = base64.b64decode(getattr(odoo.tools, 'image_resize_image_%s' % suffix)(encoded_placeholder))
else:
image_base64 = self.placeholder(image='placeholder.png') # could return (contenttype, content) in master
headers = self.force_contenttype(headers, contenttype='image/png')
image_base64 = base64.b64decode(content)
headers.append(('Content-Length', len(image_base64)))
response = request.make_response(image_base64, headers)
response.status_code = status
+2 -1
View File
@@ -63,7 +63,8 @@ class Base(models.AbstractModel):
locale=locale)
if field_type == 'selection':
group_by_value = selection_labels[group_by_value]
group_by_value = selection_labels[group_by_value] \
if group_by_value in selection_labels else False
if type(group_by_value) == tuple:
group_by_value = group_by_value[1] # FIXME should use technical value (0)
@@ -2093,6 +2093,9 @@ var TempusDominusBootstrap4 = function ($) {
}
currentDate = this._viewDate.clone().startOf('M').startOf('w').startOf('d');
// !! ODOO FIX START !!
var now = this.getMoment();
// !! ODOO FIX END !!
for (i = 0; i < 42; i++) {
//always display 42 days (should show 6 weeks)
@@ -2125,7 +2128,9 @@ var TempusDominusBootstrap4 = function ($) {
if (!this._isValid(currentDate, 'd')) {
clsName += ' disabled';
}
if (currentDate.isSame(this.getMoment(), 'd')) {
// !! ODOO FIX START !!
if (currentDate.date() === now.date() && currentDate.month() === now.month() && currentDate.year() === now.year()) {
// !! ODOO FIX END !!
clsName += ' today';
}
if (currentDate.day() === 0 || currentDate.day() === 6) {
@@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({
*/
_bindAutoComplete: function () {
var self = this;
// avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
if (this.$input.attr('placeholder')) {
this.$input.attr('placeholder', function (index, val) {
return val.split('').join('\ufeff');
});
}
this.$input.autocomplete({
source: function (req, resp) {
_.each(self._autocompleteSources, function (source) {
@@ -432,6 +432,7 @@ var BasicModel = AbstractModel.extend({
var isNew = this.isNew(id);
var rollback = 'rollback' in options ? options.rollback : isNew;
var initialOffset = element.offset;
element._domains = {};
this._visitChildren(element, function (elem) {
if (rollback && elem._savePoint) {
if (elem._savePoint instanceof Array) {
@@ -362,6 +362,11 @@ return AbstractRenderer.extend({
this.$calendar = this.$(".o_calendar_widget");
// This seems like a workaround but apparently passing the locale
// in the options is not enough. We should initialize it beforehand
var locale = moment.locale();
$.fullCalendar.locale(locale);
//Documentation here : http://arshaw.com/fullcalendar/docs/
var fc_options = $.extend({}, this.state.fc_options, {
eventDrop: function (event) {
@@ -414,6 +419,7 @@ return AbstractRenderer.extend({
height: 'parent',
unselectAuto: false,
isRTL: _t.database.parameters.direction === "rtl",
locale: locale, // reset locale when fullcalendar has already been instanciated before now
});
this.$calendar.fullCalendar(fc_options);
@@ -193,8 +193,10 @@ var DateTime = Field.extend({
operators: [
{value: "=", text: _lt("is equal to")},
{value: "!=", text: _lt("is not equal to")},
{value: ">=", text: _lt("is after")},
{value: "<=", text: _lt("is before")},
{value: ">", text: _lt("is after")},
{value: "<", text: _lt("is before")},
{value: ">=", text: _lt("is after or equal to")},
{value: "<=", text: _lt("is before or equal to")},
{value: "between", text: _lt("is between")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
@@ -227,17 +227,16 @@ var FormController = BasicController.extend({
// are displayed with an alert
var fields = self.renderer.state.fields;
var data = self.renderer.state.data;
var alertFields = [];
var alertFields = {};
for (var k = 0; k < changedFields.length; k++) {
var field = fields[changedFields[k]];
var fieldData = data[changedFields[k]];
if (field.translate && fieldData) {
alertFields.push(field);
alertFields[changedFields[k]] = field;
}
}
if (alertFields.length) {
self.renderer.alertFields = alertFields;
self.renderer.displayTranslationAlert();
if (!_.isEmpty(alertFields)) {
self.renderer.updateAlertFields(alertFields);
}
}
return changedFields;
@@ -14,6 +14,7 @@ var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
events: _.extend({}, BasicRenderer.prototype.events, {
'click .o_notification_box .oe_field_translate': '_onTranslate',
'click .o_notification_box .close': '_onTranslateNotificationClose',
'click .oe_title, .o_inner_group': '_onClick',
}),
custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
@@ -31,6 +32,7 @@ var FormRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
this.idsForLabels = {};
this.lastActivatedFieldIndex = -1;
this.alertFields = {};
},
/**
* @override
@@ -104,23 +106,32 @@ var FormRenderer = BasicRenderer.extend({
});
return fieldNames;
},
/*
* Updates translation alert fields for the current state and display updated fields
*
* @param {Object} alertFields
*/
updateAlertFields: function (alertFields) {
this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields);
this.displayTranslationAlert();
},
/**
* Show a warning message if the user modified a translated field. For each
* field, the notification provides a link to edit the field's translations.
*
* @param {Object[]} alertFields field list
*/
displayTranslationAlert: function () {
this.$('.o_notification_box').remove();
var $notification = $(qweb.render('notification-box', {type: 'info'}))
.append(qweb.render('translation-alert', {
fields: this.alertFields,
lang: _t.database.parameters.name
}));
if (this.$('.o_form_statusbar').length) {
this.$('.o_form_statusbar').after($notification);
} else {
this.$el.prepend($notification);
if (this.alertFields[this.state.res_id]) {
var $notification = $(qweb.render('notification-box', {type: 'info'}))
.append(qweb.render('translation-alert', {
fields: this.alertFields[this.state.res_id],
lang: _t.database.parameters.name
}));
if (this.$('.o_form_statusbar').length) {
this.$('.o_form_statusbar').after($notification);
} else {
this.$el.prepend($notification);
}
}
},
/**
@@ -334,6 +345,7 @@ var FormRenderer = BasicRenderer.extend({
});
self.trigger_up('swipe_right');
},
excludedElements: ".o_notebook .nav.nav-tabs",
});
},
/**
@@ -915,6 +927,9 @@ var FormRenderer = BasicRenderer.extend({
return $.when.apply($, defs).then(function () {
self._updateView($form.contents());
if (self.state.res_id in self.alertFields) {
self.displayTranslationAlert();
}
}, function () {
$form.remove();
}).then(function(){
@@ -1030,6 +1045,15 @@ var FormRenderer = BasicRenderer.extend({
ev.preventDefault();
this.trigger_up('translate', {fieldName: ev.target.name, id: this.state.id});
},
/**
* remove alert fields of record from alertFields object
*
* @private
* @param {MouseEvent} ev
*/
_onTranslateNotificationClose: function(ev) {
delete this.alertFields[this.state.res_id];
},
});
return FormRenderer;
@@ -198,10 +198,12 @@ return AbstractRenderer.extend({
}
}
// For Bar chart View, we keep only groups where count > 0
data[0].values = _.filter(data[0].values, function (elem, index) {
return self.state.data[index].count > 0;
});
// For one level Bar chart View, we keep only groups where count > 0
if (this.state.groupedBy.length <= 1) {
data[0].values = _.filter(data[0].values, function (elem, index) {
return self.state.data[index].count > 0;
});
}
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
@@ -311,7 +311,7 @@ var DataExport = Dialog.extend({
});
self.$export_format_inputs = $fmts.find('input');
self.$export_format_inputs.first().prop('checked', true);
self.$export_format_inputs.filter(':enabled').first().prop('checked', true);
}
},
show_exports_list: function() {
@@ -25,6 +25,7 @@ var DateWidget = Widget.extend({
this.name = parent.name;
this.options = _.extend({
locale: moment.locale(),
format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
minDate: moment({ y: 1900 }),
maxDate: moment().add(200, "y"),

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