[MERGE] forward port branch 10.0 up to 6466aa4
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[](http://runbot.odoo.com/runbot)
|
||||
[](http://www.odoo.com/documentation/master)
|
||||
[](https://www.odoo.com/forum/help-1)
|
||||
[](http://nightly.odoo.com/)
|
||||
[](http://www.odoo.com/documentation/master)
|
||||
[](https://www.odoo.com/forum/help-1)
|
||||
[](http://nightly.odoo.com/)
|
||||
|
||||
Odoo
|
||||
----
|
||||
@@ -27,14 +27,14 @@ a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you inst
|
||||
|
||||
Getting started with Odoo
|
||||
-------------------------
|
||||
For a standard installation please follow the <a href="https://www.odoo.com/documentation/9.0/setup/install.html">Setup instructions</a>
|
||||
For a standard installation please follow the <a href="https://www.odoo.com/documentation/master/setup/install.html">Setup instructions</a>
|
||||
from the documentation.
|
||||
|
||||
If you are a developer you may type the following command at your terminal:
|
||||
|
||||
wget -O- https://raw.githubusercontent.com/odoo/odoo/10.0/setup/setup_dev.py | python
|
||||
wget -O- https://raw.githubusercontent.com/odoo/odoo/master/setup/setup_dev.py | python
|
||||
|
||||
Then follow <a href="https://www.odoo.com/documentation/9.0/tutorials.html">the developer tutorials</a>
|
||||
Then follow <a href="https://www.odoo.com/documentation/master/tutorials.html">the developer tutorials</a>
|
||||
|
||||
|
||||
For Odoo employees
|
||||
|
||||
@@ -731,6 +731,9 @@ class AccountTax(models.Model):
|
||||
else:
|
||||
total_included += tax_amount
|
||||
|
||||
# Keep base amount used for the current tax
|
||||
tax_base = base
|
||||
|
||||
if tax.include_base_amount:
|
||||
base += tax_amount
|
||||
|
||||
@@ -738,6 +741,7 @@ class AccountTax(models.Model):
|
||||
'id': tax.id,
|
||||
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
|
||||
'amount': tax_amount,
|
||||
'base': tax_base,
|
||||
'sequence': tax.sequence,
|
||||
'account_id': tax.account_id.id,
|
||||
'refund_account_id': tax.refund_account_id.id,
|
||||
|
||||
@@ -250,13 +250,13 @@ class AccountBankStatement(models.Model):
|
||||
context = dict(self._context or {})
|
||||
context['journal_id'] = self.journal_id.id
|
||||
return {
|
||||
'name': _('Journal Items'),
|
||||
'name': _('Journal Entries'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,form',
|
||||
'res_model': 'account.move.line',
|
||||
'res_model': 'account.move',
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('statement_id', 'in', self.ids)],
|
||||
'domain': [('id', 'in', self.mapped('move_line_ids').mapped('move_id').ids)],
|
||||
'context': context,
|
||||
}
|
||||
|
||||
@@ -366,6 +366,7 @@ class AccountBankStatementLine(models.Model):
|
||||
journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True)
|
||||
amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.")
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.")
|
||||
state = fields.Selection(related='statement_id.state' , string='Status', readonly=True)
|
||||
move_name = fields.Char(string='Journal Entry Name', readonly=True,
|
||||
default=False, copy=False,
|
||||
help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.")
|
||||
|
||||
@@ -12,6 +12,9 @@ from odoo.tools.misc import formatLang
|
||||
from odoo.exceptions import UserError, RedirectWarning, ValidationError
|
||||
|
||||
import odoo.addons.decimal_precision as dp
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# mapping invoice type to journal type
|
||||
TYPE2JOURNAL = {
|
||||
@@ -592,6 +595,25 @@ class AccountInvoice(models.Model):
|
||||
else:
|
||||
return self.env.ref('account.invoice_form').id
|
||||
|
||||
def _prepare_tax_line_vals(self, line, tax):
|
||||
""" Prepare values to create an account.invoice.tax line
|
||||
|
||||
The line parameter is an account.invoice.line, and the
|
||||
tax parameter is the output of account.tax.compute_all().
|
||||
"""
|
||||
vals = {
|
||||
'invoice_id': self.id,
|
||||
'name': tax['name'],
|
||||
'tax_id': tax['id'],
|
||||
'amount': tax['amount'],
|
||||
'base': tax['base'],
|
||||
'manual': False,
|
||||
'sequence': tax['sequence'],
|
||||
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
|
||||
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
|
||||
}
|
||||
return vals
|
||||
|
||||
@api.multi
|
||||
def get_taxes_values(self):
|
||||
tax_grouped = {}
|
||||
@@ -599,16 +621,7 @@ class AccountInvoice(models.Model):
|
||||
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
|
||||
for tax in taxes:
|
||||
val = {
|
||||
'invoice_id': self.id,
|
||||
'name': tax['name'],
|
||||
'tax_id': tax['id'],
|
||||
'amount': tax['amount'],
|
||||
'manual': False,
|
||||
'sequence': tax['sequence'],
|
||||
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
|
||||
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
|
||||
}
|
||||
val = self._prepare_tax_line_vals(line, tax)
|
||||
|
||||
# If the taxes generate moves on the same financial account as the invoice line,
|
||||
# propagate the analytic account from the invoice line to the tax line.
|
||||
@@ -623,6 +636,7 @@ class AccountInvoice(models.Model):
|
||||
tax_grouped[key] = val
|
||||
else:
|
||||
tax_grouped[key]['amount'] += val['amount']
|
||||
tax_grouped[key]['base'] += val['base']
|
||||
return tax_grouped
|
||||
|
||||
@api.multi
|
||||
@@ -1336,15 +1350,19 @@ class AccountInvoiceTax(models.Model):
|
||||
|
||||
@api.depends('invoice_id.invoice_line_ids')
|
||||
def _compute_base_amount(self):
|
||||
tax_grouped = {}
|
||||
for invoice in self.mapped('invoice_id'):
|
||||
tax_grouped[invoice.id] = invoice.get_taxes_values()
|
||||
for tax in self:
|
||||
base = 0.0
|
||||
for line in tax.invoice_id.invoice_line_ids:
|
||||
if tax.tax_id in line.invoice_line_tax_ids:
|
||||
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
base += (line.invoice_line_tax_ids - tax.tax_id).compute_all(
|
||||
price_unit, line.invoice_id.currency_id, line.quantity, line.product_id, line.invoice_id.partner_id
|
||||
)['base']
|
||||
tax.base = base
|
||||
key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({
|
||||
'tax_id': tax.tax_id.id,
|
||||
'account_id': tax.account_id.id,
|
||||
'account_analytic_id': tax.account_analytic_id.id,
|
||||
})
|
||||
if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]:
|
||||
tax.base = tax_grouped[tax.invoice_id.id][key]['base']
|
||||
else:
|
||||
_logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name)
|
||||
|
||||
invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True)
|
||||
name = fields.Char(string='Tax Description', required=True)
|
||||
|
||||
@@ -1037,10 +1037,13 @@ class AccountMoveLine(models.Model):
|
||||
# the provided values were not already multi-currency
|
||||
if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
|
||||
vals['currency_id'] = account.currency_id.id
|
||||
ctx = {}
|
||||
if 'date' in vals:
|
||||
ctx['date'] = vals['date']
|
||||
vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
|
||||
if self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded':
|
||||
vals['amount_currency'] = 0.0
|
||||
else:
|
||||
ctx = {}
|
||||
if 'date' in vals:
|
||||
ctx['date'] = vals['date']
|
||||
vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
|
||||
|
||||
if not ok:
|
||||
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
|
||||
|
||||
@@ -39,7 +39,7 @@ def migrate_tags_on_taxes(cr, registry):
|
||||
('description', '=', tax_template.description)
|
||||
])
|
||||
if len(tax_id.ids) == 1:
|
||||
tax_id.sudo().write({'tag_ids': [(6, 0, [tax_template.tag_ids.ids])]})
|
||||
tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]})
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Account Templates: Account, Tax, Tax Code and chart. + Wizard
|
||||
@@ -399,7 +399,9 @@ class AccountChartTemplate(models.Model):
|
||||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
account_reconcile_models = self.env['account.reconcile.model.template'].search([])
|
||||
account_reconcile_models = self.env['account.reconcile.model.template'].search([
|
||||
('account_id.chart_template_id', '=', self.id)
|
||||
])
|
||||
for account_reconcile_model in account_reconcile_models:
|
||||
vals = {
|
||||
'name': account_reconcile_model.name,
|
||||
|
||||
@@ -445,5 +445,5 @@ class ResPartner(models.Model):
|
||||
'''
|
||||
action = self.env.ref('account.action_invoice_refund_out_tree')
|
||||
result = action.read()[0]
|
||||
result['domain'] = "[('id','in',[" + ','.join(map(str, self.ids)) + "])]"
|
||||
result['domain'] = [('partner_id', 'in', self.ids)]
|
||||
return result
|
||||
|
||||
@@ -41,3 +41,65 @@ class TestAccountSupplierInvoice(AccountingTestCase):
|
||||
#I cancel the account move which is in posted state and verifies that it gives warning message
|
||||
with self.assertRaises(Warning):
|
||||
invoice.move_id.button_cancel()
|
||||
|
||||
def test_supplier_invoice2(self):
|
||||
tax_fixed = self.env['account.tax'].create({
|
||||
'sequence': 10,
|
||||
'name': 'Tax 10.0 (Fixed)',
|
||||
'amount': 10.0,
|
||||
'amount_type': 'fixed',
|
||||
'include_base_amount': True,
|
||||
})
|
||||
tax_percent_included_base_incl = self.env['account.tax'].create({
|
||||
'sequence': 20,
|
||||
'name': 'Tax 50.0% (Percentage of Price Tax Included)',
|
||||
'amount': 50.0,
|
||||
'amount_type': 'division',
|
||||
'include_base_amount': True,
|
||||
})
|
||||
tax_percentage = self.env['account.tax'].create({
|
||||
'sequence': 30,
|
||||
'name': 'Tax 20.0% (Percentage of Price)',
|
||||
'amount': 20.0,
|
||||
'amount_type': 'percent',
|
||||
'include_base_amount': False,
|
||||
})
|
||||
analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': 'test account',
|
||||
})
|
||||
|
||||
# Should be changed by automatic on_change later
|
||||
invoice_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1).id
|
||||
invoice_line_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1).id
|
||||
|
||||
invoice = self.env['account.invoice'].create({'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'account_id': invoice_account,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
|
||||
invoice_line = self.env['account.invoice.line'].create({'product_id': self.env.ref('product.product_product_4').id,
|
||||
'quantity': 5.0,
|
||||
'price_unit': 100.0,
|
||||
'invoice_id': invoice.id,
|
||||
'name': 'product that cost 100',
|
||||
'account_id': invoice_line_account,
|
||||
'invoice_line_tax_ids': [(6, 0, [tax_fixed.id, tax_percent_included_base_incl.id, tax_percentage.id])],
|
||||
'account_analytic_id': analytic_account.id,
|
||||
})
|
||||
invoice.compute_taxes()
|
||||
|
||||
# check that Initially supplier bill state is "Draft"
|
||||
self.assertTrue((invoice.state == 'draft'), "Initially vendor bill state is Draft")
|
||||
|
||||
#change the state of invoice to open by clicking Validate button
|
||||
invoice.signal_workflow('invoice_open')
|
||||
|
||||
# Check if amount and corresponded base is correct for all tax scenarios given on a computational base
|
||||
# Keep in mind that tax amount can be changed by the user at any time before validating (based on the invoice and tax laws applicable)
|
||||
invoice_tax = invoice.tax_line_ids.sorted(key=lambda r: r.sequence)
|
||||
self.assertEquals(invoice_tax.mapped('amount'), [50.0, 550.0, 220.0])
|
||||
self.assertEquals(invoice_tax.mapped('base'), [500.0, 550.0, 1100.0])
|
||||
|
||||
#I cancel the account move which is in posted state and verifies that it gives warning message
|
||||
with self.assertRaises(Warning):
|
||||
invoice.move_id.button_cancel()
|
||||
|
||||
@@ -556,7 +556,7 @@
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="button_journal_entries"
|
||||
string="Journal Items" type="object"
|
||||
string="Journal Entries" type="object"
|
||||
attrs="{'invisible':[('move_line_ids','=',[])]}" icon="fa-bars"/>
|
||||
<field name="move_line_ids" invisible="1"/>
|
||||
</div>
|
||||
@@ -884,6 +884,7 @@
|
||||
<field name="type"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="include_initial_balance"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Description"/>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
icon="fa-pencil-square-o"
|
||||
name="open_partner_history"
|
||||
attrs="{'invisible': [('customer', '=', False)]}"
|
||||
context="{'search_default_partner_id': active_id,'default_partner_id': active_id}">
|
||||
context="{'default_partner_id': active_id}">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value"><field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/></span>
|
||||
<span class="o_stat_text">Invoiced</span>
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
<span class="fa fa-copy fa-4x"></span>
|
||||
<h3><strong>Contracts & Subscriptions</strong></h3>
|
||||
<p>Manage time & material contracts or fixed-price recurring subscriptions.</p>
|
||||
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_analytic_analysis')"
|
||||
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'sale_contract')"
|
||||
class="btn odoo_purple">
|
||||
<span class="fa fa-arrow-circle-o-down"/> Install Now
|
||||
</a>
|
||||
|
||||
@@ -153,7 +153,7 @@ class AccountAssetAsset(models.Model):
|
||||
else:
|
||||
if self.method == 'linear':
|
||||
amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
|
||||
if self.prorata and self.category_id.type == 'purchase':
|
||||
if self.prorata:
|
||||
amount = amount_to_depr / self.method_number
|
||||
if sequence == 1:
|
||||
days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
|
||||
@@ -174,7 +174,7 @@ class AccountAssetAsset(models.Model):
|
||||
while depreciation_date <= end_date:
|
||||
depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period)
|
||||
undone_dotation_number += 1
|
||||
if self.prorata and self.category_id.type == 'purchase':
|
||||
if self.prorata:
|
||||
undone_dotation_number += 1
|
||||
return undone_dotation_number
|
||||
|
||||
@@ -378,7 +378,7 @@ class AccountAssetAsset(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _compute_entries(self, date, group_entries=False):
|
||||
depreciation_ids = self.env['account.asset.depreciation.line'].with_context(depreciation_date=date).search([
|
||||
depreciation_ids = self.env['account.asset.depreciation.line'].search([
|
||||
('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
|
||||
('move_check', '=', False)])
|
||||
if group_entries:
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
<attribute name="options">{'reload_on_button': true}</attribute>
|
||||
</field>
|
||||
<xpath expr="//field[@name='bank_account_id']" position="after">
|
||||
<button name="button_cancel_reconciliation" attrs="{'invisible': [('journal_entry_ids', '=', [])]}" string="Cancel" type="object" icon="fa-undo text-warning"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<button name="button_cancel_reconciliation" attrs="{'invisible': ['|',('journal_entry_ids', '=', []), ('state', '=', 'confirm')]}" string="Cancel" type="object" icon="fa fa-undo text-warning"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -7,6 +7,7 @@ var common = require('web.form_common');
|
||||
var BarcodeEvents = require('barcodes.BarcodeEvents');
|
||||
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
|
||||
var KanbanRecord = require('web_kanban.Record');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -77,22 +78,38 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
|
||||
} else {
|
||||
var field = this.form_view.fields[this.m2x_field];
|
||||
var view = field.viewmanager.active_view;
|
||||
var $content = $('<div>').append($('<input>', {type: 'text', class: 'o_set_qty_input'}));
|
||||
|
||||
if (this.last_scanned_barcode) {
|
||||
var new_qty = window.prompt(_t('Set quantity'), character) || "0";
|
||||
new_qty = new_qty.replace(',', '.');
|
||||
var record = this._get_records(field).find(function(record) {
|
||||
return record.get('product_barcode') === self.last_scanned_barcode;
|
||||
this.dialog = new Dialog(this, {
|
||||
title: _t('Set quantity'),
|
||||
buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
|
||||
var new_qty = this.$content.find('.o_set_qty_input').val();
|
||||
var record = _.find(self._get_records(field), function (record) {
|
||||
return record.get('product_barcode') === self.last_scanned_barcode;
|
||||
});
|
||||
if (record) {
|
||||
var values = {};
|
||||
values[self.quantity_field] = parseFloat(new_qty);
|
||||
field.data_update(record.get('id'), values).then(function () {
|
||||
view.controller.reload_record(record);
|
||||
});
|
||||
} else {
|
||||
self._display_no_last_scanned_warning();
|
||||
}
|
||||
}}, {text: _t('Discard'), close: true}],
|
||||
$content: $content,
|
||||
}).open();
|
||||
// This line set the value of the key which triggered the _set_quantity in the input
|
||||
this.dialog.$content.find('.o_set_qty_input').focus().val(character);
|
||||
|
||||
var $selectBtn = this.dialog.$footer.find('.btn-primary');
|
||||
core.bus.on('keypress', this.dialog, function(event){
|
||||
if (event.which === 13) {
|
||||
event.preventDefault();
|
||||
$selectBtn.click();
|
||||
}
|
||||
});
|
||||
if (record) {
|
||||
var values = {};
|
||||
values[this.quantity_field] = parseFloat(new_qty);
|
||||
field.data_update(record.get('id'), values).then(function () {
|
||||
view.controller.reload_record(record);
|
||||
});
|
||||
} else {
|
||||
this._display_no_last_scanned_warning();
|
||||
}
|
||||
} else {
|
||||
this._display_no_last_scanned_warning();
|
||||
}
|
||||
@@ -159,8 +176,13 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
|
||||
},
|
||||
|
||||
_get_records: function(field) {
|
||||
return field.viewmanager.active_view.controller.records || // tree view
|
||||
field.viewmanager.active_view.controller.widgets; // kanban view
|
||||
var active_view = field.viewmanager.active_view;
|
||||
if (active_view.type === "kanban") {
|
||||
return active_view.controller.widgets;
|
||||
} else {
|
||||
// tree view case
|
||||
return active_view.controller.records.records;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -198,7 +198,14 @@ class BaseActionRule(models.Model):
|
||||
|
||||
# subscribe followers
|
||||
if self.act_followers and hasattr(records, 'message_subscribe'):
|
||||
records.message_subscribe(self.act_followers.ids)
|
||||
followers = self.env['mail.followers'].sudo().search(
|
||||
[('res_model', '=', records._name),
|
||||
('res_id', 'in', records.ids),
|
||||
('partner_id', 'in', self.act_followers.ids),
|
||||
]
|
||||
)
|
||||
if not len(followers) == len(self.act_followers):
|
||||
records.message_subscribe(self.act_followers.ids)
|
||||
|
||||
# execute server actions
|
||||
if self.server_action_ids:
|
||||
|
||||
@@ -14,6 +14,7 @@ import re
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.tools.misc import ustr
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
try:
|
||||
@@ -346,29 +347,37 @@ class Import(models.TransientModel):
|
||||
if not date_patterns:
|
||||
date_patterns = [pattern.replace('r', sep) for sep in separator for pattern in date_format]
|
||||
date_patterns.extend([p.replace('Y', 'y') for p in date_patterns])
|
||||
datetime_patterns = [options['datetime_format']] if options.get('datetime_format') else []
|
||||
if not datetime_patterns:
|
||||
datetime_patterns = [pattern + ' %H:%M:%S' for pattern in date_patterns]
|
||||
|
||||
current_date_pattern = False
|
||||
for date_pattern in date_patterns:
|
||||
date_ok = True
|
||||
datetime_ok = False
|
||||
for val in preview_values:
|
||||
if val == '':
|
||||
continue
|
||||
try:
|
||||
dt.strptime(val, date_pattern)
|
||||
except ValueError:
|
||||
date_ok = False
|
||||
current_datetime_pattern = False
|
||||
|
||||
def check_patterns(patterns, preview_values):
|
||||
for pattern in patterns:
|
||||
match = True
|
||||
for val in preview_values:
|
||||
if not val:
|
||||
continue
|
||||
try:
|
||||
dt.strptime(val, date_pattern+' %H:%M:%S')
|
||||
datetime_ok = True
|
||||
dt.strptime(val, pattern)
|
||||
except ValueError:
|
||||
datetime_ok = False
|
||||
match = False
|
||||
break
|
||||
if date_ok or datetime_ok:
|
||||
current_date_pattern = date_pattern
|
||||
break
|
||||
if match:
|
||||
return pattern
|
||||
return False
|
||||
|
||||
current_date_pattern = check_patterns(date_patterns, preview_values)
|
||||
if current_date_pattern:
|
||||
options['date_format'] = current_date_pattern
|
||||
return ['date'] if date_ok else ['datetime']
|
||||
return ['date']
|
||||
|
||||
current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
|
||||
if current_datetime_pattern:
|
||||
options['datetime_format'] = current_datetime_pattern
|
||||
return ['datetime']
|
||||
|
||||
return ['text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
|
||||
|
||||
@@ -598,14 +607,19 @@ class Import(models.TransientModel):
|
||||
# Parse date
|
||||
index = import_fields.index(name)
|
||||
dt = datetime.datetime
|
||||
field_date_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
|
||||
if options.get('date_format', field_date_format) != field_date_format:
|
||||
for line in data:
|
||||
server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
if options.get('%s_format' % field['type'], server_format) != server_format:
|
||||
user_format = ustr(options.get('%s_format' % field['type'])).encode('utf-8')
|
||||
for num, line in enumerate(data):
|
||||
if line[index]:
|
||||
try:
|
||||
line[index] = dt.strftime(dt.strptime(line[index], options['date_format']), field_date_format)
|
||||
except ValueError:
|
||||
raise ValueError(_("Column %s contains incorrect values (value: %s does not match date format" % (name, line[index])))
|
||||
line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format)
|
||||
except ValueError, e:
|
||||
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message)))
|
||||
except Exception, e:
|
||||
raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message)))
|
||||
|
||||
elif field['type'] in ('float', 'monetary') and name in import_fields:
|
||||
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
|
||||
# We should be able to manage both case
|
||||
|
||||
@@ -119,11 +119,12 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
opts: [
|
||||
{name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
|
||||
{name: 'separator', label: _lt("Separator:"), value: ','},
|
||||
{name: 'quoting', label: _lt("Quoting:"), value: '"'}
|
||||
{name: 'quoting', label: _lt("Text Delimiter:"), value: '"'}
|
||||
],
|
||||
parse_opts: [
|
||||
{name: 'date_format', label: _lt("Date Format:"), value: ''},
|
||||
{name: 'float_thousand_separator', label: _lt("Thousand Separator:"), value: ','},
|
||||
{name: 'datetime_format', label: _lt("Datetime Format:"), value: ''},
|
||||
{name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','},
|
||||
{name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'}
|
||||
],
|
||||
events: {
|
||||
@@ -295,8 +296,8 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
self.$('input.oe_import_' + opt.name).val();
|
||||
});
|
||||
_(this.parse_opts).each(function (opt) {
|
||||
if (opt.name === 'date_format') {
|
||||
options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_date_format').val());
|
||||
if (opt.name === 'date_format' || opt.name === 'datetime_format') {
|
||||
options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val());
|
||||
}
|
||||
else {
|
||||
options[opt.name] = self.$('input.oe_import_' + opt.name).val();
|
||||
@@ -320,6 +321,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
.prop('disabled', true);
|
||||
if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); }
|
||||
this.$('.oe_import_date_format').val('');
|
||||
this.$('.oe_import_datetime_format').val('');
|
||||
|
||||
this.$el.removeClass('oe_import_preview oe_import_error');
|
||||
var import_toggle = false;
|
||||
@@ -375,6 +377,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
|
||||
}
|
||||
|
||||
this.$('.oe_import_date_format').val(time.strftime_to_moment_format(result.options.date_format));
|
||||
this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format));
|
||||
this.$('.oe_import_float_thousand_separator').val(result.options.float_thousand_separator).change();
|
||||
this.$('.oe_import_float_decimal_separator').val(result.options.float_decimal_separator).change();
|
||||
if (result.debug === false){
|
||||
|
||||
@@ -379,6 +379,31 @@ class test_convert_import_data(TransactionCase):
|
||||
['qux', '5', '6'],
|
||||
])
|
||||
|
||||
def test_date_fields(self):
|
||||
import_wizard = self.env['base_import.import'].create({
|
||||
'res_model': 'res.partner',
|
||||
'file': 'name,date,create_date\n'
|
||||
'"foo","2013年07月18日","2016-10-12 06:06"\n',
|
||||
'file_type': 'text/csv'
|
||||
|
||||
})
|
||||
|
||||
results = import_wizard.do(
|
||||
['name', 'date', 'create_date'],
|
||||
{
|
||||
'date_format': '%Y年%m月%d日',
|
||||
'datetime_format': '%Y-%m-%d %H:%M',
|
||||
'quoting': '"',
|
||||
'separator': ',',
|
||||
'headers': True
|
||||
}
|
||||
)
|
||||
|
||||
# if results empty, no errors
|
||||
self.assertItemsEqual(results, [])
|
||||
|
||||
|
||||
|
||||
def test_filtered(self):
|
||||
""" If ``False`` is provided as field mapping for a column,
|
||||
that column should be removed from importable data
|
||||
|
||||
@@ -1372,7 +1372,7 @@ class Meeting(models.Model):
|
||||
if not self._context.get('dont_notify'):
|
||||
if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
|
||||
partners_to_notify = meeting.partner_ids.ids
|
||||
event_attendees_changes = attendees_create and attendees_create[real_ids[0]]
|
||||
event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
|
||||
if event_attendees_changes:
|
||||
partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
|
||||
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
|
||||
|
||||
@@ -526,7 +526,7 @@
|
||||
<field name="source_id" invisible="1"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="planned_revenue" sum="Expected Revenues"/>
|
||||
<field name="probability" />
|
||||
<field name="probability" avg="Avg. of Probability"/>
|
||||
<field name="team_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="referred" invisible="1"/>
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
<label for="weight" string="Weight"/>
|
||||
<div>
|
||||
<field name="weight" class="oe_inline"/>
|
||||
<field name="weight_uom_id" nolabel="1" class="oe_inline"/>
|
||||
<field name="weight_uom_id" nolabel="1" class="oe_inline" style="margin-left:5px"/>
|
||||
</div>
|
||||
<field name="shipping_weight"/>
|
||||
<field name="number_of_packages" attrs="{'readonly': [('state', '=', 'done')]}"/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('hr_attendance.greeting_message', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var Widget = require('web.Widget');
|
||||
@@ -8,27 +10,39 @@ var Widget = require('web.Widget');
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var GreetingMessage = Widget.extend({
|
||||
var GreetingMessage = Widget.extend(BarcodeHandlerMixin, {
|
||||
template: 'HrAttendanceGreetingMessage',
|
||||
|
||||
events: {
|
||||
"click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
"click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
},
|
||||
|
||||
init: function(parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_attendances', params: {} };
|
||||
if(!action.attendance){
|
||||
self.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group){
|
||||
if(has_group){
|
||||
self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_kiosk_mode', target: 'fullscreen', params: {} };
|
||||
this._super.apply(this, arguments);
|
||||
BarcodeHandlerMixin.init.apply(this, arguments);
|
||||
|
||||
// if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
|
||||
// to the (likely) appropriate menu, according to the user access rights
|
||||
if(!action.attendance) {
|
||||
this.stop_listening();
|
||||
this.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) {
|
||||
if(has_group) {
|
||||
self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
|
||||
} else {
|
||||
self.next_action = 'hr_attendance.hr_attendance_action_my_attendances';
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.next_action = action.next_action || default_action;
|
||||
|
||||
this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances';
|
||||
// no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
|
||||
if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
|
||||
this.stop_listening();
|
||||
}
|
||||
this.attendance = action.attendance;
|
||||
// check in/out times displayed in the greeting message template.
|
||||
this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
|
||||
this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
|
||||
this.previous_attendance_change_date = action.previous_attendance_change_date;
|
||||
@@ -36,48 +50,42 @@ var GreetingMessage = Widget.extend({
|
||||
},
|
||||
|
||||
start: function() {
|
||||
if (!this.attendance){
|
||||
return;
|
||||
}
|
||||
if (this.attendance.check_out) {
|
||||
this.farewell_message();
|
||||
} else {
|
||||
this.welcome_message();
|
||||
if (this.attendance) {
|
||||
this.attendance.check_out ? this.farewell_message() : this.welcome_message();
|
||||
}
|
||||
},
|
||||
|
||||
welcome_message: function() {
|
||||
var self = this;
|
||||
var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
|
||||
if(now.getHours()<5){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
} else if(now.getHours() < 12){
|
||||
if(now.getHours() < 8 && Math.random() < .3){
|
||||
if(Math.random()<.75){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
|
||||
if (now.getHours() < 5) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
} else if (now.getHours() < 12) {
|
||||
if (now.getHours() < 8 && Math.random() < 0.3) {
|
||||
if (Math.random() < 0.75) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
|
||||
this.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
|
||||
}
|
||||
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good morning"));
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
|
||||
}
|
||||
} else if(now.getHours()<17){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
|
||||
} else if(now.getHours()<23){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good evening"));
|
||||
} else if (now.getHours() < 17){
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
|
||||
} else if (now.getHours() < 23){
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
}
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
|
||||
} else {
|
||||
if(Math.random() < .02){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
|
||||
if(Math.random() < 0.02){
|
||||
this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,38 +94,55 @@ var GreetingMessage = Widget.extend({
|
||||
farewell_message: function() {
|
||||
var self = this;
|
||||
var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){
|
||||
self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
|
||||
clearTimeout(self.return_to_main_menu);
|
||||
this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
this.stop_listening();
|
||||
} else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
|
||||
}
|
||||
}
|
||||
|
||||
if(now.getHours()<12){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
|
||||
} else if(now.getHours()<14){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
|
||||
if(Math.random() < 0.05){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
|
||||
} else if(Math.random() < 0.06){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
|
||||
if (now.getHours() < 12) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
|
||||
} else if (now.getHours() < 14) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
|
||||
if (Math.random() < 0.05) {
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
|
||||
} else if (Math.random() < 0.06) {
|
||||
this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
|
||||
}
|
||||
} else if(now.getHours()<17){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
|
||||
} else if (now.getHours() < 17) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
|
||||
} else {
|
||||
if(now.getHours()<18 && Math.random()<0.2){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
|
||||
if (now.getHours() < 18 && Math.random() < 0.2) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
on_barcode_scanned: function(barcode) {
|
||||
var self = this;
|
||||
if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
}
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.call('attendance_scan', [barcode, ])
|
||||
.then(function (result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.do_warn(result.warning);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
@@ -55,7 +55,7 @@ var KioskConfirm = Widget.extend({
|
||||
|
||||
init: function (parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_kiosk_mode', target: "fullscreen", params: {} };
|
||||
this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
|
||||
this.employee_id = action.employee_id;
|
||||
this.employee_name = action.employee_name;
|
||||
this.employee_state = action.employee_state;
|
||||
|
||||
@@ -136,6 +136,7 @@ class HrExpense(models.Model):
|
||||
'product_id': line.get('product_id'),
|
||||
'product_uom_id': line.get('uom_id'),
|
||||
'analytic_account_id': line.get('analytic_account_id'),
|
||||
'payment_id': line.get('payment_id'),
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@@ -185,7 +186,10 @@ class HrExpense(models.Model):
|
||||
'journal_id': journal.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
'date': acc_date,
|
||||
'ref': ref
|
||||
'ref': ref,
|
||||
# force the name to the default value, to avoid an eventual 'default_name' in the context
|
||||
# to set it to '' which cause no number to be given to the account.move when posted.
|
||||
'name': '/',
|
||||
})
|
||||
for expense in expense_list:
|
||||
company_currency = expense.company_id.currency_id
|
||||
@@ -194,11 +198,29 @@ class HrExpense(models.Model):
|
||||
move_lines = expense._move_line_get()
|
||||
|
||||
#create one more move line, a counterline for the total on payable account
|
||||
payment_id = False
|
||||
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
|
||||
if expense.payment_mode == 'company_account':
|
||||
if not expense.sheet_id.bank_journal_id.default_credit_account_id:
|
||||
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
|
||||
emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
|
||||
journal = expense.bank_journal_id
|
||||
#create payment
|
||||
payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
|
||||
journal_currency = journal.currency_id or journal.company_id.currency_id
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_method_id': payment_methods and payment_methods[0].id or False,
|
||||
'payment_type': total < 0 and 'outbound' or 'inbound',
|
||||
'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
|
||||
'partner_type': 'supplier',
|
||||
'journal_id': journal.id,
|
||||
'payment_date': expense.date,
|
||||
'state': 'reconciled',
|
||||
'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id,
|
||||
'amount': diff_currency_p and abs(total_currency) or abs(total),
|
||||
'name': expense.name,
|
||||
})
|
||||
payment_id = payment.id
|
||||
else:
|
||||
if not expense.employee_id.address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
|
||||
@@ -212,6 +234,7 @@ class HrExpense(models.Model):
|
||||
'date_maturity': acc_date,
|
||||
'amount_currency': diff_currency_p and total_currency or False,
|
||||
'currency_id': diff_currency_p and expense.currency_id.id or False,
|
||||
'payment_id': payment_id,
|
||||
})
|
||||
|
||||
#convert eml into an osv-valid format
|
||||
|
||||
@@ -213,5 +213,5 @@ class Employee(models.Model):
|
||||
('date_from', '<=', today_end),
|
||||
('date_to', '>=', today_start),
|
||||
('type', '=', 'remove')
|
||||
], ['employee_id'])
|
||||
])
|
||||
return [('id', 'in', holidays.mapped('employee_id').ids)]
|
||||
|
||||
@@ -77,10 +77,15 @@ class EscposDriver(Thread):
|
||||
printers = usb.core.find(find_all=True, idVendor=0x0519)
|
||||
|
||||
for printer in printers:
|
||||
try:
|
||||
description = usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
|
||||
except Exception as e:
|
||||
_logger.error("Can not get printer description: %s" % (e.message or repr(e)))
|
||||
description = 'Unknown printer'
|
||||
connected.append({
|
||||
'vendor': printer.idVendor,
|
||||
'product': printer.idProduct,
|
||||
'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
|
||||
'name': description
|
||||
})
|
||||
|
||||
return connected
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
<field name="account_id" ref="p11235"/>
|
||||
<field name="refund_account_id" ref="p20011"/>
|
||||
<field name="chart_template_id" ref="indian_chart_template_standard"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_70'), ref('st_tag_in_21')])]"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_21'), ref('st_tag_in_66')])]"/>
|
||||
</record>
|
||||
<record id="input_tax_12" model="account.tax.template">
|
||||
<field name="name">(standard)Input Sales Tax @ 12%</field>
|
||||
@@ -474,7 +474,7 @@
|
||||
<field name="account_id" ref="p11235"/>
|
||||
<field name="refund_account_id" ref="p20011"/>
|
||||
<field name="chart_template_id" ref="indian_chart_template_standard"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_56')])]"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_18'), ref('st_tag_in_63')])]"/>
|
||||
</record>
|
||||
<record id="input_tax_4" model="account.tax.template">
|
||||
<field name="name">(standard)Input Sales Tax @ 4%</field>
|
||||
@@ -486,7 +486,7 @@
|
||||
<field name="account_id" ref="p11235"/>
|
||||
<field name="refund_account_id" ref="p20011"/>
|
||||
<field name="chart_template_id" ref="indian_chart_template_standard"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_15'), ref('st_tag_in_70')])]"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_15'), ref('st_tag_in_60')])]"/>
|
||||
</record>
|
||||
<record id="add_input_vat_1" model="account.tax.template">
|
||||
<field name="name">(standard)Additional Input VAT @ 1%</field>
|
||||
@@ -605,7 +605,7 @@
|
||||
<field name="account_id" ref="p112341"/>
|
||||
<field name="refund_account_id" ref="p20011"/>
|
||||
<field name="chart_template_id" ref="indian_chart_template_standard"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_25')])]"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_25'), ref('st_tag_in_63')])]"/>
|
||||
</record>
|
||||
<record id="sale_edu_cess_service_tax_2" model="account.tax.template">
|
||||
<field name="name">(standard)Input Education Cess @ 2% On Service Tax</field>
|
||||
|
||||
@@ -1109,37 +1109,23 @@ class MailThread(models.AbstractModel):
|
||||
reply_match = False
|
||||
|
||||
if reply_match:
|
||||
compat_mode = False
|
||||
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
||||
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
||||
|
||||
# message is a reply to an existing thread (6.1 compatibility)
|
||||
# do not match forwarded emails from another OpenERP system (thread_id collision!)
|
||||
if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env:
|
||||
mail_messages = MailMessage.search([
|
||||
('message_id', '=', False),
|
||||
('model', '=', reply_model),
|
||||
('res_id', '=', reply_thread_id)])
|
||||
compat_mode = True
|
||||
|
||||
if mail_messages:
|
||||
model, thread_id = mail_messages.model, mail_messages.res_id
|
||||
if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior
|
||||
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)], limit=1)
|
||||
|
||||
# TDE Note: compat mode = without context key, why ? because
|
||||
route = self.message_route_verify(
|
||||
message, message_dict,
|
||||
(model, thread_id, custom_values, self._uid, dest_aliases),
|
||||
update_author=True, assert_model=reply_private, create_fallback=True,
|
||||
allow_private=reply_private, drop_alias=True)
|
||||
if route:
|
||||
# TDE Note: compat mode: parent is invalid for a compat-reply
|
||||
# message_dict.pop('parent_id', None)
|
||||
# TDE note: add compat mode for compat mode in debug
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid)
|
||||
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
elif route is False:
|
||||
return []
|
||||
|
||||
@@ -580,25 +580,29 @@ class TestMailgateway(TestMail):
|
||||
msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>')
|
||||
|
||||
# when 6.1 messages are present, compat mode is available
|
||||
# Odoo 10 update: compat mode has been removed and should not work anymore
|
||||
self.fake_email.write({'message_id': False})
|
||||
# Do: compat mode accepts partial-matching emails
|
||||
frog_groups = self.format_and_process(
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.format_and_process,
|
||||
MAIL_TEMPLATE, email_from='other5@gmail.com',
|
||||
msg_id='<1.2.JavaMail.new@agrolait.com>',
|
||||
to='noone@example.com>', subject='spam',
|
||||
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@%s>' % (self.group_public.id, socket.gethostname()))
|
||||
|
||||
# 3''. 6.1 compat mode should not work if hostname does not match!
|
||||
# Odoo 10 update: compat mode has been removed and should not work anymore and does not depend from hostname
|
||||
self.assertRaises(ValueError,
|
||||
self.format_and_process,
|
||||
MAIL_TEMPLATE, email_from='other5@gmail.com',
|
||||
msg_id='<1.3.JavaMail.new@agrolait.com>',
|
||||
to='noone@example.com>', subject='spam',
|
||||
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % frog_groups.id)
|
||||
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % self.group_public.id)
|
||||
|
||||
# Test created messages
|
||||
self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain 6 messages')
|
||||
self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0, 'message_process: msg1 should not have children')
|
||||
self.assertEqual(len(self.group_public.message_ids), 1)
|
||||
self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
|
||||
def test_message_process_duplicate(self):
|
||||
|
||||
@@ -410,6 +410,10 @@ var PosDB = core.Class.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// Only necessary when we store a new, validated order. Orders
|
||||
// that where already stored should already have been removed.
|
||||
this.remove_unpaid_order(order);
|
||||
|
||||
orders.push({id: order_id, data: order});
|
||||
this.save('orders',orders);
|
||||
return order_id;
|
||||
|
||||
@@ -1895,7 +1895,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
}
|
||||
|
||||
// if the change is too large, it's probably an input error, make the user confirm.
|
||||
if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
|
||||
if (!force_validation && order.get_total_with_tax() > 0 && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
|
||||
this.gui.show_popup('confirm',{
|
||||
title: _t('Please Confirm Large Amount'),
|
||||
body: _t('Are you sure that the customer wants to pay') +
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import models, fields, api
|
||||
@@ -29,7 +29,7 @@ class pos_cache(models.Model):
|
||||
prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
|
||||
res = prod_ctx.read(self.get_product_fields())
|
||||
datas = {
|
||||
'cache': cPickle.dumps(res),
|
||||
'cache': base64.encodestring(cPickle.dumps(res)),
|
||||
}
|
||||
|
||||
self.write(datas)
|
||||
@@ -49,7 +49,8 @@ class pos_cache(models.Model):
|
||||
self.product_fields = str(fields)
|
||||
self.refresh_cache()
|
||||
|
||||
return cPickle.loads(self.cache)
|
||||
cache = base64.decodestring(self.cache)
|
||||
return cPickle.loads(cache)
|
||||
|
||||
|
||||
class pos_config(models.Model):
|
||||
|
||||
@@ -30,6 +30,35 @@ class AccountInvoice(models.Model):
|
||||
]}
|
||||
return result
|
||||
|
||||
def _prepare_invoice_line_from_po_line(self, line):
|
||||
if line.product_id.purchase_method == 'purchase':
|
||||
qty = line.product_qty - line.qty_invoiced
|
||||
else:
|
||||
qty = line.qty_received - line.qty_invoiced
|
||||
if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
|
||||
qty = 0.0
|
||||
taxes = line.taxes_id
|
||||
invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
|
||||
invoice_line = self.env['account.invoice.line']
|
||||
data = {
|
||||
'purchase_line_id': line.id,
|
||||
'name': self.purchase_id.name+': '+line.name,
|
||||
'origin': self.purchase_id.origin,
|
||||
'uom_id': line.product_uom.id,
|
||||
'product_id': line.product_id.id,
|
||||
'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
|
||||
'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
|
||||
'quantity': qty,
|
||||
'discount': 0.0,
|
||||
'account_analytic_id': line.account_analytic_id.id,
|
||||
'analytic_tag_ids': line.analytic_tag_ids.ids,
|
||||
'invoice_line_tax_ids': invoice_line_tax_ids.ids
|
||||
}
|
||||
account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
|
||||
if account:
|
||||
data['account_id'] = account.id
|
||||
return data
|
||||
|
||||
# Load all unsold PO lines
|
||||
@api.onchange('purchase_id')
|
||||
def purchase_order_change(self):
|
||||
@@ -43,31 +72,7 @@ class AccountInvoice(models.Model):
|
||||
# Load a PO line only once
|
||||
if line in self.invoice_line_ids.mapped('purchase_line_id'):
|
||||
continue
|
||||
if line.product_id.purchase_method == 'purchase':
|
||||
qty = line.product_qty - line.qty_invoiced
|
||||
else:
|
||||
qty = line.qty_received - line.qty_invoiced
|
||||
if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
|
||||
qty = 0.0
|
||||
taxes = line.taxes_id
|
||||
invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
|
||||
data = {
|
||||
'purchase_line_id': line.id,
|
||||
'name': self.purchase_id.name+': '+line.name,
|
||||
'origin': self.purchase_id.origin,
|
||||
'uom_id': line.product_uom.id,
|
||||
'product_id': line.product_id.id,
|
||||
'account_id': self.env['account.invoice.line'].with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
|
||||
'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
|
||||
'quantity': qty,
|
||||
'discount': 0.0,
|
||||
'account_analytic_id': line.account_analytic_id.id,
|
||||
'analytic_tag_ids': line.analytic_tag_ids.ids,
|
||||
'invoice_line_tax_ids': invoice_line_tax_ids.ids
|
||||
}
|
||||
account = new_lines.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
|
||||
if account:
|
||||
data['account_id'] = account.id
|
||||
data = self._prepare_invoice_line_from_po_line(line)
|
||||
new_line = new_lines.new(data)
|
||||
new_line._set_additional_fields(self)
|
||||
new_lines += new_line
|
||||
|
||||
@@ -194,16 +194,16 @@ class PurchaseOrder(models.Model):
|
||||
taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids
|
||||
|
||||
# Compute quantity and price_unit
|
||||
if requisition.type_id.quantity_copy != 'copy':
|
||||
product_qty = 0
|
||||
price_unit = line.price_unit
|
||||
elif line.product_uom_id != line.product_id.uom_po_id:
|
||||
if line.product_uom_id != line.product_id.uom_po_id:
|
||||
product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
|
||||
price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
|
||||
else:
|
||||
product_qty = line.product_qty
|
||||
price_unit = line.price_unit
|
||||
|
||||
if requisition.type_id.quantity_copy != 'copy':
|
||||
product_qty = 0
|
||||
|
||||
# Compute price_unit in appropriate currency
|
||||
if requisition.company_id.currency_id != currency:
|
||||
price_unit = requisition.company_id.currency_id.compute(price_unit, currency)
|
||||
@@ -258,6 +258,24 @@ class PurchaseOrder(models.Model):
|
||||
return result
|
||||
|
||||
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = "purchase.order.line"
|
||||
|
||||
@api.onchange('product_qty', 'product_uom')
|
||||
def _onchange_quantity(self):
|
||||
res = super(PurchaseOrderLine, self)._onchange_quantity()
|
||||
if self.order_id.requisition_id:
|
||||
for line in self.order_id.requisition_id.line_ids:
|
||||
if line.product_id == self.product_id:
|
||||
if line.product_uom_id != self.product_uom:
|
||||
self.price_unit = self.env['product.uom']._compute_price(
|
||||
line.product_uom_id.id, line.price_unit, to_uom_id=self.product_uom.id)
|
||||
else:
|
||||
self.price_unit = line.price_unit
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ class ProjectTaskType(models.Model):
|
||||
|
||||
_inherit = 'project.task.type'
|
||||
|
||||
def _default_domain_rating_template_id(self):
|
||||
return [('model', '=', 'project.task')]
|
||||
|
||||
rating_template_id = fields.Many2one(
|
||||
'mail.template',
|
||||
string='Rating Email Template',
|
||||
domain=[('model', '=', 'project.task')],
|
||||
domain=lambda self: self._default_domain_rating_template_id(),
|
||||
help="Select an email template. An email will be sent to the customer when the task reach this step.")
|
||||
auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False,
|
||||
help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n"
|
||||
|
||||
@@ -28,6 +28,16 @@ class ProjectIssue(models.Model):
|
||||
return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating")
|
||||
|
||||
|
||||
|
||||
class Stage(models.Model):
|
||||
|
||||
_inherit = ['project.task.type']
|
||||
|
||||
def _default_domain_rating_template_id(self):
|
||||
domain = super(Stage, self)._default_domain_rating_template_id()
|
||||
return ['|'] + domain + [('model', '=', 'project.issue')]
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
|
||||
_inherit = "project.project"
|
||||
|
||||
@@ -83,6 +83,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
else:
|
||||
amount = self.amount
|
||||
name = _('Down Payment')
|
||||
if order.fiscal_position_id and self.product_id.taxes_id:
|
||||
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
|
||||
else:
|
||||
tax_ids = self.product_id.taxes_id.ids
|
||||
|
||||
invoice = inv_obj.create({
|
||||
'name': order.client_order_ref or order.name,
|
||||
@@ -102,7 +106,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
'uom_id': self.product_id.uom_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'sale_line_ids': [(6, 0, [so_line.id])],
|
||||
'invoice_line_tax_ids': [(6, 0, [x.id for x in self.product_id.taxes_id])],
|
||||
'invoice_line_tax_ids': [(6, 0, tax_ids)],
|
||||
'account_analytic_id': order.project_id.id or False,
|
||||
})],
|
||||
'currency_id': order.pricelist_id.currency_id.id,
|
||||
@@ -142,6 +146,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
|
||||
if self.product_id.type != 'service':
|
||||
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
|
||||
if order.fiscal_position_id and self.product_id.taxes_id:
|
||||
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
|
||||
else:
|
||||
tax_ids = self.product_id.taxes_id.ids
|
||||
so_line = sale_line_obj.create({
|
||||
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
|
||||
'price_unit': amount,
|
||||
@@ -150,7 +158,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
'discount': 0.0,
|
||||
'product_uom': self.product_id.uom_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'tax_id': [(6, 0, self.product_id.taxes_id.ids)],
|
||||
'tax_id': [(6, 0, tax_ids)],
|
||||
'is_downpayment': True,
|
||||
})
|
||||
self._create_invoice(order, so_line, amount)
|
||||
|
||||
@@ -393,16 +393,16 @@ class ProductTemplate(models.Model):
|
||||
('none', 'No Tracking')], string="Tracking", default='none', required=True)
|
||||
description_picking = fields.Text('Description on Picking', translate=True)
|
||||
qty_available = fields.Float(
|
||||
'Quantity On Hand', compute='_compute_quantities', search='_search_quantities',
|
||||
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
virtual_available = fields.Float(
|
||||
'Forecasted Quantity', compute='_compute_quantities', search='_search_quantities',
|
||||
'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
incoming_qty = fields.Float(
|
||||
'Incoming', compute='_compute_quantities', search='_search_quantities',
|
||||
'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
outgoing_qty = fields.Float(
|
||||
'Outgoing', compute='_compute_quantities', search='_search_quantities',
|
||||
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
location_id = fields.Many2one('stock.location', 'Location')
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
|
||||
@@ -452,9 +452,23 @@ class ProductTemplate(models.Model):
|
||||
}
|
||||
return prod_available
|
||||
|
||||
def _search_quantities(self, operator, value):
|
||||
# TDE FIXME: does this work anyway ?
|
||||
domain = [] # TDE ADDED
|
||||
def _search_qty_available(self, operator, value):
|
||||
domain = [('qty_available', operator, value)]
|
||||
product_variant_ids = self.env['product.product'].search(domain)
|
||||
return [('product_variant_ids', 'in', product_variant_ids.ids)]
|
||||
|
||||
def _search_virtual_available(self, operator, value):
|
||||
domain = [('virtual_available', operator, value)]
|
||||
product_variant_ids = self.env['product.product'].search(domain)
|
||||
return [('product_variant_ids', 'in', product_variant_ids.ids)]
|
||||
|
||||
def _search_incoming_qty(self, operator, value):
|
||||
domain = [('incoming_qty', operator, value)]
|
||||
product_variant_ids = self.env['product.product'].search(domain)
|
||||
return [('product_variant_ids', 'in', product_variant_ids.ids)]
|
||||
|
||||
def _search_outgoing_qty(self, operator, value):
|
||||
domain = [('outgoing_qty', operator, value)]
|
||||
product_variant_ids = self.env['product.product'].search(domain)
|
||||
return [('product_variant_ids', 'in', product_variant_ids.ids)]
|
||||
|
||||
|
||||
@@ -91,14 +91,12 @@ class Inventory(models.Model):
|
||||
('category', _('One product category')),
|
||||
('product', _('One product only')),
|
||||
('partial', _('Select products manually'))]
|
||||
stock_settings = self.env['stock.config.settings'].search([], limit=1, order='id DESC')
|
||||
if not stock_settings:
|
||||
return res_filter
|
||||
if stock_settings.group_stock_tracking_owner:
|
||||
|
||||
if self.user_has_groups('stock.group_tracking_owner'):
|
||||
res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))]
|
||||
if stock_settings.group_stock_production_lot:
|
||||
if self.user_has_groups('stock.group_production_lot'):
|
||||
res_filter.append(('lot', _('One Lot/Serial Number')))
|
||||
if stock_settings.group_stock_tracking_lot:
|
||||
if self.user_has_groups('stock.group_tracking_lot'):
|
||||
res_filter.append(('pack', _('A Pack')))
|
||||
return res_filter
|
||||
|
||||
|
||||
@@ -194,7 +194,6 @@ class StockMove(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _compute_string_qty_information(self):
|
||||
StockConfig = self.env['stock.config.settings']
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
void_moves = self.filtered(lambda move: move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal')
|
||||
other_moves = self - void_moves
|
||||
@@ -205,9 +204,7 @@ class StockMove(models.Model):
|
||||
total_available = move.product_id.uom_id._compute_quantity(total_available, move.product_uom, round=False)
|
||||
total_available = float_round(total_available, precision_digits=precision)
|
||||
info = str(total_available)
|
||||
# look in the settings if we need to display the UoM name or not
|
||||
config = StockConfig.search([], limit=1, order='id DESC')
|
||||
if config and config.group_uom:
|
||||
if self.user_has_groups('product.group_uom'):
|
||||
info += ' ' + move.product_uom.name
|
||||
if move.reserved_availability:
|
||||
if move.reserved_availability != total_available:
|
||||
@@ -380,7 +377,7 @@ class StockMove(models.Model):
|
||||
# if no specialized push rule has been found yet, we try to find a general one (without route)
|
||||
rules = Push.search(domain + [('route_id', '=', False)], order='sequence')
|
||||
# Make sure it is not returning the return
|
||||
if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_id.id != rules.location_dest_id.id):
|
||||
if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_dest_id.id != rules.location_dest_id.id):
|
||||
rules._apply(move)
|
||||
return True
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class AccountInvoiceLine(models.Model):
|
||||
|
||||
def _get_price(self, company_currency, price_unit):
|
||||
if self.invoice_id.currency_id.id != company_currency:
|
||||
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id)
|
||||
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id)
|
||||
else:
|
||||
price = price_unit * self.quantity
|
||||
return round(price, self.invoice_id.currency_id.decimal_places)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
@@ -105,7 +106,7 @@ class ProductProduct(models.Model):
|
||||
for location in locations:
|
||||
for product in self.with_context(location=location.id, compute_child=False):
|
||||
diff = product.standard_price - new_price
|
||||
if diff:
|
||||
if float_is_zero(diff, precision_rounding=product.currency_id.rounding):
|
||||
raise UserError(_("No difference between standard price and new price!"))
|
||||
if not product_accounts[product.id].get('stock_valuation', False):
|
||||
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
|
||||
|
||||
@@ -115,6 +115,11 @@ class StockQuant(models.Model):
|
||||
else:
|
||||
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id)
|
||||
|
||||
if move.company_id.anglo_saxon_accounting and move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'customer':
|
||||
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
|
||||
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
|
||||
self.with_context(force_company=move.company_id.id)._create_account_move_line(move, acc_src, acc_dest, journal_id)
|
||||
|
||||
def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id):
|
||||
# group quants by cost
|
||||
quant_cost_qty = defaultdict(lambda: 0.0)
|
||||
|
||||
@@ -459,6 +459,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
}
|
||||
},
|
||||
do_load_state: function(state, warm) {
|
||||
if (state.view_type && state.view_type !== this.active_view.type) {
|
||||
this.switch_mode(state.view_type, true);
|
||||
}
|
||||
this.active_view.controller.do_load_state(state, warm);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -430,6 +430,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, {
|
||||
this.process_modifiers();
|
||||
this._super();
|
||||
this.$el.addClass(this.node.attrs["class"] || "");
|
||||
this.$el.attr('style', this.node.attrs.style);
|
||||
},
|
||||
destroy: function() {
|
||||
$.fn.tooltip('destroy');
|
||||
|
||||
@@ -1275,6 +1275,9 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
return self.mutex.def;
|
||||
});
|
||||
},
|
||||
is_false: function() {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
var Many2ManyListView = X2ManyListView.extend({
|
||||
|
||||
@@ -1733,7 +1733,7 @@ var Column = Class.extend({
|
||||
}
|
||||
|
||||
var aggregation_func = (this.sum && 'sum') || (this.avg && 'avg') ||
|
||||
(this.max && 'max') || (this.min && 'min') || this.group_operator;
|
||||
(this.max && 'max') || (this.min && 'min');
|
||||
|
||||
if (!aggregation_func) {
|
||||
return {};
|
||||
|
||||
@@ -113,25 +113,37 @@ return AbstractWebClient.extend({
|
||||
}
|
||||
},
|
||||
on_hashchange: function(event) {
|
||||
var self = this;
|
||||
var stringstate = event.getState(false);
|
||||
if (!_.isEqual(this._current_state, stringstate)) {
|
||||
var state = event.getState(true);
|
||||
if(!state.action && state.menu_id) {
|
||||
self.menu.is_bound.done(function() {
|
||||
self.menu.menu_click(state.menu_id);
|
||||
});
|
||||
} else {
|
||||
state._push_me = false; // no need to push state back...
|
||||
this.action_manager.do_load_state(state, !!this._current_state).then(function () {
|
||||
var action = self.action_manager.get_inner_action();
|
||||
if (action) {
|
||||
self.menu.open_action(action.action_descr.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this._ignore_hashchange) {
|
||||
this._ignore_hashchange = false;
|
||||
return;
|
||||
}
|
||||
this._current_state = stringstate;
|
||||
|
||||
var self = this;
|
||||
this.clear_uncommitted_changes().then(function () {
|
||||
var stringstate = event.getState(false);
|
||||
if (!_.isEqual(self._current_state, stringstate)) {
|
||||
var state = event.getState(true);
|
||||
if(!state.action && state.menu_id) {
|
||||
self.menu.is_bound.done(function() {
|
||||
self.menu.menu_click(state.menu_id);
|
||||
});
|
||||
} else {
|
||||
state._push_me = false; // no need to push state back...
|
||||
self.action_manager.do_load_state(state, !!self._current_state).then(function () {
|
||||
var action = self.action_manager.get_inner_action();
|
||||
if (action) {
|
||||
self.menu.open_action(action.action_descr.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
self._current_state = stringstate;
|
||||
}, function () {
|
||||
if (event) {
|
||||
self._ignore_hashchange = true;
|
||||
window.location = event.originalEvent.oldURL;
|
||||
}
|
||||
});
|
||||
},
|
||||
on_menu_action: function(options) {
|
||||
var self = this;
|
||||
|
||||
@@ -460,6 +460,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notebooks
|
||||
.o_notebook {
|
||||
.nav-tabs {
|
||||
> .tab-pane {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overriden style when form view in modal
|
||||
|
||||
@@ -226,7 +226,7 @@ class Web_Editor(http.Controller):
|
||||
@http.route('/web_editor/customize_template_get', type='json', auth='user', website=True)
|
||||
def customize_template_get(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``.
|
||||
returns templates (which can be active or not)
|
||||
returns templates info (which can be active or not)
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
|
||||
|
||||
@@ -175,42 +175,21 @@ class IrUiView(models.Model):
|
||||
return views_to_return
|
||||
|
||||
@api.model
|
||||
def customize_template_get(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``.
|
||||
returns templates (which can be active or not)
|
||||
def _customize_template_get_views(self, key, full=False, bundles=False):
|
||||
""" Get inherit views of the template ``key``.
|
||||
returns views (which can be active or not)
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
user = self.env.user
|
||||
user_groups = set(user.groups_id)
|
||||
user_groups = set(self.env.user.groups_id)
|
||||
views = self.with_context(active_test=False)._views_get(key, bundles=bundles)
|
||||
views = views.filtered(lambda v: user_groups.issuperset(v.groups_id))
|
||||
return views
|
||||
|
||||
view_theme_id = self.env['ir.model.data'].xmlid_to_res_id('website.theme')
|
||||
done = set()
|
||||
result = []
|
||||
for view in views:
|
||||
if full or (view.customize_show and view.inherit_id.id != view_theme_id):
|
||||
if not user_groups.issuperset(view.groups_id):
|
||||
continue
|
||||
if view.inherit_id not in done:
|
||||
result.append({
|
||||
'name': view.inherit_id.name,
|
||||
'id': view.id,
|
||||
'key': view.key,
|
||||
'xml_id': view.xml_id,
|
||||
'arch': view.arch,
|
||||
'inherit_id': view.inherit_id.id,
|
||||
'header': True,
|
||||
'active': False
|
||||
})
|
||||
done.add(view.inherit_id)
|
||||
result.append({
|
||||
'name': view.name,
|
||||
'id': view.id,
|
||||
'key': view.key,
|
||||
'xml_id': view.xml_id,
|
||||
'arch': view.arch,
|
||||
'inherit_id': view.inherit_id.id,
|
||||
'header': False,
|
||||
'active': view.active,
|
||||
})
|
||||
return result
|
||||
@api.model
|
||||
def customize_template_get(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``.
|
||||
returns templates info (which can be active or not)
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
views = self._customize_template_get_views(key, full=full, bundles=bundles)
|
||||
return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
|
||||
|
||||
@@ -149,7 +149,7 @@ var ViewEditor = Widget.extend({
|
||||
view.children = [];
|
||||
});
|
||||
_.each(index, function (view) {
|
||||
var parentId = view.inherit_id;
|
||||
var parentId = view.inherit_id[0];
|
||||
if (parentId && index[parentId]) {
|
||||
index[parentId].children.push(view);
|
||||
} else {
|
||||
|
||||
@@ -1145,7 +1145,7 @@ var LinkDialog = Dialog.extend({
|
||||
$preview.attr("target", new_window ? '_blank' : "")
|
||||
.attr("href", url && url.length ? url : "#")
|
||||
.html((label && label.length ? label : url))
|
||||
.attr("class", classes.replace(/pull-\w+/, ''));
|
||||
.attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,7 +189,7 @@ body .modal {
|
||||
ul.pager {
|
||||
margin: 0;
|
||||
> li > a {
|
||||
border-radius: 3px;
|
||||
border-radius: 0px;
|
||||
margin-left: 2px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
@@ -206,20 +206,34 @@ body .modal {
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown-toggle {
|
||||
line-height: 0;
|
||||
height: 34px;
|
||||
|
||||
.well {
|
||||
border-top-width: 0;
|
||||
|
||||
label {
|
||||
margin: auto 10px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_existing_attachment_cell {
|
||||
@odoo-border-image: 1px;
|
||||
.o_existing_attachment_remove {
|
||||
.o-position-absolute(@odoo-border-image, 15px + @odoo-border-image);
|
||||
padding: 3px 3px 12px 12px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid black;
|
||||
border-left: 1px solid black;
|
||||
border-radius: 0 0 0 100%;
|
||||
opacity: 0.8;
|
||||
padding: 3px 5px 5px 5px;
|
||||
background-color: fade(white, 80%);
|
||||
text-shadow: 0 0 1px white;
|
||||
border-radius: 0 0 0 2px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
color: @o-we-color-danger;
|
||||
}
|
||||
}
|
||||
.o_image {
|
||||
border: @odoo-border-image solid transparent;
|
||||
@@ -235,15 +249,29 @@ body .modal {
|
||||
border-color: @odoo-brand-primary;
|
||||
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
|
||||
}
|
||||
&:hover {
|
||||
.o_existing_attachment_remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.font-icons-icons > span {
|
||||
font-size: 30px;
|
||||
margin: 5px;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
.font-icons-icons {
|
||||
padding: 20px 0;
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
|
||||
> span {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
margin: 5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&.o_selected {
|
||||
cursor: auto;
|
||||
background-color: @o-we-color-paper;
|
||||
box-shadow: 0px 0px 0px 1px @o-we-color-success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
@o-we-color-text-normal: #999999;
|
||||
@o-we-color-text-light: #d4d5d7;
|
||||
@o-we-color-danger: #e6586c;
|
||||
@o-we-color-warning: #f0ad4e;
|
||||
@o-we-color-success: #40ad67;
|
||||
@o-we-color-info: #6999a8;
|
||||
|
||||
@o-we-sidebar-width: 210px;
|
||||
|
||||
@@ -76,3 +79,119 @@
|
||||
from { background: @odoo-brand-optional; }
|
||||
to { width: 0; background: transparent; }
|
||||
}
|
||||
|
||||
|
||||
// ============ Preservetion mixins ===============
|
||||
// Used to preserve web_editor and website UI from themes customizations
|
||||
|
||||
.o-w-preserve-base() {
|
||||
font-family: @o-we-font-family;
|
||||
line-height: 1.42857143;
|
||||
color: @o-we-color-divider;
|
||||
|
||||
.text-muted {
|
||||
color: @o-we-color-text-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-headings() {
|
||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
font-family: @o-we-font-family;
|
||||
line-height: 1.42857143;
|
||||
color: @o-we-color-dark;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-links() {
|
||||
a:not(.o_btn_preview) {
|
||||
color: @odoo-brand-optional;
|
||||
|
||||
&:focus, &:active, &:focus:active {
|
||||
outline: none!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-forms() {
|
||||
.form-control {
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid @o-we-color-text-light;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-modals() {
|
||||
background: 0;
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0;
|
||||
background-color: @o-we-color-paper;
|
||||
|
||||
.modal-body {
|
||||
background-color: white;
|
||||
}
|
||||
.modal-footer {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-btn() {
|
||||
.btn:not(.o_btn_preview) {
|
||||
border-width: 1px;
|
||||
border-radius: 0;
|
||||
font-weight: normal;
|
||||
.button-size(6px; 12px; 14px; 1.42857143; 0);
|
||||
|
||||
&.btn-primary {
|
||||
.button-variant(white, @odoo-brand-optional, darken(@odoo-brand-optional,5%));
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.btn-default {
|
||||
.button-variant(@odoo-brand-optional, white, @o-we-color-text-light);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.btn-link {
|
||||
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
|
||||
}
|
||||
&.btn-success {
|
||||
.button-variant(white; @o-we-color-success; darken(@o-we-color-success, 5%));
|
||||
}
|
||||
&.btn-info {
|
||||
.button-variant(white; @o-we-color-info; darken(@o-we-color-info, 5%));
|
||||
}
|
||||
&.btn-warning {
|
||||
.button-variant(white; @o-we-color-warning; darken(@o-we-color-warning, 5%));
|
||||
}
|
||||
&.btn-danger {
|
||||
.button-variant(white; @o-we-color-danger; darken(@o-we-color-danger, 5%));
|
||||
}
|
||||
|
||||
&.btn-lg {
|
||||
.button-size(10px; 16px; 18px; 1.3333333; 0);
|
||||
}
|
||||
&.btn-sm {
|
||||
.button-size(5px; 10px; 12px; 1.5; 0);
|
||||
}
|
||||
&.btn-xs {
|
||||
.button-size(1px; 5px; 12px; 1.5; 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o-w-preserve-wells() {
|
||||
.well {
|
||||
padding: 19px;
|
||||
margin-bottom: 20px;
|
||||
background-color: @o-we-color-paper;
|
||||
border: 1px solid darken(@o-we-color-paper,5%);
|
||||
border-radius: 0;
|
||||
.box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,31 +251,31 @@
|
||||
<span class="caret"/></a>
|
||||
<ul aria-labelledby="dLabel" class="dropdown-menu" role="menu">
|
||||
<li>
|
||||
<label class="btn btn-link">Link
|
||||
<label class="btn btn-link o_btn_preview">Link
|
||||
<input checked="checked" class="link-style" name="link-style-type" type="radio" value=""/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-default">Basic
|
||||
<label class="btn btn-default o_btn_preview">Basic
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-default"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-success">Success
|
||||
<label class="btn btn-success o_btn_preview">Success
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-success"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-primary">Primary
|
||||
<label class="btn btn-primary o_btn_preview">Primary
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-primary"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-info">Info
|
||||
<label class="btn btn-info o_btn_preview">Info
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-info"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-warning">Warning
|
||||
<label class="btn btn-warning o_btn_preview">Warning
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-warning"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-danger">Danger
|
||||
<label class="btn btn-danger o_btn_preview">Danger
|
||||
<input class="link-style" name="link-style-type" type="radio" value="btn-danger"/></label>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -287,19 +287,19 @@
|
||||
<span class="caret"/></a>
|
||||
<ul aria-labelledby="dLabel" class="dropdown-menu" role="menu">
|
||||
<li>
|
||||
<label class="btn btn-default btn-xs">Extra Small
|
||||
<label class="btn btn-default btn-xs o_btn_preview">Extra Small
|
||||
<input class="link-style" name="link-style-size" type="radio" value="btn-xs"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-default btn-sm">Small
|
||||
<label class="btn btn-default btn-sm o_btn_preview">Small
|
||||
<input class="link-style" name="link-style-size" type="radio" value="btn-sm"/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-default">Default
|
||||
<label class="btn btn-default o_btn_preview">Default
|
||||
<input checked="checked" class="link-style" name="link-style-size" type="radio" value=""/></label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="btn btn-default btn-lg">Large
|
||||
<label class="btn btn-default btn-lg o_btn_preview">Large
|
||||
<input class="link-style" name="link-style-size" type="radio" value="btn-lg"/></label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -62,7 +62,7 @@ var KanbanView = View.extend({
|
||||
|
||||
this.qweb = new QWeb(session.debug, {_s: session.origin});
|
||||
|
||||
this.limit = this.options.limit || 40;
|
||||
this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40;
|
||||
this.fields = {};
|
||||
this.fields_keys = _.keys(this.fields_view.fields);
|
||||
this.grouped = undefined;
|
||||
|
||||
@@ -262,10 +262,10 @@ class Website(Home):
|
||||
|
||||
@http.route('/website/customize_template_get', type='json', auth='user', website=True)
|
||||
def customize_template_get(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``. By default, only
|
||||
:returns ``customize_show`` templates (which can be active or not), if
|
||||
``full=True`` returns inherit view's informations of the template ``key``.
|
||||
``bundles=True`` returns also the asset bundles
|
||||
""" Get inherit view's informations of the template ``key``.
|
||||
returns templates info (which can be active or not)
|
||||
``full=False`` returns only the customize_show template
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
|
||||
|
||||
@@ -370,6 +370,7 @@ class Website(Home):
|
||||
], type='http', auth="public", website=True)
|
||||
def actions_server(self, path_or_xml_id_or_id, **post):
|
||||
ServerActions = request.env['ir.actions.server']
|
||||
action = action_id = None
|
||||
|
||||
# find the action_id: either an xml_id, the path, or an ID
|
||||
if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
|
||||
|
||||
@@ -136,25 +136,13 @@ class View(models.Model):
|
||||
return qcontext
|
||||
|
||||
@api.model
|
||||
def customize_template_get(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``. By default, only
|
||||
:returns ``customize_show`` templates (which can be active or not), if
|
||||
``full=True`` returns inherit view's informations of the template ``key``.
|
||||
``bundles=True`` returns also the asset bundles
|
||||
def _customize_template_get_views(self, key, full=False, bundles=False):
|
||||
""" Get inherit view's informations of the template ``key``.
|
||||
returns views (which can be active or not)
|
||||
``full=False`` returns only the customize_show template
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
result = super(View, self).customize_template_get(key, full=full, bundles=bundles)
|
||||
|
||||
imd = self.env['ir.model.data']
|
||||
view_theme_id = imd.xmlid_to_res_id('website.theme')
|
||||
|
||||
result_filtered = []
|
||||
for x in result:
|
||||
if not full:
|
||||
view = self.browse(x['id'])
|
||||
if not view.customize_show:
|
||||
continue
|
||||
if view_theme_id and x['inherit_id'] == view_theme_id:
|
||||
continue
|
||||
result_filtered.append(x)
|
||||
|
||||
return result_filtered
|
||||
views = super(View, self)._customize_template_get_views(key, full=full, bundles=bundles)
|
||||
if full:
|
||||
return views
|
||||
return views.filtered(lambda v: v.customize_show)
|
||||
|
||||
@@ -74,16 +74,17 @@ website.TopBarCustomize = Widget.extend({
|
||||
key: this.view_name,
|
||||
full: false
|
||||
}).then(function (result) {
|
||||
var current_group = "";
|
||||
_.each(result, function (item) {
|
||||
if (item.header) {
|
||||
$menu.append('<li class="dropdown-header">' + item.name + '</li>');
|
||||
} else {
|
||||
var $li = $('<li/>', {role: 'presentation'})
|
||||
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
|
||||
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
|
||||
$li.find('input').prop('checked', !!item.active);
|
||||
$menu.append($li);
|
||||
if (current_group !== item.inherit_id[1]) {
|
||||
current_group = item.inherit_id[1];
|
||||
$menu.append("<li class=\"dropdown-header\">" + current_group + "</li>");
|
||||
}
|
||||
var $li = $('<li/>', {role: 'presentation'})
|
||||
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
|
||||
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
|
||||
$li.find('input').prop('checked', !!item.active);
|
||||
$menu.append($li);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@ animation.registry.share = animation.Class.extend({
|
||||
animation.registry.media_video = animation.Class.extend({
|
||||
selector: ".media_iframe_video",
|
||||
start: function () {
|
||||
if (!this.$target.has('.media_iframe_video_size').length) {
|
||||
if (!this.$target.has('> iframe').length) {
|
||||
var editor = '<div class="css_editable_mode_display"> </div>';
|
||||
var size = '<div class="media_iframe_video_size"> </div>';
|
||||
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
|
||||
|
||||
@@ -311,8 +311,10 @@ options.registry.parallax = options.Class.extend({
|
||||
this.$target.data("snippet-view", new animation.registry.parallax(this.$target));
|
||||
}
|
||||
this.scroll();
|
||||
this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh.bind(this));
|
||||
this.$target.on('snippet-option-change snippet-option-preview', this._refresh.bind(this));
|
||||
|
||||
this._refresh_callback = this._refresh.bind(this);
|
||||
this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh_callback);
|
||||
this.$target.on('snippet-option-change snippet-option-preview', this._refresh_callback);
|
||||
},
|
||||
scroll: function (type, value) {
|
||||
this.$target.attr("data-scroll-background-ratio", value);
|
||||
@@ -330,6 +332,11 @@ options.registry.parallax = options.Class.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this._refresh();
|
||||
},
|
||||
on_remove: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$target.off("snippet-option-change snippet-option-preview", this._refresh_callback);
|
||||
this.buildingBlock.$el.off("snippet-dropped snippet-activated", this._refresh_callback);
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.ul = options.Class.extend({
|
||||
|
||||
@@ -1,52 +1,25 @@
|
||||
// MODALS
|
||||
body .modal {
|
||||
a {
|
||||
&:focus, &:active, &:focus:active {
|
||||
outline: none!important;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_website_modal {
|
||||
font-family: @o-we-font-family;
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
font-family: @o-we-font-family;
|
||||
}
|
||||
.o-w-preserve-base();
|
||||
.o-w-preserve-headings();
|
||||
.o-w-preserve-forms();
|
||||
.o-w-preserve-links();
|
||||
.o-w-preserve-btn();
|
||||
.o-w-preserve-wells();
|
||||
.o-w-preserve-modals();
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0;
|
||||
color: initial!important;
|
||||
background-color: @o-we-color-paper!important;
|
||||
.modal-header .o_subtitle {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.modal-body {
|
||||
color: inherit!important;
|
||||
background-color: transparent!important;
|
||||
.o_modal_header {
|
||||
.o-webclient-padding(@top: 10px, @bottom: 10px);
|
||||
.clearfix();
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
text-align: left;
|
||||
|
||||
.btn {
|
||||
border-radius: 0;
|
||||
|
||||
&.btn-primary {
|
||||
.button-variant(white, @odoo-brand-optional, @odoo-brand-optional);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.btn-default {
|
||||
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.btn-link {
|
||||
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
@@ -89,7 +62,6 @@ body .modal {
|
||||
|
||||
.modal-content {
|
||||
background-color: black!important;
|
||||
color: lightgray!important;
|
||||
border: 3px outset gray;
|
||||
border-radius: 20px;
|
||||
|
||||
@@ -99,6 +71,7 @@ body .modal {
|
||||
font-family: @o-we-font-family;
|
||||
|
||||
h4 {
|
||||
color: lightgray;
|
||||
font-family:inherit;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -109,6 +82,8 @@ body .modal {
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
background-color: inherit!important;
|
||||
border-radius: 20px;
|
||||
padding: 15px;
|
||||
|
||||
@mobile-preview-width: 320px;
|
||||
@@ -324,6 +299,10 @@ body .modal {
|
||||
|
||||
// ACE EDITOR
|
||||
.o_ace_view_editor {
|
||||
.o-w-preserve-base();
|
||||
.o-w-preserve-btn();
|
||||
.o-w-preserve-forms();
|
||||
|
||||
.o-position-absolute(@odoo-navbar-height, 0, 0);
|
||||
position: fixed;
|
||||
z-index: 1001;
|
||||
|
||||
@@ -139,6 +139,24 @@ class TestViewSaving(common.TransactionCase):
|
||||
Company = self.env['res.company']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# create a view with an xmlid, like the file import would
|
||||
with self.env.norecompute():
|
||||
self.view_id = View.create(self.cr, self.uid, {
|
||||
'name': "Test View",
|
||||
'type': 'qweb',
|
||||
'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8')
|
||||
})
|
||||
imd = self.env['ir.model.data'].create({
|
||||
'module': 'website',
|
||||
'name': 'test_view',
|
||||
'model': 'ir.ui.view',
|
||||
'res_id': self.view_id,
|
||||
})
|
||||
|
||||
# the xml_id of the view should not be flagged as 'noupdate'
|
||||
self.assertEqual(View.browse(self.cr, self.uid, self.view_id).model_data_id, imd)
|
||||
self.assertFalse(imd.noupdate)
|
||||
|
||||
replacement = ET.tostring(h.DIV(
|
||||
h.H3("Column 2"),
|
||||
h.UL(
|
||||
@@ -149,6 +167,9 @@ class TestViewSaving(common.TransactionCase):
|
||||
), encoding='utf-8')
|
||||
self.view_id.save(value=replacement, xpath='/div/div[2]')
|
||||
|
||||
# the xml_id of the view should be flagged as 'noupdate'
|
||||
self.assertTrue(imd.noupdate)
|
||||
|
||||
company = Company.browse(1)
|
||||
self.assertEqual(company.name, "Acme Corporation")
|
||||
self.assertEqual(company.phone, "+12 3456789")
|
||||
|
||||
@@ -31,6 +31,20 @@ class Blog(models.Model):
|
||||
post_ids.write({'active': vals['active']})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def message_post(self, parent_id=False, subtype=None, **kwargs):
|
||||
""" Temporary workaround to avoid spam. If someone replies on a channel
|
||||
through the 'Presentation Published' email, it should be considered as a
|
||||
note as we don't want all channel followers to be notified of this answer. """
|
||||
self.ensure_one()
|
||||
if parent_id:
|
||||
parent_message = self.env['mail.message'].sudo().browse(parent_id)
|
||||
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_blog.mt_blog_blog_published'):
|
||||
if kwargs.get('subtype_id'):
|
||||
kwargs['subtype_id'] = False
|
||||
subtype = 'mail.mt_note'
|
||||
return super(Blog, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
|
||||
|
||||
@api.multi
|
||||
def all_tags(self, min_limit=1):
|
||||
req = """
|
||||
@@ -87,11 +101,13 @@ class BlogPost(models.Model):
|
||||
blog_post.website_url = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post))
|
||||
|
||||
@api.multi
|
||||
@api.depends('post_date', 'visits')
|
||||
def _compute_ranking(self):
|
||||
res = {}
|
||||
for blog_post in self:
|
||||
age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
|
||||
res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
|
||||
if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange.
|
||||
age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
|
||||
res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
|
||||
return res
|
||||
|
||||
def _default_content(self):
|
||||
@@ -167,6 +183,8 @@ class BlogPost(models.Model):
|
||||
def _set_post_date(self):
|
||||
for blog_post in self:
|
||||
blog_post.published_date = blog_post.post_date
|
||||
if not blog_post.published_date:
|
||||
blog_post._write(dict(post_date=blog_post.create_date)) # dont trigger inverse function
|
||||
|
||||
def _check_for_publication(self, vals):
|
||||
if vals.get('website_published'):
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
<div class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by signing this proposal, I
|
||||
accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span>
|
||||
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
|
||||
@@ -325,7 +325,7 @@
|
||||
<div class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by paying this proposal, I
|
||||
accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span>
|
||||
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
|
||||
|
||||
@@ -207,7 +207,6 @@ class Product(models.Model):
|
||||
self.ensure_one()
|
||||
return self.product_tmpl_id.website_publish_button()
|
||||
|
||||
|
||||
class ProductAttribute(models.Model):
|
||||
_inherit = "product.attribute"
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ $(document).ready(function () {
|
||||
}
|
||||
$form.off('submit');
|
||||
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', params).then(function (data) {
|
||||
$(data).submit();
|
||||
$(data).appendTo('body').submit();
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -164,6 +164,20 @@ class Channel(models.Model):
|
||||
self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def message_post(self, parent_id=False, subtype=None, **kwargs):
|
||||
""" Temporary workaround to avoid spam. If someone replies on a channel
|
||||
through the 'Presentation Published' email, it should be considered as a
|
||||
note as we don't want all channel followers to be notified of this answer. """
|
||||
self.ensure_one()
|
||||
if parent_id:
|
||||
parent_message = self.env['mail.message'].sudo().browse(parent_id)
|
||||
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'):
|
||||
if kwargs.get('subtype_id'):
|
||||
kwargs['subtype_id'] = False
|
||||
subtype = 'mail.mt_note'
|
||||
return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
""" Channel contain various categories to manage its slides """
|
||||
|
||||
@@ -1208,6 +1208,7 @@ class IrModelData(models.Model):
|
||||
datas = self.search([('module', 'in', modules_to_remove)])
|
||||
wkf_todo = []
|
||||
to_unlink = tools.OrderedSet()
|
||||
undeletable = self.browse([])
|
||||
|
||||
for data in datas.sorted(key='id', reverse=True):
|
||||
model = data.model
|
||||
@@ -1232,6 +1233,7 @@ class IrModelData(models.Model):
|
||||
_logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
|
||||
|
||||
def unlink_if_refcount(to_unlink):
|
||||
undeletable = self.browse()
|
||||
for model, res_id in to_unlink:
|
||||
external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)])
|
||||
if external_ids - datas:
|
||||
@@ -1255,28 +1257,30 @@ class IrModelData(models.Model):
|
||||
self.env[model].browse(res_id).unlink()
|
||||
except Exception:
|
||||
_logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
|
||||
undeletable += external_ids
|
||||
self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
|
||||
else:
|
||||
self._cr.execute('RELEASE SAVEPOINT record_unlink_save')
|
||||
return undeletable
|
||||
|
||||
# Remove non-model records first, then model fields, and finish with models
|
||||
unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
|
||||
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
|
||||
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
|
||||
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
|
||||
|
||||
modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
|
||||
constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
|
||||
constraints._module_data_uninstall()
|
||||
|
||||
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
|
||||
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
|
||||
|
||||
relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
|
||||
relations._module_data_uninstall()
|
||||
|
||||
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
|
||||
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
|
||||
|
||||
self._cr.commit()
|
||||
|
||||
datas.unlink()
|
||||
(datas - undeletable).unlink()
|
||||
|
||||
@api.model
|
||||
def _process_end(self, modules):
|
||||
|
||||
@@ -229,10 +229,11 @@ actual arch.
|
||||
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
|
||||
view_wo_lang.arch = view.arch_base
|
||||
|
||||
@api.depends('write_date')
|
||||
def _compute_model_data_id(self):
|
||||
# get the last ir_model_data record corresponding to self
|
||||
# get the first ir_model_data record corresponding to self
|
||||
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
|
||||
for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
|
||||
for data in self.env['ir.model.data'].search_read(domain, ['res_id'], order='id desc'):
|
||||
view = self.browse(data['res_id'])
|
||||
view.model_data_id = data['id']
|
||||
|
||||
@@ -343,7 +344,15 @@ actual arch.
|
||||
values['name'] = "%s %s" % (values.get('model'), values['type'])
|
||||
|
||||
self.clear_caches()
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
|
||||
if 'install_mode_data' in self._context:
|
||||
# the view is created from a data file by installing a module; delay
|
||||
# the recomputation of field 'model_data_id' until its xmlid record
|
||||
# is created
|
||||
with self.env.norecompute():
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
else:
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
@@ -1168,4 +1177,4 @@ actual arch.
|
||||
try:
|
||||
self.browse(vid)._check_xml()
|
||||
except Exception as e:
|
||||
self.raise_view_error("Can't validate view: %s" % e.message, vid)
|
||||
self.raise_view_error("Can't validate view:\n%s" % (e.message or repr(e)), vid)
|
||||
|
||||
@@ -12,6 +12,14 @@ from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class ViewXMLID(common.TransactionCase):
|
||||
def test_model_data_id(self):
|
||||
""" Check whether views know their xmlid record. """
|
||||
view = self.env.ref('base.view_company_form')
|
||||
self.assertTrue(view)
|
||||
self.assertTrue(view.model_data_id)
|
||||
self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form')
|
||||
|
||||
class ViewCase(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(ViewCase, self).setUp()
|
||||
|
||||
@@ -23,7 +23,7 @@ class CreatorCase(common.TransactionCase):
|
||||
def export(self, value, fields=('value',), context=None):
|
||||
record = self.make(value)
|
||||
record.invalidate_cache()
|
||||
return record._BaseModel__export_rows([f.split('/') for f in fields])
|
||||
return record._export_rows([f.split('/') for f in fields])
|
||||
|
||||
|
||||
class test_boolean_field(CreatorCase):
|
||||
@@ -449,7 +449,7 @@ class test_o2m_multiple(CreatorCase):
|
||||
|
||||
def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
|
||||
record = self.make(value, **values)
|
||||
return record._BaseModel__export_rows([f.split('/') for f in fields])
|
||||
return record._export_rows([f.split('/') for f in fields])
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
from . import test_new_fields
|
||||
from . import test_onchange
|
||||
from . import test_attributes
|
||||
from . import test_one2many
|
||||
from . import test_ui
|
||||
from . import test_domain
|
||||
|
||||
@@ -176,7 +176,7 @@ class TestOnChange(common.TransactionCase):
|
||||
self.assertEqual(values, {
|
||||
'name': partner.name,
|
||||
'partner': partner.id,
|
||||
'lines': [(5,), (4, line.id)],
|
||||
'lines': [(6, 0, [line.id])],
|
||||
})
|
||||
|
||||
# modify 'partner'
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.tests.common import TransactionCase
|
||||
|
||||
|
||||
class One2manyCase(TransactionCase):
|
||||
def setUp(self):
|
||||
super(One2manyCase, self).setUp()
|
||||
self.Line = self.env["test_new_api.multi.line"]
|
||||
self.multi = self.env["test_new_api.multi"].create({
|
||||
"name": "What is up?"
|
||||
})
|
||||
|
||||
def operations(self):
|
||||
"""Run operations on o2m fields to check all works fine."""
|
||||
# Check the lines first
|
||||
self.assertItemsEqual(
|
||||
self.multi.lines.mapped('name'),
|
||||
map(str, range(10)))
|
||||
# Modify the first line and drop the last one
|
||||
self.multi.lines[0].name = "hello"
|
||||
self.multi.lines = self.multi.lines[:-1]
|
||||
self.assertEqual(len(self.multi.lines), 9)
|
||||
self.assertIn("hello", self.multi.lines.mapped('name'))
|
||||
# Invalidate the cache and check again; this crashes if the value
|
||||
# of self.multi.lines in cache contains new records
|
||||
self.multi.invalidate_cache()
|
||||
self.assertEqual(len(self.multi.lines), 9)
|
||||
self.assertIn("hello", self.multi.lines.mapped('name'))
|
||||
|
||||
def test_new_one_by_one(self):
|
||||
"""Check lines created with ``new()`` and appended one by one."""
|
||||
for name in range(10):
|
||||
self.multi.lines |= self.Line.new({"name": str(name)})
|
||||
self.operations()
|
||||
|
||||
def test_new_single(self):
|
||||
"""Check lines created with ``new()`` and added in one step."""
|
||||
self.multi.lines = self.Line.browse(
|
||||
[self.Line.new({"name": str(name)}).id for name in range(10)]
|
||||
)
|
||||
self.operations()
|
||||
|
||||
def test_create_one_by_one(self):
|
||||
"""Check lines created with ``create()`` and appended one by one."""
|
||||
for name in range(10):
|
||||
self.multi.lines |= self.Line.create({"name": str(name)})
|
||||
self.operations()
|
||||
|
||||
def test_create_single(self):
|
||||
"""Check lines created with ``create()`` and added in one step."""
|
||||
self.multi.lines = self.Line.browse(
|
||||
[self.Line.create({"name": str(name)}).id for name in range(10)]
|
||||
)
|
||||
self.operations()
|
||||
|
||||
def test_rpcstyle_one_by_one(self):
|
||||
"""Check lines created with RPC style and appended one by one."""
|
||||
for name in range(10):
|
||||
self.multi.lines = [(0, 0, {"name": str(name)})]
|
||||
self.operations()
|
||||
|
||||
def test_rpcstyle_single(self):
|
||||
"""Check lines created with RPC style and added in one step"""
|
||||
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
|
||||
self.operations()
|
||||
@@ -22,6 +22,7 @@ _logger = logging.getLogger(__name__)
|
||||
class TestPyLint(TransactionCase):
|
||||
|
||||
ENABLED_CODES = [
|
||||
'E0601', # using variable before assignment
|
||||
'W0123', # eval used
|
||||
'W0101', # unreachable code
|
||||
]
|
||||
|
||||
+11
-5
@@ -894,10 +894,12 @@ class Field(object):
|
||||
env.invalidate(spec)
|
||||
|
||||
else:
|
||||
# simply write to the database, and update cache
|
||||
# Write to database
|
||||
write_value = self.convert_to_write(self.convert_to_record(value, record), record)
|
||||
record.write({self.name: write_value})
|
||||
record._cache[self] = value
|
||||
# Update the cache unless value contains a new record
|
||||
if not (self.relational and not all(value)):
|
||||
record._cache[self] = value
|
||||
|
||||
############################################################################
|
||||
#
|
||||
@@ -1987,7 +1989,7 @@ class _RelationalMulti(_Relational):
|
||||
|
||||
def convert_to_write(self, value, record):
|
||||
# make result with new and existing records
|
||||
result = [(5,)]
|
||||
result = [(6, 0, [])]
|
||||
for record in value:
|
||||
if not record.id:
|
||||
values = {name: record[name] for name in record._cache}
|
||||
@@ -1998,7 +2000,7 @@ class _RelationalMulti(_Relational):
|
||||
values = record._convert_to_write(values)
|
||||
result.append((1, record.id, values))
|
||||
else:
|
||||
result.append((4, record.id))
|
||||
result[0][2].append(record.id)
|
||||
return result
|
||||
|
||||
def convert_to_onchange(self, value, record, fnames=()):
|
||||
@@ -2171,7 +2173,11 @@ class One2many(_RelationalMulti):
|
||||
query = "SELECT id FROM %s WHERE %s=%%s AND id <> ALL(%%s)" % (comodel._table, inverse)
|
||||
comodel._cr.execute(query, (record.id, act[2] or [0]))
|
||||
lines = comodel.browse([row[0] for row in comodel._cr.fetchall()])
|
||||
lines.write({inverse: False})
|
||||
inverse_field = comodel._fields[inverse]
|
||||
if inverse_field.ondelete == 'cascade':
|
||||
lines.unlink()
|
||||
else:
|
||||
lines.write({inverse: False})
|
||||
|
||||
|
||||
class Many2many(_RelationalMulti):
|
||||
|
||||
+7
-5
@@ -738,7 +738,7 @@ class BaseModel(object):
|
||||
return '__export__.' + name
|
||||
|
||||
@api.multi
|
||||
def __export_rows(self, fields):
|
||||
def _export_rows(self, fields):
|
||||
""" Export fields of the records in ``self``.
|
||||
|
||||
:param fields: list of lists of fields to traverse
|
||||
@@ -785,7 +785,7 @@ class BaseModel(object):
|
||||
|
||||
# recursively export the fields that follow name
|
||||
fields2 = [(p[1:] if p and p[0] == name else []) for p in fields]
|
||||
lines2 = value.__export_rows(fields2)
|
||||
lines2 = value._export_rows(fields2)
|
||||
if lines2:
|
||||
# merge first line with record's main line
|
||||
for j, val in enumerate(lines2[0]):
|
||||
@@ -804,6 +804,9 @@ class BaseModel(object):
|
||||
|
||||
return lines
|
||||
|
||||
# backward compatibility
|
||||
__export_rows = _export_rows
|
||||
|
||||
@api.multi
|
||||
def export_data(self, fields_to_export, raw_data=False):
|
||||
""" Export fields for selected objects
|
||||
@@ -817,7 +820,7 @@ class BaseModel(object):
|
||||
fields_to_export = map(fix_import_export_id_paths, fields_to_export)
|
||||
if raw_data:
|
||||
self = self.with_context(export_raw_data=True)
|
||||
return {'datas': self.__export_rows(fields_to_export)}
|
||||
return {'datas': self._export_rows(fields_to_export)}
|
||||
|
||||
@api.model
|
||||
def load(self, fields, data):
|
||||
@@ -3505,8 +3508,7 @@ class BaseModel(object):
|
||||
``(6, _, ids)``
|
||||
replaces all existing records in the set by the ``ids`` list,
|
||||
equivalent to using the command ``5`` followed by a command
|
||||
``4`` for each ``id`` in ``ids``. Can not be used on
|
||||
:class:`~odoo.fields.One2many`.
|
||||
``4`` for each ``id`` in ``ids``.
|
||||
|
||||
.. note:: Values marked as ``_`` in the list above are ignored and
|
||||
can be anything, generally ``0`` or ``False``.
|
||||
|
||||
@@ -363,7 +363,7 @@ class configmanager(object):
|
||||
rcfilepath = old_rcfilepath
|
||||
|
||||
self.rcfile = os.path.abspath(
|
||||
self.config_file or opt.config or os.environ.get('OPENERP_SERVER') or rcfilepath)
|
||||
self.config_file or opt.config or os.environ.get('ODOO_RC') or os.environ.get('OPENERP_SERVER') or rcfilepath)
|
||||
self.load()
|
||||
|
||||
# Verify that we want to log or not, if not the output will go to stdout
|
||||
|
||||
Reference in New Issue
Block a user