summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/report/account_aged_partner_balance.py20
-rw-r--r--addons/account/security/ir.model.access.csv2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js5
-rw-r--r--addons/account/wizard/account_report_common.py2
-rw-r--r--addons/auth_signup/models/res_partner.py8
-rw-r--r--addons/base_address_city/models/res_partner.py58
-rw-r--r--addons/calendar/models/calendar.py10
-rw-r--r--addons/calendar/tests/test_calendar.py70
-rw-r--r--addons/delivery/models/delivery_carrier.py2
-rw-r--r--addons/point_of_sale/static/src/css/pos.css2
-rw-r--r--addons/pos_restaurant/static/src/js/multiprint.js6
-rw-r--r--addons/product/report/product_pricelist.py2
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py12
-rw-r--r--addons/sale/models/sale.py3
-rw-r--r--addons/sale/report/sale_report_views.xml2
-rw-r--r--addons/sale_margin/models/sale_order.py8
-rw-r--r--addons/sale_stock/models/sale_order.py8
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js5
-rw-r--r--addons/web/static/tests/views/form_tests.js64
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js55
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js16
-rw-r--r--addons/web_editor/static/src/xml/editor.xml5
-rw-r--r--addons/website/models/ir_actions.py2
-rw-r--r--addons/website_event/views/event_templates.xml10
-rw-r--r--addons/website_form/controllers/main.py8
-rw-r--r--doc/cla/corporate/cybrosys.md2
-rw-r--r--doc/cla/corporate/subteno-it.md16
-rw-r--r--doc/cla/individual/ugaitzolaizola.md11
-rw-r--r--odoo/PKG-INFO18
-rw-r--r--odoo/addons/base/rng/calendar_view.rng2
-rw-r--r--requirements.txt2
-rw-r--r--setup.py112
33 files changed, 359 insertions, 193 deletions
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 948cebf5c23..b89ec6bcb13 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -46,7 +46,9 @@ class ReportAgedPartnerBalance(models.AbstractModel):
total = []
partner_clause = ''
cr = self.env.cr
- company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id,))
+ user_company = self.env.user.company_id
+ user_currency = user_company.currency_id
+ company_ids = self._context.get('company_ids') or [user_company.id]
move_state = ['draft', 'posted']
if target_move == 'posted':
move_state = ['posted']
@@ -128,15 +130,15 @@ class ReportAgedPartnerBalance(models.AbstractModel):
partner_id = line.partner_id.id or False
if partner_id not in partners_amount:
partners_amount[partner_id] = 0.0
- line_amount = line.balance
- if line.balance == 0:
+ line_amount = line.company_id.currency_id._convert(line.balance, user_currency, line.company_id, date_from)
+ if user_currency.is_zero(line_amount):
continue
for partial_line in line.matched_debit_ids:
if partial_line.max_date <= date_from:
- line_amount += partial_line.amount
+ line_amount += partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from)
for partial_line in line.matched_credit_ids:
if partial_line.max_date <= date_from:
- line_amount -= partial_line.amount
+ line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from)
if not self.env.user.company_id.currency_id.is_zero(line_amount):
partners_amount[partner_id] += line_amount
@@ -166,15 +168,15 @@ class ReportAgedPartnerBalance(models.AbstractModel):
partner_id = line.partner_id.id or False
if partner_id not in undue_amounts:
undue_amounts[partner_id] = 0.0
- line_amount = line.balance
- if line.balance == 0:
+ line_amount = line.company_id.currency_id._convert(line.balance, user_currency, line.company_id, date_from)
+ if user_currency.is_zero(line_amount):
continue
for partial_line in line.matched_debit_ids:
if partial_line.max_date <= date_from:
- line_amount += partial_line.amount
+ line_amount += partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from)
for partial_line in line.matched_credit_ids:
if partial_line.max_date <= date_from:
- line_amount -= partial_line.amount
+ line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from)
if not self.env.user.company_id.currency_id.is_zero(line_amount):
undue_amounts[partner_id] += line_amount
lines[partner_id].append({
diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv
index 52ac52ad108..ef09176438b 100644
--- a/addons/account/security/ir.model.access.csv
+++ b/addons/account/security/ir.model.access.csv
@@ -39,8 +39,6 @@ access_account_journal_invoice,account.journal invoice,model_account_journal,acc
access_account_invoice_group_invoice,account.invoice group invoice,model_account_invoice,account.group_account_invoice,1,1,1,1
access_res_currency_account_manager,res.currency account manager,base.model_res_currency,group_account_manager,1,1,1,1
access_res_currency_rate_account_manager,res.currency.rate account manager,base.model_res_currency_rate,group_account_manager,1,1,1,1
-access_account_invoice_user,account.invoice user,model_account_invoice,base.group_user,1,0,0,0
-access_account_invoice_line_user,account.invoice.line user,model_account_invoice_line,base.group_user,1,0,0,0
access_account_invoice_portal,account.invoice.portal,account.model_account_invoice,base.group_portal,1,0,0,0
access_account_invoice_line_portal,account.invoice.line.portal,account.model_account_invoice_line,base.group_portal,1,0,0,0
access_account_payment_term_partner_manager,account.payment.term partner manager,model_account_payment_term,base.group_user,1,0,0,0
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index b9504bd6ee6..3c9174546cf 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -1262,8 +1262,9 @@ var ManualModel = StatementModel.extend({
});
var domainReconcile = [];
- if (context && context.company_ids) {
- domainReconcile.push(['company_id', 'in', context.company_ids]);
+ var company_ids = context && context.company_ids || [session.company_id]
+ if (company_ids) {
+ domainReconcile.push(['company_id', 'in', company_ids]);
}
var def_reconcileModel = this._rpc({
model: 'account.reconcile.model',
diff --git a/addons/account/wizard/account_report_common.py b/addons/account/wizard/account_report_common.py
index 6b47f2a7e8e..f5e5814c814 100644
--- a/addons/account/wizard/account_report_common.py
+++ b/addons/account/wizard/account_report_common.py
@@ -45,4 +45,4 @@ class AccountCommonReport(models.TransientModel):
data['form'] = self.read(['date_from', 'date_to', 'journal_ids', 'target_move', 'company_id'])[0]
used_context = self._build_contexts(data)
data['form']['used_context'] = dict(used_context, lang=self.env.context.get('lang') or 'en_US')
- return self._print_report(data)
+ return self.with_context(discard_logo_check=True)._print_report(data)
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index d6161e17b91..87a47ffa455 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -57,17 +57,17 @@ class ResPartner(models.Model):
for partner in self:
# when required, make sure the partner has a valid signup token
if self.env.context.get('signup_valid') and not partner.user_ids:
- partner.signup_prepare()
+ partner.sudo().signup_prepare()
route = 'login'
# the parameters to encode for the query
query = dict(db=self.env.cr.dbname)
- signup_type = self.env.context.get('signup_force_type_in_url', partner.signup_type or '')
+ signup_type = self.env.context.get('signup_force_type_in_url', partner.sudo().signup_type or '')
if signup_type:
route = 'reset_password' if signup_type == 'reset' else signup_type
- if partner.signup_token and signup_type:
- query['token'] = partner.signup_token
+ if partner.sudo().signup_token and signup_type:
+ query['token'] = partner.sudo().signup_token
elif partner.user_ids:
query['login'] = partner.user_ids[0].login
else:
diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py
index 07c42694ef1..7591299f3e2 100644
--- a/addons/base_address_city/models/res_partner.py
+++ b/addons/base_address_city/models/res_partner.py
@@ -25,18 +25,58 @@ class Partner(models.Model):
# render the partner address accordingly to address_view_id
doc = etree.fromstring(arch)
if doc.xpath("//field[@name='city_id']"):
- return arch
- label = _('City')
- for city_node in doc.xpath("//field[@name='city']"):
- replacement_xml = """
+ return arch
+
+ replacement_xml = """
<div>
<field name="country_enforce_cities" invisible="1"/>
- <field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
- <field name='city_id' placeholder="%s" string="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/>
+ <field name='city' placeholder="%(placeholder)s"
+ attrs="{
+ 'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)],
+ 'readonly': [('type', '=', 'contact')%(parent_condition)s]
+ }"
+ />
+ <field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s"
+ context="{'default_country_id': country_id}"
+ domain="[('country_id', '=', country_id)]"
+ attrs="{
+ 'invisible': [('country_enforce_cities', '=', False)],
+ 'readonly': [('type', '=', 'contact')%(parent_condition)s]
+ }"
+ />
</div>
- """ % (label, label, label)
- city_id_node = etree.fromstring(replacement_xml)
- city_node.getparent().replace(city_node, city_id_node)
+ """
+
+ replacement_data = {
+ 'placeholder': _('City'),
+ }
+
+ def _arch_location(node):
+ in_subview = False
+ view_type = False
+ parent = node.getparent()
+ while parent is not None and (not view_type or not in_subview):
+ if parent.tag == 'field':
+ in_subview = True
+ elif parent.tag in ['list', 'tree', 'kanban', 'form']:
+ view_type = parent.tag
+ parent = parent.getparent()
+ return {
+ 'view_type': view_type,
+ 'in_subview': in_subview,
+ }
+
+ for city_node in doc.xpath("//field[@name='city']"):
+ location = _arch_location(city_node)
+ replacement_data['parent_condition'] = ''
+ if location['view_type'] == 'form' or not location['in_subview']:
+ replacement_data['parent_condition'] = ", ('parent_id', '!=', False)"
+
+ replacement_formatted = replacement_xml % replacement_data
+ for replace_node in etree.fromstring(replacement_formatted).getchildren():
+ city_node.addprevious(replace_node)
+ parent = city_node.getparent()
+ parent.remove(city_node)
arch = etree.tostring(doc, encoding='unicode')
return arch
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 8124749f75e..cc91739fb6a 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -1789,7 +1789,15 @@ class Meeting(models.Model):
if values.get('description'):
activity_values['note'] = values['description']
if values.get('start'):
- activity_values['date_deadline'] = fields.Datetime.from_string(values['start']).date()
+ # self.start is a datetime UTC *only when the event is not allday*
+ # activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is)
+ # See 72254129dbaeae58d0a2055cba4e4a82cde495b7 for the same issue, but elsewhere
+ deadline = fields.Datetime.from_string(values['start'])
+ user_tz = self.env.context.get('tz')
+ if user_tz and not self.allday:
+ deadline = pytz.UTC.localize(deadline)
+ deadline = deadline.astimezone(pytz.timezone(user_tz))
+ activity_values['date_deadline'] = deadline.date()
if values.get('user_id'):
activity_values['user_id'] = values['user_id']
if activity_values.keys():
diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py
index fe456339eaa..085c3671491 100644
--- a/addons/calendar/tests/test_calendar.py
+++ b/addons/calendar/tests/test_calendar.py
@@ -314,3 +314,73 @@ class TestCalendar(TransactionCase):
else:
self.assertEqual(d.hour, 15)
self.assertEqual(d.minute, 30)
+
+ def test_event_activity_timezone(self):
+ activty_type = self.env['mail.activity.type'].create({
+ 'name': 'Meeting',
+ 'category': 'meeting'
+ })
+
+ activity_id = self.env['mail.activity'].create({
+ 'summary': 'Meeting with partner',
+ 'activity_type_id': activty_type.id,
+ 'res_model_id': self.env['ir.model'].search([('model', '=', 'res.partner')], limit=1).id,
+ 'res_id': self.env['res.partner'].search([('name', 'ilike', 'Agrolait')], limit=1).id,
+ })
+
+ calendar_event = self.env['calendar.event'].create({
+ 'name': 'Meeting with partner',
+ 'activity_ids': [(6, False, activity_id.ids)],
+ 'start': '2018-11-12 21:00:00',
+ 'stop': '2018-11-13 00:00:00',
+ })
+
+ # Check output in UTC
+ self.assertEqual(activity_id.date_deadline, '2018-11-12')
+
+ # Check output in the user's tz
+ # write on the event to trigger sync of activities
+ calendar_event.with_context({'tz': 'Australia/Brisbane'}).write({
+ 'start': '2018-11-12 21:00:00',
+ })
+
+ self.assertEqual(activity_id.date_deadline, '2018-11-13')
+
+ def test_event_allday_activity_timezone(self):
+ # Covers use case of commit eef4c3b48bcb4feac028bf640b545006dd0c9b91
+ # Also, read the comment in the code at calendar.event._inverse_dates
+ activty_type = self.env['mail.activity.type'].create({
+ 'name': 'Meeting',
+ 'category': 'meeting'
+ })
+
+ activity_id = self.env['mail.activity'].create({
+ 'summary': 'Meeting with partner',
+ 'activity_type_id': activty_type.id,
+ 'res_model_id': self.env['ir.model'].search([('model', '=', 'res.partner')], limit=1).id,
+ 'res_id': self.env['res.partner'].search([('name', 'ilike', 'Agrolait')], limit=1).id,
+ })
+
+ calendar_event = self.env['calendar.event'].create({
+ 'name': 'All Day',
+ 'start': "2018-10-16 00:00:00",
+ 'start_date': "2018-10-16",
+ 'start_datetime': False,
+ 'stop': "2018-10-18 00:00:00",
+ 'stop_date': "2018-10-18",
+ 'stop_datetime': False,
+ 'allday': True,
+ 'activity_ids': [(6, False, activity_id.ids)],
+ })
+
+ # Check output in UTC
+ self.assertEqual(activity_id.date_deadline, '2018-10-16')
+
+ # Check output in the user's tz
+ # write on the event to trigger sync of activities
+ calendar_event.with_context({'tz': 'Pacific/Honolulu'}).write({
+ 'start': '2018-10-16 00:00:00',
+ 'start_date': '2018-10-16',
+ })
+
+ self.assertEqual(activity_id.date_deadline, '2018-10-16')
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py
index 77f34f856d0..9f07ad5c6e5 100644
--- a/addons/delivery/models/delivery_carrier.py
+++ b/addons/delivery/models/delivery_carrier.py
@@ -217,7 +217,7 @@ class DeliveryCarrier(models.Model):
'error_message': _('Error: this delivery method is not available for this address.'),
'warning_message': False}
price = self.fixed_price
- if self.company_id.currency_id.id != order.currency_id.id:
+ if self.company_id and self.company_id.currency_id.id != order.currency_id.id:
price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
return {'success': True,
'price': price,
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index b991609ca89..59ec2f0b801 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -437,6 +437,8 @@ td {
display: flex;
-webkit-flex: 1;
flex: 1;
+ max-width: -moz-available;
+ max-width: -webkit-fill-available;
}
.pos .orders {
display: -webkit-flex;
diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js
index e61f8d1097e..c48ecae1c5d 100644
--- a/addons/pos_restaurant/static/src/js/multiprint.js
+++ b/addons/pos_restaurant/static/src/js/multiprint.js
@@ -139,8 +139,10 @@ models.Orderline = models.Orderline.extend({
}
},
set_dirty: function(dirty) {
- this.mp_dirty = dirty;
- this.trigger('change',this);
+ if (this.mp_dirty !== dirty) {
+ this.mp_dirty = dirty;
+ this.trigger('change', this);
+ }
},
get_line_diff_hash: function(){
if (this.get_note()) {
diff --git a/addons/product/report/product_pricelist.py b/addons/product/report/product_pricelist.py
index 2a94a4b2a29..e6ccde44d8e 100644
--- a/addons/product/report/product_pricelist.py
+++ b/addons/product/report/product_pricelist.py
@@ -17,7 +17,7 @@ class report_product_pricelist(models.AbstractModel):
quantities = self._get_quantity(data)
return {
'doc_ids': data.get('ids', data.get('active_ids')),
- 'doc_model': 'hr.contribution.register',
+ 'doc_model': 'product.pricelist',
'docs': products,
'data': dict(
data,
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 6778c47ebc3..7e899b2e928 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -492,3 +492,15 @@ class StockMove(models.Model):
return [(requisition_line.requisition_id, requisition_line.requisition_id.user_id, visited) for requisition_line in self.requisition_line_ids if requisition_line.state not in ('done', 'cancel')]
else:
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
+
+
+class Orderpoint(models.Model):
+ _inherit = "stock.warehouse.orderpoint"
+
+ def _quantity_in_progress(self):
+ res = super(Orderpoint, self)._quantity_in_progress()
+ for op in self:
+ for pr in self.env['purchase.requisition'].search([('state','=','draft'),('origin','=',op.name)]):
+ for prline in pr.line_ids.filtered(lambda l: l.product_id.id == op.product_id.id):
+ res[op.id] += prline.product_uom_id._compute_quantity(prline.product_qty, op.product_uom, round=False)
+ return res
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 6afc0b6b5d3..1169b137e1c 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -549,10 +549,11 @@ class SaleOrder(models.Model):
raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.'))
for invoice in invoices.values():
+ invoice.compute_taxes()
if not invoice.invoice_line_ids:
raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.'))
# If invoice is negative, do a refund invoice instead
- if invoice.amount_untaxed < 0:
+ if invoice.amount_total < 0:
invoice.type = 'out_refund'
for line in invoice.invoice_line_ids:
line.quantity = -line.quantity
diff --git a/addons/sale/report/sale_report_views.xml b/addons/sale/report/sale_report_views.xml
index ef419e6eb0a..0b098040bd5 100644
--- a/addons/sale/report/sale_report_views.xml
+++ b/addons/sale/report/sale_report_views.xml
@@ -18,7 +18,7 @@
<field name="model">sale.report</field>
<field name="arch" type="xml">
<graph string="Sales Analysis" type="line">
- <field name="date" type="row" interval="day"/>
+ <field name="confirmation_date" type="row" interval="day"/>
<field name="price_subtotal" type="measure"/>
</graph>
</field>
diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py
index cf5902d6b2b..866890ff822 100644
--- a/addons/sale_margin/models/sale_order.py
+++ b/addons/sale_margin/models/sale_order.py
@@ -60,14 +60,6 @@ class SaleOrderLine(models.Model):
for line in self:
currency = line.order_id.pricelist_id.currency_id
price = line.purchase_price
- if not price:
- from_cur = line.env.user.company_id.currency_id
- price = from_cur._convert(
- line.product_id.standard_price,
- currency,
- line.order_id.company_id or self.env.user.company_id,
- line.order_id.date_order or fields.Date.today(), round=False)
-
line.margin = currency.round(line.price_subtotal - (price * line.product_uom_qty))
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 9d84752dd24..430c3cc52d9 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -327,7 +327,10 @@ class SaleOrderLine(models.Model):
self.ensure_one()
qty = 0.0
for move in self.move_ids.filtered(lambda r: r.state != 'cancel'):
- qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
+ if move.picking_code == 'outgoing':
+ qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
+ elif move.picking_code == 'incoming':
+ qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
return qty
@api.multi
@@ -433,7 +436,8 @@ class SaleOrderLine(models.Model):
return is_available
def _update_line_quantity(self, values):
- if self.mapped('qty_delivered') and values['product_uom_qty'] < max(self.mapped('qty_delivered')):
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ if self.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(self.mapped('qty_delivered')), precision_digits=precision) == -1:
raise UserError(_('You cannot decrease the ordered quantity below the delivered quantity.\n'
'Create a return first.'))
super(SaleOrderLine, self)._update_line_quantity(values)
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 43d604eac0b..76adef0a154 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -264,7 +264,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
resIDs: record.res_ids,
},
on_success: def.resolve.bind(def),
- on_fail: def.reject.bind(def),
+ on_fail: function () {
+ self.update({}, {reload: false}).always(def.reject.bind(def));
+ },
on_closed: reload,
});
return this.alive(def);
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index d6ea10d1243..ee11afbc3bd 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -186,9 +186,12 @@ var FormViewDialog = ViewDialog.extend({
}
fields_view_def.then(function (viewInfo) {
+ var refinedContext = _.pick(self.context, function (value, key) {
+ return key.indexOf('_view_ref') === -1;
+ });
var formview = new FormView(viewInfo, {
modelName: self.res_model,
- context: self.context,
+ context: refinedContext,
ids: self.res_id ? [self.res_id] : [],
currentId: self.res_id || undefined,
index: 0,
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 0af96038c96..ada27d09afc 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -6986,6 +6986,69 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('keep editing after call_button fail', function (assert) {
+ assert.expect(4);
+
+ var values;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form>' +
+ '<button name="post" class="p" string="Raise Error" type="object"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ execute_action: function (ev) {
+ assert.ok(true, 'the action is correctly executed');
+ ev.data.on_fail();
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p[0][2], values);
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // add a row and partially fill it
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('input:nth(0)').val('abc').trigger('input');
+
+ // click button which will trigger_up 'execute_action' (this will save)
+ values = {
+ display_name: 'abc',
+ product_id: false,
+ };
+ form.$('button.p').click();
+
+ // edit the new row again and set a many2one value
+ form.$('.o_form_view .o_field_one2many .o_data_row .o_data_cell').click();
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ form.$('.o_field_many2one input').click();
+ $dropdown.find('li:first()').click();
+
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'xphone',
+ "value of the m2o should have been correctly updated");
+
+ values = {
+ product_id: 37,
+ };
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
QUnit.test('asynchronous rendering of a widget tag', function (assert) {
assert.expect(1);
@@ -7283,7 +7346,6 @@ QUnit.module('Views', {
'</sheet>' +
'</form>',
res_id: 2,
- //debug:1,
});
assert.strictEqual(form.$buttons.find('.o_form_button_edit')[0],document.activeElement,
"in read mode, when there are no primary buttons on the form, the default button with the focus should be edit");
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index b45c27e4d60..372edbc0f24 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -364,6 +364,61 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('Form dialog and subview with _view_ref contexts', function (assert) {
+ assert.expect(2);
+
+ this.data.instrument.records = [{id: 1, name: 'Tromblon', badassery: [1]}];
+ this.data.partner.records[0].instrument = 1;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="instrument" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'instrument,false,form': '<form>'+
+ '<field name="name"/>'+
+ '<field name="badassery" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' +
+ '</form>',
+
+ 'badassery,false,list': '<tree>'+
+ '<field name="level"/>'+
+ '</tree>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+
+ mockRPC: function(route, args) {
+ if (args.method === 'get_formview_id') {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+
+ interceptsPropagate: {
+ load_views: function (ev) {
+ var evaluatedContext = ev.data.context;
+ if (ev.data.modelName === 'instrument') {
+ assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_tree_view'},
+ 'The correct _view_ref should have been sent to the server, first time');
+ }
+ if (ev.data.modelName === 'badassery') {
+ assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_other_tree_view'},
+ 'The correct _view_ref should have been sent to the server for the subview');
+ }
+ },
+ },
+ });
+
+ form.$('.o_field_widget[name="instrument"] button.o_external_button').click();
+ form.destroy();
+ });
+
QUnit.test('SelectCreateDialog: save current search', function (assert) {
assert.expect(4);
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index a2f0f6a308f..36f5becce06 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -1936,6 +1936,15 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
if (!r) {
return;
}
+ // select content since container (that firefox selects) may be removed
+ if (r.so === 0) {
+ r.sc = dom.firstChild(r.sc);
+ }
+ if (dom.nodeLength(r.ec) >= r.eo) {
+ r.ec = dom.lastChild(r.ec);
+ r.eo = dom.nodeLength(r.ec);
+ }
+ r = range.create(r.sc, r.so, r.ec, r.eo);
r.reRange().select();
if (sTagName === "blockquote" || sTagName === "pre") {
@@ -1949,6 +1958,10 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
for (var i=0; i<nodes.length; i++) {
if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
var ancestor = dom.ancestor(nodes[i], isFormatNode);
+ if ($(ancestor).parent().is('blockquote')) {
+ // firefox may wrap formatting block in blockquote
+ $(ancestor).unwrap();
+ }
if (!ancestor) {
dom.wrap(nodes[i], sTagName);
} else if (ancestor.tagName.toLowerCase() !== sTagName) {
@@ -2272,7 +2285,8 @@ options.onCreateLink = function (sLinkUrl) {
// pass
} else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
sLinkUrl = 'mailto:' + sLinkUrl;
- } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl.indexOf('/') !== 0 && sLinkUrl.indexOf('#') !== 0) {
+ } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/'
+ && sLinkUrl[0] !== '#' && sLinkUrl.slice(0, 2) !== '${') {
sLinkUrl = 'http://' + sLinkUrl;
}
return sLinkUrl;
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index 85cf336130d..631bd069e8b 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -81,11 +81,12 @@
<!-- Image choosing part of the Media Dialog -->
<t t-name="web_editor.dialog.image">
+ <t t-set="iframeName" t-value="_.uniqueId('fileframe-')"/>
<form class="form-inline"
method="POST"
action="/web_editor/attachment/add"
enctype="multipart/form-data"
- target="fileframe">
+ t-att-target="iframeName">
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
<input type="hidden" name="filters" t-att-value="widget.firstFilters.join('_')"/>
<input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
@@ -119,7 +120,7 @@
<div class="existing-attachments">
<t t-call="web_editor.dialog.image.existing"/>
</div>
- <iframe class="d-none" name="fileframe"/>
+ <iframe class="d-none" t-att-name="iframeName"/>
</form>
</t>
<t t-name="web_editor.dialog.image.existing">
diff --git a/addons/website/models/ir_actions.py b/addons/website/models/ir_actions.py
index f26d6263004..4add5587d7f 100644
--- a/addons/website/models/ir_actions.py
+++ b/addons/website/models/ir_actions.py
@@ -38,7 +38,7 @@ class ServerAction(models.Model):
def _get_website_url(self):
for action in self:
if action.state == 'code' and action.website_published:
- action.website_url = self._compute_website_url(action.website_path, action.xml_id)
+ action.website_url = action._compute_website_url(action.website_path, action.xml_id)
@api.model
def _get_eval_context(self, action):
diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml
index 20608d9e0d7..fc70e2ee4b0 100644
--- a/addons/website_event/views/event_templates.xml
+++ b/addons/website_event/views/event_templates.xml
@@ -74,14 +74,14 @@
</li>
</ul>
</div>
- <div class="row">
- <div class="col-md-6 offset-3">
- <t t-call="website.pager"/>
- </div>
- </div>
<div class="col-lg-3 oe_structure" id="right_column">
</div>
</div>
+ <div class="row">
+ <div class="col-md-6 offset-3">
+ <t t-call="website.pager"/>
+ </div>
+ </div>
</div>
<div class="oe_structure" id="oe_structure_website_event_index_2"/>
</div>
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index 41a120008b4..f8a75af3130 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -221,10 +221,10 @@ class WebsiteForm(http.Controller):
else:
orphan_attachment_ids.append(attachment_id.id)
- # If some attachments didn't match a field on the model,
- # we create a mail.message to link them to the record
- if orphan_attachment_ids:
- if model_name != 'mail.mail':
+ if model_name != 'mail.mail':
+ # If some attachments didn't match a field on the model,
+ # we create a mail.message to link them to the record
+ if orphan_attachment_ids:
values = {
'body': _('<p>Attached files : </p>'),
'model': model_name,
diff --git a/doc/cla/corporate/cybrosys.md b/doc/cla/corporate/cybrosys.md
index 0d413d31ba7..14fedd60e02 100644
--- a/doc/cla/corporate/cybrosys.md
+++ b/doc/cla/corporate/cybrosys.md
@@ -8,7 +8,7 @@ declaration.
Signed,
-Niyas Raphy niyas@cybrosys.com https://github.com/CybroOdoo
+Sainu Abideen sainu@cybrosys.com https://github.com/CybroOdoo
List of contributors:
diff --git a/doc/cla/corporate/subteno-it.md b/doc/cla/corporate/subteno-it.md
new file mode 100644
index 00000000000..8bcf028ec2d
--- /dev/null
+++ b/doc/cla/corporate/subteno-it.md
@@ -0,0 +1,16 @@
+France, 2018-11-14
+
+Subteno IT agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Sébastien LANGE sebastien.lange@subteno-it.fr https://github.com/sla-subteno-it
+
+List of contributors:
+
+Christian LECOUFLE christian.lecoufle@subteno-it.fr https://github.com/cle-subteno-it
+Vincent COFFIN vincent.coffin@subteno-it.fr https://github.com/vco-subteno-it
diff --git a/doc/cla/individual/ugaitzolaizola.md b/doc/cla/individual/ugaitzolaizola.md
new file mode 100644
index 00000000000..535dad871ab
--- /dev/null
+++ b/doc/cla/individual/ugaitzolaizola.md
@@ -0,0 +1,11 @@
+Spain, 2018-10-17
+
+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,
+
+Ugaitz Olaizola Arbelaitz uolaizola@binovo.es
diff --git a/odoo/PKG-INFO b/odoo/PKG-INFO
deleted file mode 100644
index fba4a850f51..00000000000
--- a/odoo/PKG-INFO
+++ /dev/null
@@ -1,18 +0,0 @@
-Metadata-Version: 1.1
-Name: OpenERP
-Version: 6.0.0
-Author: OpenERP S.A.
-Author-email: fp at openerp com
-Maintainer: OpenERP S.A.
-Maintainer-email: fp at openerp com
-Home-page: http://www.openerp.com
-Download-url: http://www.openerp.com/downloads.html
-Summary: OpenERP is an Enterprise Resource Management written entirely in python.
-License: GPL-3
-Description: OpenERP is a complete ERP and CRM. The main features are accounting (analytic
- and financial), stock management, sales and purchases management, tasks
- automation, marketing campaigns, help desk, POS, etc. Technical features include
- a distributed server, flexible workflows, an object database, a dynamic GUI,
- customizable reports, and an XML-RPC interface.
-Keywords: ERP, Accounting, Stock, CRM, Enterprise, Logistics, Management, Sales, Purchases
-Platform: Linux, Win32
diff --git a/odoo/addons/base/rng/calendar_view.rng b/odoo/addons/base/rng/calendar_view.rng
index 949c3f48318..500a61b3389 100644
--- a/odoo/addons/base/rng/calendar_view.rng
+++ b/odoo/addons/base/rng/calendar_view.rng
@@ -32,7 +32,7 @@
<rng:ref name="field"/>
</rng:zeroOrMore>
<rng:optional>
- <rng:element name="template">
+ <rng:element name="templates">
<rng:oneOrMore>
<rng:ref name="any"/>
</rng:oneOrMore>
diff --git a/requirements.txt b/requirements.txt
index 3328dcabebb..e95eb80da24 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ lxml ; sys_platform == 'win32'
Mako==1.0.4
MarkupSafe==0.23
mock==2.0.0
-num2words==0.5.4
+num2words==0.5.6
ofxparse==0.16
passlib==1.6.5
Pillow==4.0.0
diff --git a/setup.py b/setup.py
index eba927b573b..ab2b730b50e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-import os
-import re
-from glob import glob
from setuptools import find_packages, setup
from os.path import join, dirname
@@ -11,114 +8,6 @@ from os.path import join, dirname
exec(open(join(dirname(__file__), 'odoo', 'release.py'), 'rb').read()) # Load release variables
lib_name = 'odoo'
-
-def py2exe_datafiles():
- data_files = {}
- data_files['Microsoft.VC90.CRT'] = glob('C:\Microsoft.VC90.CRT\*.*')
-
- for root, dirnames, filenames in os.walk('odoo'):
- for filename in filenames:
- if not re.match(r'.*(\.pyc|\.pyo|\~)$', filename):
- data_files.setdefault(root, []).append(join(root, filename))
-
- import babel
- data_files['babel/localedata'] = glob(join(dirname(babel.__file__), 'localedata', '*'))
- others = ['global.dat', 'numbers.py', 'support.py', 'plural.py']
- data_files['babel'] = [join(dirname(babel.__file__), f) for f in others]
- others = ['frontend.py', 'mofile.py']
- data_files['babel/messages'] = [join(dirname(babel.__file__), 'messages', f) for f in others]
-
- import pytz
- tzdir = dirname(pytz.__file__)
- for root, _, filenames in os.walk(join(tzdir, 'zoneinfo')):
- base = join('pytz', root[len(tzdir) + 1:])
- data_files[base] = [join(root, f) for f in filenames]
-
- import docutils
- import passlib
- import reportlab
- import requests
- data_mapping = ((docutils, 'docutils'),
- (passlib, 'passlib'),
- (reportlab, 'reportlab'),
- (requests, 'requests'))
-
- for mod, datadir in data_mapping:
- basedir = dirname(mod.__file__)
- for root, _, filenames in os.walk(basedir):
- base = join(datadir, root[len(basedir) + 1:])
- data_files[base] = [join(root, f)
- for f in filenames
- if not f.endswith(('.py', '.pyc', '.pyo'))]
-
- return list(data_files.items())
-
-
-def py2exe_options():
- if os.name == 'nt':
- import py2exe
- return {
- 'console': [
- {'script': 'odoo-bin', 'icon_resources': [
- (1, join('setup', 'win32', 'static', 'pixmaps', 'openerp-icon.ico'))
- ]},
- ],
- 'options': {
- 'py2exe': {
- 'skip_archive': 1,
- 'optimize': 0, # Keep the assert running as the integrated tests rely on them.
- 'dist_dir': 'dist',
- 'packages': [
- 'asynchat', 'asyncore',
- 'BeautifulSoup',
- 'commands',
- 'dateutil',
- 'decimal',
- 'decorator',
- 'docutils',
- 'email',
- 'encodings',
- 'HTMLParser',
- 'imaplib',
- 'jinja2',
- 'lxml', 'lxml._elementpath', 'lxml.builder', 'lxml.etree', 'lxml.objectify',
- 'mako',
- 'markupsafe',
- 'mock',
- 'ofxparse',
- 'odoo',
- 'passlib',
- 'PIL',
- 'poplib',
- 'psutil',
- 'pychart',
- 'pydot',
- 'pyparsing',
- 'PyPDF2',
- 'pytz',
- 'reportlab',
- 'requests',
- 'select',
- 'smtplib',
- 'suds',
- 'uuid',
- 'vatnumber',
- 'vobject',
- 'win32service', 'win32serviceutil',
- 'xlrd',
- 'xlsxwriter',
- 'xlwt',
- 'xml', 'xml.dom',
- ],
- 'excludes': ['Tkconstants', 'Tkinter', 'tcl'],
- }
- },
- 'data_files': py2exe_datafiles()
- }
- else:
- return {}
-
-
setup(
name='odoo',
version=version,
@@ -175,5 +64,4 @@ setup(
tests_require=[
'mock',
],
- **py2exe_options()
)