[MERGE] forward port branch 10.0 up to aaadb232b2

This commit is contained in:
Christophe Simonis
2017-06-26 13:00:18 +02:00
70 changed files with 759 additions and 541 deletions
+5
View File
@@ -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
+5
View File
@@ -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 = []
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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);
},
+5
View File
@@ -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(),
)
)
""")
+1 -1
View File
@@ -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"/>
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -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);
}
}
});
}
+1 -1
View File
@@ -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')
+26 -4
View File
@@ -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
+4 -1
View File
@@ -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)
+10 -5
View File
@@ -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
+26 -3
View File
@@ -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)
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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.
+10
View File
@@ -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})
+2 -2
View File
@@ -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)
-3
View File
@@ -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'),
+1 -1
View File
@@ -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)
+20 -1
View File
@@ -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 = []
+34
View File
@@ -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)
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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
+4 -3
View File
@@ -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):
+2
View File
@@ -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. '))
-1
View File
@@ -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)])
+9 -2
View File
@@ -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;
});
+9
View File
@@ -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">
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

-9
View File
@@ -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>
+11
View File
@@ -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
-5
View File
@@ -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
+1 -1
View File
@@ -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 -2
View File
@@ -2,9 +2,9 @@
.. _reference/cmdline:
===============================
================================
Command-line interface: odoo-bin
===============================
================================
.. _reference/cmdline/server:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+41 -17
View File
@@ -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
+5 -5
View File
@@ -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
+8 -6
View File
@@ -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
==========
+1 -1
View File
@@ -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:
+3 -3
View File
@@ -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'])
+8 -3
View File
@@ -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)))
+2
View File
@@ -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
-4
View File
@@ -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>
+1 -1
View File
@@ -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