[MERGE] forward port branch 10.0 up to aaadb232b2
@@ -272,6 +272,11 @@ file_filter = addons/hr_expense/i18n/<lang>.po
|
||||
source_file = addons/hr_expense/i18n/hr_expense.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_expense_check]
|
||||
file_filter = addons/hr_expense_check/i18n/<lang>.po
|
||||
source_file = addons/hr_expense_check/i18n/hr_expense_check.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_gamification]
|
||||
file_filter = addons/hr_gamification/i18n/<lang>.po
|
||||
source_file = addons/hr_gamification/i18n/hr_gamification.pot
|
||||
|
||||
@@ -889,6 +889,11 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
|
||||
# Empty self can happen if the user tries to reconcile entries which are already reconciled.
|
||||
# The calling method might have filtered out reconciled lines.
|
||||
if not self:
|
||||
return True
|
||||
|
||||
#Perform all checks on lines
|
||||
company_ids = set()
|
||||
all_accounts = []
|
||||
|
||||
@@ -50,7 +50,9 @@ class ReportTrialBalance(models.AbstractModel):
|
||||
res['balance'] = account_result[account.id].get('balance')
|
||||
if display_account == 'all':
|
||||
account_res.append(res)
|
||||
if display_account in ['movement', 'not_zero'] and not currency.is_zero(res['balance']):
|
||||
if display_account == 'not_zero' and not currency.is_zero(res['balance']):
|
||||
account_res.append(res)
|
||||
if display_account == 'movement' and (not currency.is_zero(res['debit']) or not currency.is_zero(res['credit'])):
|
||||
account_res.append(res)
|
||||
return account_res
|
||||
|
||||
|
||||
@@ -702,7 +702,7 @@
|
||||
<field name="name">Bank Statement Lines</field>
|
||||
<field name="res_model">account.bank.statement.line</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="view_bank_statement_line_search"/>
|
||||
|
||||
@@ -102,7 +102,7 @@ var BarcodeParser = Class.extend({
|
||||
ean = ean.substr(0,13);
|
||||
|
||||
for(var n = 0, count = (13 - ean.length); n < count; n++){
|
||||
ean = ean + '0';
|
||||
ean = '0' + ean;
|
||||
}
|
||||
return ean.substr(0,12) + this.ean_checksum(ean);
|
||||
},
|
||||
|
||||
@@ -276,6 +276,11 @@ class Lead(models.Model):
|
||||
|
||||
if vals.get('user_id') and 'date_open' not in vals:
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
|
||||
if context.get('default_partner_id') and not vals.get('email_from'):
|
||||
partner = self.env['res.partner'].browse(context['default_partner_id'])
|
||||
vals['email_from'] = partner.email
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
return super(Lead, self.with_context(context, mail_create_nolog=True)).create(vals)
|
||||
|
||||
|
||||
@@ -27,13 +27,8 @@ class ReportEventRegistration(models.Model):
|
||||
name_registration = fields.Char('Participant / Contact Name', readonly=True)
|
||||
company_id = fields.Many2one('res.company', 'Company', readonly=True)
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
"""Initialize the sql view for the event registration """
|
||||
tools.drop_view_if_exists(self.env.cr, 'report_event_registration')
|
||||
|
||||
# TOFIX this request won't select events that have no registration
|
||||
self.env.cr.execute(""" CREATE VIEW report_event_registration AS (
|
||||
def _select(self):
|
||||
return """
|
||||
SELECT
|
||||
e.id::varchar || '/' || coalesce(r.id::varchar,'') AS id,
|
||||
e.id AS event_id,
|
||||
@@ -51,10 +46,17 @@ class ReportEventRegistration(models.Model):
|
||||
e.seats_max AS seats_max,
|
||||
e.state AS event_state,
|
||||
r.state AS registration_state
|
||||
"""
|
||||
|
||||
def _from(self):
|
||||
return """
|
||||
FROM
|
||||
event_event e
|
||||
LEFT JOIN event_registration r ON (e.id=r.event_id)
|
||||
"""
|
||||
|
||||
def _group_by(self):
|
||||
return """
|
||||
GROUP BY
|
||||
event_id,
|
||||
r.id,
|
||||
@@ -67,5 +69,13 @@ class ReportEventRegistration(models.Model):
|
||||
e.company_id,
|
||||
e.seats_max,
|
||||
name_registration
|
||||
"""
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute(
|
||||
"CREATE or REPLACE VIEW %s as (%s %s %s)" % (
|
||||
self._table, self._select(), self._from(), self._group_by(),
|
||||
)
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</record>
|
||||
<record id="l10nch_chart_template" model="account.chart.template">
|
||||
<field name="name">Plan comptable 2015</field>
|
||||
<field name="code_digits">1</field>
|
||||
<field name="code_digits">4</field>
|
||||
<field name="bank_account_code_prefix">102</field>
|
||||
<field name="cash_account_code_prefix">100</field>
|
||||
<field name="currency_id" ref="base.CHF"/>
|
||||
|
||||
@@ -221,14 +221,22 @@ function make_message (data) {
|
||||
// can not be done in preprocess, since it alter the original value
|
||||
if (msg.tracking_value_ids && msg.tracking_value_ids.length) {
|
||||
_.each(msg.tracking_value_ids, function(f) {
|
||||
if (_.contains(['date', 'datetime'], f.field_type)) {
|
||||
var format = (f.field_type === 'date') ? 'LL' : 'LLL';
|
||||
if (f.field_type === 'datetime') {
|
||||
var format = 'LLL';
|
||||
if (f.old_value) {
|
||||
f.old_value = moment.utc(f.old_value).local().format(format);
|
||||
}
|
||||
if (f.new_value) {
|
||||
f.new_value = moment.utc(f.new_value).local().format(format);
|
||||
}
|
||||
} else if (f.field_type === 'date') {
|
||||
var format = 'LL';
|
||||
if (f.old_value) {
|
||||
f.old_value = moment(f.old_value).local().format(format);
|
||||
}
|
||||
if (f.new_value) {
|
||||
f.new_value = moment(f.new_value).local().format(format);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ class MassMailing(models.Model):
|
||||
create_date = fields.Datetime(string='Creation Date')
|
||||
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
|
||||
schedule_date = fields.Datetime(string='Schedule in the Future')
|
||||
body_html = fields.Html(string='Body', translate=html_translate, sanitize_attributes=False)
|
||||
body_html = fields.Html(string='Body', sanitize_attributes=False)
|
||||
attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
|
||||
'mass_mailing_id', 'attachment_id', string='Attachments')
|
||||
keep_archives = fields.Boolean(string='Keep Archives')
|
||||
|
||||
@@ -117,26 +117,48 @@ class MrpBom(models.Model):
|
||||
Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
|
||||
and converted into its UoM
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
graph = defaultdict(list)
|
||||
V = set()
|
||||
|
||||
def check_cycle(v, visited, recStack, graph):
|
||||
visited[v] = True
|
||||
recStack[v] = True
|
||||
for neighbour in graph[v]:
|
||||
if visited[neighbour] == False:
|
||||
if check_cycle(neighbour, visited, recStack, graph) == True:
|
||||
return True
|
||||
elif recStack[neighbour] == True:
|
||||
return True
|
||||
recStack[v] = False
|
||||
return False
|
||||
|
||||
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
|
||||
lines_done = []
|
||||
templates_done = product.product_tmpl_id
|
||||
V |= set([product.product_tmpl_id.id])
|
||||
|
||||
bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids]
|
||||
for bom_line in self.bom_line_ids:
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
graph[product.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
while bom_lines:
|
||||
current_line, current_product, current_qty, parent_line = bom_lines[0]
|
||||
bom_lines = bom_lines[1:]
|
||||
|
||||
if current_line._skip_bom_line(current_product):
|
||||
continue
|
||||
if current_line.product_id.product_tmpl_id in templates_done:
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
|
||||
line_quantity = current_qty * current_line.product_qty
|
||||
bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id)
|
||||
if bom.type == 'phantom':
|
||||
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity / bom.product_qty, bom.product_uom_id)
|
||||
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
|
||||
templates_done |= current_line.product_id.product_tmpl_id
|
||||
for bom_line in bom.bom_line_ids:
|
||||
graph[current_line.product_id.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph):
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
|
||||
else:
|
||||
# We round up here because the user expects that if he has to consume a little more, the whole UOM unit
|
||||
|
||||
@@ -269,7 +269,10 @@ class MrpProduction(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if not values.get('name', False) or values['name'] == _('New'):
|
||||
values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
|
||||
if values.get('picking_type_id'):
|
||||
values['name'] = self.env['stock.picking.type'].browse(values['picking_type_id']).sequence_id.next_by_id()
|
||||
else:
|
||||
values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
|
||||
if not values.get('procurement_group_id'):
|
||||
values['procurement_group_id'] = self.env["procurement.group"].create({'name': values['name']}).id
|
||||
production = super(MrpProduction, self).create(values)
|
||||
|
||||
@@ -391,9 +391,12 @@ class MrpWorkorder(models.Model):
|
||||
domain = [('workorder_id', 'in', self.ids), ('date_end', '=', False)]
|
||||
if not doall:
|
||||
domain.append(('user_id', '=', self.env.user.id))
|
||||
not_productive_timelines = timeline_obj.browse()
|
||||
for timeline in timeline_obj.search(domain, limit=None if doall else 1):
|
||||
wo = timeline.workorder_id
|
||||
if timeline.loss_type != 'productive':
|
||||
if wo.duration_expected <= wo.duration:
|
||||
if timeline.loss_type == 'productive':
|
||||
not_productive_timelines += timeline
|
||||
timeline.write({'date_end': fields.Datetime.now()})
|
||||
else:
|
||||
maxdate = fields.Datetime.from_string(timeline.date_start) + relativedelta(minutes=wo.duration_expected - wo.duration)
|
||||
@@ -402,10 +405,12 @@ class MrpWorkorder(models.Model):
|
||||
timeline.write({'date_end': enddate})
|
||||
else:
|
||||
timeline.write({'date_end': maxdate})
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
timeline.copy({'date_start': maxdate, 'date_end': enddate, 'loss_id': loss_id.id})
|
||||
not_productive_timelines += timeline.copy({'date_start': maxdate, 'date_end': enddate})
|
||||
if not_productive_timelines:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
not_productive_timelines.write({'loss_id': loss_id.id})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -131,7 +131,30 @@ class TestBoM(TestMrpCommon):
|
||||
self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
|
||||
self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
|
||||
|
||||
# check recursion
|
||||
test_bom_2_l3.write({'attribute_value_ids': [(6, 0, [self.prod_attr1_v1.id])]})
|
||||
#check recursion
|
||||
test_bom_3 = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_9.id,
|
||||
'product_tmpl_id': self.product_9.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_9.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal'
|
||||
})
|
||||
test_bom_4 = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_10.id,
|
||||
'product_tmpl_id': self.product_10.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_10.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'
|
||||
})
|
||||
test_bom_3_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_3.id,
|
||||
'product_id': self.product_10.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
test_bom_4_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_4.id,
|
||||
'product_id': self.product_9.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
test_bom_2.explode(self.product_7_1, 4)
|
||||
test_bom_3.explode(self.product_9, 1)
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
<tree editable="bottom" create="0" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="quantity" readonly="1"/>
|
||||
<field name="lot_id" context="{'default_product_id': product_id}"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
|
||||
<field name="quantity_done"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
<field name="done_wo" invisible="1"/>
|
||||
|
||||
@@ -5,7 +5,9 @@ from StringIO import StringIO
|
||||
import xml.etree.ElementTree as ET
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo import _
|
||||
|
||||
XMLNS = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
@@ -848,7 +848,8 @@ class PosOrderLine(models.Model):
|
||||
fiscal_position_id = line.order_id.fiscal_position_id
|
||||
if fiscal_position_id:
|
||||
taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id)
|
||||
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
price = self.env['account.tax']._fix_tax_included_price(
|
||||
line.price_unit * (1 - (line.discount or 0.0) / 100.0), line.product_id.taxes_id, taxes)
|
||||
line.price_subtotal = line.price_subtotal_incl = price * line.qty
|
||||
if taxes:
|
||||
taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
|
||||
|
||||
@@ -2347,7 +2347,6 @@ td {
|
||||
}
|
||||
.pos .popup-password .popup-input {
|
||||
width: 70%;
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
.pos .popup .body ul,
|
||||
|
||||
@@ -307,6 +307,11 @@ var PasswordPopupWidget = NumberPopupWidget.extend({
|
||||
this._super();
|
||||
this.$('.popup').addClass('popup-password');
|
||||
},
|
||||
click_numpad: function(event){
|
||||
this._super.apply(this, arguments);
|
||||
var $value = this.$('.value');
|
||||
$value.text($value.text().replace(/./g, '•'));
|
||||
},
|
||||
});
|
||||
gui.define_popup({name:'password', widget: PasswordPopupWidget});
|
||||
|
||||
|
||||
@@ -138,22 +138,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': 2.0,
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0002",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': 3.0,
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (2*450 + 3*300 = 1800)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos1.id],
|
||||
"active_id": self.pos_order_pos1.id
|
||||
}
|
||||
self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': 1800
|
||||
'amount': 1845
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
@@ -200,22 +201,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': (-2.0),
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0004",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': (-3.0),
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + -3*300 = -1800)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos2.id],
|
||||
"active_id": self.pos_order_pos2.id
|
||||
}
|
||||
self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': (-1800)
|
||||
'amount': (-1845)
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
@@ -262,22 +264,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': (-2.0),
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0006",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': 3.0,
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + 3*300 = 0)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos3.id],
|
||||
"active_id": self.pos_order_pos3.id
|
||||
}
|
||||
self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': 0
|
||||
'amount': 45,
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
|
||||
@@ -90,3 +90,13 @@ class TestProductCommon(common.SavepointCase):
|
||||
'name': 'House',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
cls.product_9 = Product.create({
|
||||
'name': 'Paper',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
cls.product_10 = Product.create({
|
||||
'name': 'Stone',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
@@ -141,7 +141,7 @@ class PurchaseOrder(models.Model):
|
||||
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
|
||||
|
||||
# There is no inverse function on purpose since the date may be different on each line
|
||||
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True, oldname='minimum_planned_date')
|
||||
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True)
|
||||
|
||||
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
|
||||
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all')
|
||||
@@ -668,7 +668,7 @@ class PurchaseOrderLine(models.Model):
|
||||
}
|
||||
# Fullfill all related procurements with this po line
|
||||
diff_quantity = self.product_qty - qty
|
||||
for procurement in self.procurement_ids:
|
||||
for procurement in self.procurement_ids.filtered(lambda p: p.state != 'cancel'):
|
||||
# If the procurement has some moves already, we should deduct their quantity
|
||||
sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel')
|
||||
existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom)
|
||||
|
||||
@@ -33,7 +33,14 @@ class PurchaseRequisition(models.Model):
|
||||
_order = "id desc"
|
||||
|
||||
def _get_picking_in(self):
|
||||
return self.env.ref('stock.picking_type_in')
|
||||
pick_in = self.env.ref('stock.picking_type_in')
|
||||
if not pick_in:
|
||||
company = self.env['res.company']._company_default_get('purchase.requisition')
|
||||
pick_in = self.env['stock.picking.type'].search(
|
||||
[('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')],
|
||||
limit=1,
|
||||
)
|
||||
return pick_in
|
||||
|
||||
def _get_type_id(self):
|
||||
return self.env['purchase.requisition.type'].search([], limit=1)
|
||||
|
||||
@@ -35,9 +35,6 @@ class Rating(http.Controller):
|
||||
return request.not_found()
|
||||
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
|
||||
# redirect to the form view if logged person
|
||||
if request.session.uid:
|
||||
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
|
||||
lang = rating.partner_id.lang or 'en_US'
|
||||
return request.env['ir.ui.view'].with_context(lang=lang).render_template('rating.rating_external_page_view', {
|
||||
'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
|
||||
@@ -297,7 +297,7 @@ class Report(models.Model):
|
||||
active_ids = docids
|
||||
context = dict(self.env.context, active_ids=active_ids)
|
||||
|
||||
report = self.env['ir.actions.report.xml'].with_context(context).search([('report_name', '=', report_name)])
|
||||
report = self.env['ir.actions.report.xml'].with_context(context).search([('report_name', '=', report_name)], limit=1)
|
||||
if not report:
|
||||
raise UserError(_("Bad Report Reference") + _("This report is not loaded into the database: %s.") % report_name)
|
||||
|
||||
|
||||
@@ -304,6 +304,11 @@ class ResourceCalendar(models.Model):
|
||||
elif start_dt is None:
|
||||
start_dt = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
# FORWARD-PORT UP TO SAAS-14
|
||||
# Add a strict limit when searching for intervals
|
||||
force_start_dt = self.env.context.get('force_start_dt')
|
||||
if force_start_dt and force_start_dt < start_dt:
|
||||
work_limits.append((force_start_dt.replace(hour=0, minute=0, second=0, microsecond=0), force_start_dt))
|
||||
work_limits.append((start_dt.replace(hour=0, minute=0, second=0, microsecond=0), start_dt))
|
||||
if end_dt is None:
|
||||
end_dt = start_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
@@ -335,7 +340,15 @@ class ResourceCalendar(models.Model):
|
||||
dt_t.replace(tzinfo=tz_info).astimezone(pytz.UTC).replace(tzinfo=None),
|
||||
calendar_working_day.id
|
||||
)
|
||||
working_intervals += self.interval_remove_leaves(working_interval, work_limits)
|
||||
|
||||
# FORWARD-PORT UP TO SAAS-14
|
||||
# Add a strict limit when searching for intervals (yeah, once again!)
|
||||
if self.env.context.get('force_start_dt'):
|
||||
for wi in self.interval_remove_leaves(working_interval, work_limits):
|
||||
if wi[0] >= self.env.context['force_start_dt']:
|
||||
working_intervals += [wi]
|
||||
else:
|
||||
working_intervals += self.interval_remove_leaves(working_interval, work_limits)
|
||||
|
||||
# find leave intervals
|
||||
if leaves is None and compute_leaves:
|
||||
@@ -423,6 +436,12 @@ class ResourceCalendar(models.Model):
|
||||
"""
|
||||
if day_dt is None:
|
||||
day_dt = datetime.datetime.now()
|
||||
elif day_dt is not None and hours > 0:
|
||||
# FORWARD-PORT UP TO SAAS-14
|
||||
# FIXME: think about a better fix... However, solved from saas-15 thanks to refactoring
|
||||
# Make sure the to keep the original start date when searching for intervals, since a
|
||||
# difference in day may occur depending on the TZ.
|
||||
self = self.with_context(force_start_dt=day_dt)
|
||||
backwards = (hours < 0)
|
||||
hours = abs(hours)
|
||||
intervals = []
|
||||
|
||||
@@ -477,6 +477,40 @@ class TestResource(TestResourceCommon):
|
||||
resource_id = self.env.ref('resource.resource_analyst').copy()
|
||||
self.assertTrue(resource_id, 'Unable to Duplicate Resource')
|
||||
|
||||
# FORWARD-PORT UP TO SAAS-14
|
||||
# Test already in Saas-15
|
||||
def test_80_resource_schedule_tz(self):
|
||||
# Call schedule_hours for a user in Autralia, Sydney (GMT+10)
|
||||
# Two cases:
|
||||
# - start at 2013-02-15 08:00:00 => 2013-02-14 21:00:00 in UTC
|
||||
# - start at 2013-02-15 11:00:00 => 2013-02-15 00:00:00 in UTC
|
||||
|
||||
tz_context = dict(tz='Australia/Sydney')
|
||||
self.env.user.with_context(tz_context).write({'tz': 'Australia/Sydney'})
|
||||
calendar = self.calendar.with_context(tz_context)
|
||||
|
||||
self.env['resource.calendar.attendance'].create({
|
||||
'name': 'Day3 - 1',
|
||||
'dayofweek': '3',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'calendar_id': calendar.id,
|
||||
})
|
||||
self.env['resource.calendar.attendance'].create({
|
||||
'name': 'Day3 - 2',
|
||||
'dayofweek': '3',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'calendar_id': calendar.id,
|
||||
})
|
||||
|
||||
hours = 1.0/60.0
|
||||
start_dt = Datetime.from_string('2013-02-14 21:00:00')
|
||||
res = calendar.schedule_hours(hours, start_dt)
|
||||
self.assertEqual([(start_dt, start_dt.replace(minute=1))], res, 'resource_calendar: wrong schedule_hours computation')
|
||||
start_dt = Datetime.from_string('2013-02-15 00:00:00')
|
||||
res = calendar.schedule_hours(hours, start_dt)
|
||||
self.assertEqual([(start_dt, start_dt.replace(minute=1))], res, 'resource_calendar: wrong schedule_hours computation')
|
||||
|
||||
WAR_START = date(1932, 11, 2)
|
||||
WAR_END = date(1932, 12, 10)
|
||||
|
||||
@@ -113,7 +113,7 @@ class SaleOrder(models.Model):
|
||||
('cancel', 'Cancelled'),
|
||||
], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', default='draft')
|
||||
date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now)
|
||||
validity_date = fields.Date(string='Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
validity_date = fields.Date(string='Expiration Date', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
help="Manually set the expiration date of your quotation (offer), or it will set the date automatically based on the template if online quotation is installed.")
|
||||
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
|
||||
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sales order is confirmed.", oldname="date_confirm")
|
||||
|
||||
@@ -55,7 +55,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
return super(SaleOrderLine, self).create(vals)
|
||||
|
||||
@api.depends('product_id', 'purchase_price', 'product_uom_qty', 'price_unit')
|
||||
@api.depends('product_id', 'purchase_price', 'product_uom_qty', 'price_unit', 'price_subtotal')
|
||||
def _product_margin(self):
|
||||
for line in self:
|
||||
currency = line.order_id.pricelist_id.currency_id
|
||||
|
||||
@@ -110,10 +110,11 @@ class PackOperation(models.Model):
|
||||
qty -= record.qty
|
||||
self.remaining_qty = float_round(qty, precision_rounding=self.product_id.uom_id.rounding)
|
||||
|
||||
@api.one
|
||||
@api.multi
|
||||
def _compute_location_description(self):
|
||||
self.from_loc = '%s%s' % (self.location_id.name, self.product_id and self.package_id.name or '')
|
||||
self.to_loc = '%s%s' % (self.location_dest_id.name, self.result_package_id.name or '')
|
||||
for operation, operation_sudo in zip(self, self.sudo()):
|
||||
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
|
||||
operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name, operation_sudo.result_package_id.name or '')
|
||||
|
||||
@api.one
|
||||
def _compute_lots_visible(self):
|
||||
|
||||
@@ -779,6 +779,8 @@ class Picking(models.Model):
|
||||
@api.multi
|
||||
def do_new_transfer(self):
|
||||
for pick in self:
|
||||
if pick.state == 'done':
|
||||
raise UserError(_('The pick is already validated'))
|
||||
pack_operations_delete = self.env['stock.pack.operation']
|
||||
if not pick.move_lines and not pick.pack_operation_ids:
|
||||
raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
|
||||
|
||||
@@ -1576,7 +1576,6 @@ class TestStockFlow(TestStockCommon):
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
|
||||
# validate this delivery order, it should be in the waiting state
|
||||
picking_out.action_assign()
|
||||
self.assertEquals(picking_out.state, "confirmed")
|
||||
|
||||
@@ -19,11 +19,10 @@ class StockBackorderConfirmation(models.TransientModel):
|
||||
|
||||
@api.one
|
||||
def _process(self, cancel_backorder=False):
|
||||
for pack in self.pick_id.pack_operation_ids:
|
||||
if pack.qty_done > 0:
|
||||
pack.product_qty = pack.qty_done
|
||||
else:
|
||||
pack.unlink()
|
||||
operations_to_delete = self.pick_id.pack_operation_ids.filtered(lambda o: o.qty_done <= 0)
|
||||
for pack in self.pick_id.pack_operation_ids - operations_to_delete:
|
||||
pack.product_qty = pack.qty_done
|
||||
operations_to_delete.unlink()
|
||||
self.pick_id.do_transfer()
|
||||
if cancel_backorder:
|
||||
backorder_pick = self.env['stock.picking'].search([('backorder_id', '=', self.pick_id.id)])
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
from lxml import etree, html
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo import api, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +33,10 @@ class IrUiView(models.Model):
|
||||
def extract_embedded_fields(self, arch):
|
||||
return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
|
||||
|
||||
@api.model
|
||||
def get_default_lang_code(self):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def save_embedded_field(self, el):
|
||||
Model = self.env[el.get('data-oe-model')]
|
||||
@@ -44,7 +48,10 @@ class IrUiView(models.Model):
|
||||
|
||||
if value is not None:
|
||||
# TODO: batch writes?
|
||||
Model.browse(int(el.get('data-oe-id'))).write({field: value})
|
||||
if self.get_default_lang_code() and field != 'arch' and not isinstance(Model._fields[field], fields.Html):
|
||||
Model.browse(int(el.get('data-oe-id'))).with_context(lang=self.get_default_lang_code()).write({field: value})
|
||||
else:
|
||||
Model.browse(int(el.get('data-oe-id'))).write({field: value})
|
||||
|
||||
def _pretty_arch(self, arch):
|
||||
# remove_blank_string does not seem to work on HTMLParser, and
|
||||
|
||||
@@ -183,6 +183,11 @@ var FieldTextHtml = widget.extend({
|
||||
template: 'web_editor.FieldTextHtml',
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
|
||||
if (this.field.translate === false) {
|
||||
this.languages = [];
|
||||
return $.when();
|
||||
}
|
||||
return new Model('res.lang').call("search_read", [[['code', '!=', 'en_US']], ["name", "code"]]).then(function (res) {
|
||||
self.languages = res;
|
||||
});
|
||||
|
||||
@@ -146,3 +146,12 @@ class View(models.Model):
|
||||
if full:
|
||||
return views
|
||||
return views.filtered(lambda v: v.customize_show)
|
||||
|
||||
@api.model
|
||||
def get_default_lang_code(self):
|
||||
website_id = self.env.context.get('website_id')
|
||||
if website_id:
|
||||
lang_code = self.env['website'].browse(website_id).default_lang_code
|
||||
return lang_code
|
||||
else:
|
||||
return super(View, self).get_default_lang_code()
|
||||
|
||||
@@ -7,6 +7,12 @@ class TestUi(odoo.tests.HttpCase):
|
||||
at_install = False
|
||||
|
||||
def test_admin(self):
|
||||
# Seen that:
|
||||
# - this test relies on demo data that are entirely in USD (pricelists)
|
||||
# - that main demo company is gelocated in US
|
||||
# - that this test awaits for hardcoded USDs amount
|
||||
# we have to force company currency as USDs only for this test
|
||||
self.env.ref('base.main_company').write({'currency_id': self.env.ref('base.USD').id})
|
||||
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('event_buy_tickets')", "odoo.__DEBUG__.services['web_tour.tour'].tours.event_buy_tickets.ready", login="admin")
|
||||
|
||||
def test_demo(self):
|
||||
|
||||
@@ -172,7 +172,7 @@ class website_account(website_account):
|
||||
domain = [
|
||||
('type', 'in', ['out_invoice', 'out_refund']),
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', 'in', ['open', 'paid', 'cancelled'])
|
||||
('state', 'in', ['open', 'paid', 'cancel'])
|
||||
]
|
||||
|
||||
searchbar_sortings = {
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
t-att-style="line.discount and 'text-decoration: line-through' or None"
|
||||
t-att-class="(line.discount and 'text-danger' or '') + ' text-right'"/>
|
||||
<!-- TODO: apply monetary widget formating -->
|
||||
<div t-if="line.discount">
|
||||
<t t-esc="'%.2f' % ((1-line.discount / 100.0) * line.price_unit)"/>
|
||||
<t t-esc="((1-line.discount / 100.0) * line.price_unit)"
|
||||
t-options="{'widget': 'monetary', 'display_currency': quotation.pricelist_id.currency_id}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right" groups="sale.group_show_price_subtotal">
|
||||
|
||||
|
Before Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 246 KiB |
@@ -1,9 +0,0 @@
|
||||
==================
|
||||
Business Mementoes
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
|
||||
Accounting Memento (US GAAP) <https://odoo.com/documentation/functional/accounting.html>
|
||||
Double-Entry Inventory <https://odoo.com/documentation/functional/double-entry.html>
|
||||
Inventory Valuations <https://odoo.com/documentation/functional/valuation.html>
|
||||
@@ -0,0 +1,11 @@
|
||||
Belgium, 22/06/2017
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Ward Marissen ward.marissen@gmail.com https://github.com/wardm95
|
||||
@@ -94,11 +94,6 @@ pygments_style = 'odoo'
|
||||
html_theme = 'odoo_ext'
|
||||
|
||||
odoo_cover_default = 'banners/installing_odoo.jpg'
|
||||
odoo_cover_external = {
|
||||
'https://odoo.com/documentation/functional/accounting.html' : 'banners/m_accounting.jpg',
|
||||
'https://odoo.com/documentation/functional/double-entry.html' : 'banners/m_1.jpg',
|
||||
'https://odoo.com/documentation/functional/valuation.html' : 'banners/m_2.jpg',
|
||||
}
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
||||
@@ -17,7 +17,7 @@ Index: addons/openacademy/views/openacademy.xml
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:07.879004780 +0200
|
||||
+++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:07.871004780 +0200
|
||||
@@ -238,5 +238,26 @@
|
||||
@@ -237,5 +237,26 @@
|
||||
<menuitem id="session_menu" name="Sessions"
|
||||
parent="openacademy_menu"
|
||||
action="session_list_action"/>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
.. _reference/cmdline:
|
||||
|
||||
===============================
|
||||
================================
|
||||
Command-line interface: odoo-bin
|
||||
===============================
|
||||
================================
|
||||
|
||||
.. _reference/cmdline/server:
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -10,11 +10,17 @@ Mobile JavaScript
|
||||
Introduction
|
||||
============
|
||||
|
||||
In Odoo 10.0 we released a mobile app, allows you to access all **Odoo apps** (even your customized modules).
|
||||
In Odoo 10.0 we released a mobile app, allows you to access all **Odoo apps**
|
||||
(even your customized modules).
|
||||
|
||||
The application is a combination of **Odoo Web** and **Native Mobile components**, in other words it is a Odoo Web instance loaded inside native WebView container of mobile.
|
||||
The application is a combination of **Odoo Web** and **Native Mobile
|
||||
components**, in other words it is a Odoo Web instance loaded inside native
|
||||
WebView container of mobile.
|
||||
|
||||
This page documents how you can access mobile native components like Camera, Vibration, Notification, Toast through Odoo Web (via JavaScript). For this, you do not need to be a mobile developer, if you know Odoo JavaScript API you can access all available mobile features.
|
||||
This page documents how you can access mobile native components like Camera,
|
||||
Vibration, Notification, Toast through Odoo Web (via JavaScript). For this, you
|
||||
do not need to be a mobile developer, if you know Odoo JavaScript API you can
|
||||
access all available mobile features.
|
||||
|
||||
.. warning:: This features works with **Odoo Enterprise 10.0+** only
|
||||
|
||||
@@ -24,34 +30,41 @@ How does it work?
|
||||
Internal working of Mobile application:
|
||||
|
||||
.. image:: images/mobile_working.jpg
|
||||
:align: left
|
||||
|
||||
Of course, it is a web page that loads on a Mobile Native Web container. But it is integrated in such a way that you can access native resources from your web JavaScript.
|
||||
Of course, it is a web page that loads on a Mobile Native Web container. But it
|
||||
is integrated in such a way that you can access native resources from your web
|
||||
JavaScript.
|
||||
|
||||
WebPages (Odoo Web) is on the top of each layer, where second layer is a Bridge between Odoo Web (JS) and Native component of mobile.
|
||||
WebPages (Odoo Web) is on the top of each layer, where second layer is a Bridge
|
||||
between Odoo Web (JS) and Native component of mobile.
|
||||
|
||||
When any call from JavaScript triggered it passes through Bridge and Bridge pass it to native invoker to perform that action.
|
||||
When any call from JavaScript triggered it passes through Bridge and Bridge
|
||||
pass it to native invoker to perform that action.
|
||||
|
||||
When the native component has done their work, it again is passed to Bridge and you get the output in JavaScript.
|
||||
When the native component has done their work, it again is passed to Bridge and
|
||||
you get the output in JavaScript.
|
||||
|
||||
Process time taken by the Native component depends on what you are requesting from Native resources. Like Camera or GPS Location.
|
||||
Process time taken by the Native component depends on what you are requesting
|
||||
from Native resources. Like Camera or GPS Location.
|
||||
|
||||
How to use it?
|
||||
==============
|
||||
|
||||
As Odoo Web Framework, Mobile api can be used anywhere by getting object of **web_mobile.rpc**
|
||||
As Odoo Web Framework, Mobile api can be used anywhere by getting object of
|
||||
**web_mobile.rpc**
|
||||
|
||||
.. image:: images/odoo_mobile_api.png
|
||||
:align: left
|
||||
|
||||
Mobile rpc object provide list of methods available (Works only with Mobile App).
|
||||
Mobile rpc object provide list of methods available (Works only with Mobile
|
||||
App).
|
||||
|
||||
Just check for method availability and execute it.
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. note:: Each of the methods returns JQuery Deffered object with returning data JSON dictionary
|
||||
.. note:: Each of the methods returns JQuery Deffered object with returning
|
||||
data JSON dictionary
|
||||
|
||||
Show Toast in device
|
||||
.....................
|
||||
@@ -60,7 +73,9 @@ Show Toast in device
|
||||
|
||||
:param object args: **message** text to display
|
||||
|
||||
A toast provides simple feedback about an operation in a small popup. It only fills the amount of space required for the message and the current activity remains visible and interactive
|
||||
A toast provides simple feedback about an operation in a small popup. It only
|
||||
fills the amount of space required for the message and the current activity
|
||||
remains visible and interactive
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
@@ -75,7 +90,8 @@ Vibrating device
|
||||
|
||||
.. js:function:: vibrate
|
||||
|
||||
:param object args: Vibrates constantly for the specified period of time (in milliseconds).
|
||||
:param object args: Vibrates constantly for the specified period of time
|
||||
(in milliseconds).
|
||||
|
||||
Vibrate mobile device with given duration.
|
||||
|
||||
@@ -91,7 +107,10 @@ Show snackbar with action
|
||||
:param object args: (*required*) **Message** to show in snackbar and action **button label** in Snackbar (optional)
|
||||
:returns: ``True`` if User click on Action button, ``False`` if SnackBar auto dismissed after some time
|
||||
|
||||
Snackbars provide lightweight feedback about an operation. They show a brief message at the bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other elements on screen and only one can be displayed at a time.
|
||||
Snackbars provide lightweight feedback about an operation. They show a brief
|
||||
message at the bottom of the screen on mobile and lower left on larger devices.
|
||||
Snackbars appear above all other elements on screen and only one can be
|
||||
displayed at a time.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
@@ -112,7 +131,12 @@ Showing notification
|
||||
|
||||
:param object args: **title** (first row) of the notification, **message** (second row) of the notification, in a standard notification.
|
||||
|
||||
A notification is a message you can display to the user outside of your application's normal UI. When you tell the system to issue a notification, it first appears as an icon in the notification area. To see the details of the notification, the user opens the notification drawer. Both the notification area and the notification drawer are system-controlled areas that the user can view at any time.
|
||||
A notification is a message you can display to the user outside of your
|
||||
application's normal UI. When you tell the system to issue a notification, it
|
||||
first appears as an icon in the notification area. To see the details of the
|
||||
notification, the user opens the notification drawer. Both the notification
|
||||
area and the notification drawer are system-controlled areas that the user can
|
||||
view at any time.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
|
||||
@@ -910,13 +910,13 @@ and using them:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py
|
||||
:language: python
|
||||
:lines: 8,12,9,19
|
||||
:lines: 10,11,14,19
|
||||
|
||||
will yield:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py
|
||||
:language: text
|
||||
:lines: 15,22
|
||||
:lines: 16,21
|
||||
|
||||
the second model has inherited from the first model's ``check`` method and its
|
||||
``name`` field, but overridden the ``call`` method, as when using standard
|
||||
@@ -964,19 +964,19 @@ model:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py
|
||||
:language: python
|
||||
:lines: 9-12,21,26
|
||||
:lines: 11-14,23,28
|
||||
|
||||
will result in:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py
|
||||
:language: text
|
||||
:lines: 23,28
|
||||
:lines: 25,30
|
||||
|
||||
and it's possible to write directly on the delegated field:
|
||||
|
||||
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py
|
||||
:language: python
|
||||
:lines: 47
|
||||
:lines: 45
|
||||
|
||||
.. warning:: when using delegation inheritance, methods are *not* inherited,
|
||||
only fields
|
||||
|
||||
@@ -415,14 +415,16 @@ At a deeper level than the previous helper is the ``render`` method on
|
||||
|
||||
.. _reference/qweb/javascript:
|
||||
|
||||
API
|
||||
---
|
||||
.. todo:: the members below are no longer relevant, section to rewrite
|
||||
|
||||
It is also possible to use the ``ir.qweb`` model directly (and extend it, and
|
||||
inherit from it):
|
||||
.. API
|
||||
.. ---
|
||||
|
||||
.. automodule:: odoo.addons.base.ir.ir_qweb
|
||||
:members: QWeb, QWebContext, FieldConverter, QwebWidget
|
||||
.. It is also possible to use the ``ir.qweb`` model directly (and extend it, and
|
||||
.. inherit from it):
|
||||
|
||||
.. .. automodule:: odoo.addons.base.ir.ir_qweb
|
||||
.. :members: QWeb, QWebContext, FieldConverter, QwebWidget
|
||||
|
||||
Javascript
|
||||
==========
|
||||
|
||||
@@ -937,7 +937,7 @@ Javascript API
|
||||
:returns: an image URL
|
||||
|
||||
.. warning::
|
||||
``kanban_text_ellipsis`` has been removed in Odoo 9. CSS ``text-overflow`` should be used instead.
|
||||
``kanban_text_ellipsis`` has been removed in Odoo 9. CSS ``text-overflow`` should be used instead.
|
||||
|
||||
.. _reference/views/calendar:
|
||||
|
||||
|
||||
@@ -676,7 +676,7 @@ class IrModelFields(models.Model):
|
||||
return
|
||||
attrs['comodel_name'] = field_data['relation']
|
||||
attrs['ondelete'] = field_data['on_delete']
|
||||
attrs['domain'] = safe_eval(field_data['domain']) if field_data['domain'] else None
|
||||
attrs['domain'] = safe_eval(field_data['domain'] or '[]')
|
||||
elif field_data['ttype'] == 'one2many':
|
||||
if partial and not (
|
||||
field_data['relation'] in self.env and (
|
||||
@@ -686,7 +686,7 @@ class IrModelFields(models.Model):
|
||||
return
|
||||
attrs['comodel_name'] = field_data['relation']
|
||||
attrs['inverse_name'] = field_data['relation_field']
|
||||
attrs['domain'] = safe_eval(field_data['domain']) if field_data['domain'] else None
|
||||
attrs['domain'] = safe_eval(field_data['domain'] or '[]')
|
||||
elif field_data['ttype'] == 'many2many':
|
||||
if partial and field_data['relation'] not in self.env:
|
||||
return
|
||||
@@ -695,7 +695,7 @@ class IrModelFields(models.Model):
|
||||
attrs['relation'] = field_data['relation_table'] or rel
|
||||
attrs['column1'] = field_data['column1'] or col1
|
||||
attrs['column2'] = field_data['column2'] or col2
|
||||
attrs['domain'] = safe_eval(field_data['domain']) if field_data['domain'] else None
|
||||
attrs['domain'] = safe_eval(field_data['domain'] or '[]')
|
||||
# add compute function if given
|
||||
if field_data['compute']:
|
||||
attrs['compute'] = make_compute(field_data['compute'], field_data['depends'])
|
||||
|
||||
@@ -47,7 +47,8 @@ class IrTranslationImport(object):
|
||||
# of ir_translation, so this copy will be much faster.
|
||||
query = """ CREATE TEMP TABLE %s (
|
||||
imd_model VARCHAR(64),
|
||||
imd_name VARCHAR(128)
|
||||
imd_name VARCHAR(128),
|
||||
noupdate BOOLEAN
|
||||
) INHERITS (%s) """ % (self._table, self._model_table)
|
||||
self._cr.execute(query)
|
||||
|
||||
@@ -104,7 +105,8 @@ class IrTranslationImport(object):
|
||||
|
||||
# Step 1: resolve ir.model.data references to res_ids
|
||||
cr.execute(""" UPDATE %s AS ti
|
||||
SET res_id = imd.res_id
|
||||
SET res_id = imd.res_id,
|
||||
noupdate = imd.noupdate
|
||||
FROM ir_model_data AS imd
|
||||
WHERE ti.res_id IS NULL
|
||||
AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
|
||||
@@ -150,7 +152,10 @@ class IrTranslationImport(object):
|
||||
src = ti.src,
|
||||
state = 'translated'
|
||||
FROM %s AS ti
|
||||
WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
|
||||
WHERE %s
|
||||
AND ti.value IS NOT NULL
|
||||
AND ti.value != ''
|
||||
AND noupdate IS NOT TRUE
|
||||
""" % (self._model_table, self._table, find_expr),
|
||||
(tuple(src_relevant_fields), tuple(src_relevant_fields)))
|
||||
|
||||
|
||||
@@ -373,6 +373,8 @@ actual arch.
|
||||
else:
|
||||
|
||||
try:
|
||||
if not values.get('arch') and not values.get('arch_base'):
|
||||
raise ValidationError(_('Missing view architecture.'))
|
||||
values['type'] = etree.fromstring(values.get('arch') or values.get('arch_base')).tag
|
||||
except LxmlError:
|
||||
# don't raise here, the constraint that runs `self._check_xml` will
|
||||
|
||||
@@ -270,10 +270,6 @@ class Users(models.Model):
|
||||
if self.login and tools.single_email_re.match(self.login):
|
||||
self.email = self.login
|
||||
|
||||
@api.onchange('state_id')
|
||||
def onchange_state(self):
|
||||
return self.mapped('partner_id').onchange_state()
|
||||
|
||||
@api.onchange('parent_id')
|
||||
def onchange_parent_id(self):
|
||||
return self.mapped('partner_id').onchange_parent_id()
|
||||
|
||||
@@ -19,8 +19,16 @@ class TestDelegation(common.TransactionCase):
|
||||
record = self.record
|
||||
|
||||
# children fields can be looked up on the parent record directly
|
||||
self.assertEqual(record.field_0, 0)
|
||||
self.assertEqual(record.field_1, 1)
|
||||
self.assertEqual(
|
||||
record.field_0
|
||||
,
|
||||
0
|
||||
)
|
||||
self.assertEqual(
|
||||
record.field_1
|
||||
,
|
||||
1
|
||||
)
|
||||
|
||||
def test_swap_child(self):
|
||||
env = self.env
|
||||
|
||||
@@ -10,5 +10,13 @@ class TestBasicInheritance(common.TransactionCase):
|
||||
a = env['inheritance.0'].create({'name': 'A'})
|
||||
b = env['inheritance.1'].create({'name': 'B'})
|
||||
|
||||
self.assertEqual(a.call(), "This is model 0 record A")
|
||||
self.assertEqual(b.call(), "This is model 1 record B")
|
||||
self.assertEqual(
|
||||
a.call()
|
||||
,
|
||||
"This is model 0 record A"
|
||||
)
|
||||
self.assertEqual(
|
||||
b.call()
|
||||
,
|
||||
"This is model 1 record B"
|
||||
)
|
||||
|
||||
@@ -13,3 +13,20 @@ class TestTermCount(common.TransactionCase):
|
||||
ids = self.env['ir.translation'].search(
|
||||
[('src', '=', '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB')])
|
||||
self.assertEqual(len(ids), 2)
|
||||
|
||||
def test_noupdate(self):
|
||||
"""
|
||||
Make sure no update do not overwrite translations
|
||||
"""
|
||||
menu = self.env.ref('test_translation_import.menu_test_translation_import')
|
||||
menu.name = "New Name"
|
||||
# install french and change translation content
|
||||
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
|
||||
menu.with_context(lang='fr_FR').name = "Nouveau nom"
|
||||
# reload with overwrite
|
||||
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False, context={'overwrite': True})
|
||||
|
||||
# trans_load invalidates ormcache but not record cache
|
||||
menu.refresh()
|
||||
self.assertEqual(menu.name, "New Name")
|
||||
self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="action_test_translation_import" model="ir.actions.act_window">
|
||||
<field name="name">Test translation import</field>
|
||||
|
||||
@@ -17,7 +17,7 @@ passlib==1.6.5
|
||||
Pillow==3.4.1
|
||||
psutil==4.3.1
|
||||
psycogreen==1.0
|
||||
psycopg2==2.6.2
|
||||
psycopg2==2.7.1
|
||||
pydot==1.2.3
|
||||
pyparsing==2.1.10
|
||||
pyPdf==1.13
|
||||
|
||||