summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/models/account_invoice.py13
-rw-r--r--addons/account/models/res_config_settings.py2
-rw-r--r--addons/account/views/account_journal_dashboard_view.xml12
-rw-r--r--addons/account_bank_statement_import/account_bank_statement_import.py6
-rw-r--r--addons/board/static/src/js/favorite_menu.js5
-rw-r--r--addons/crm/models/crm_lead.py2
-rw-r--r--addons/event/models/res_partner.py2
-rw-r--r--addons/event/report/event_event_templates.xml2
-rw-r--r--addons/hr_holidays/models/hr_leave.py2
-rw-r--r--addons/mail/static/src/js/systray.js8
-rw-r--r--addons/mass_mailing/static/src/js/mass_mailing.js2
-rw-r--r--addons/mass_mailing_event_track/models/event.py2
-rw-r--r--addons/mrp/models/mrp_bom.py2
-rw-r--r--addons/mrp/models/mrp_workorder.py1
-rw-r--r--addons/mrp/models/stock_move.py7
-rw-r--r--addons/mrp/report/mrp_bom_structure_report.py55
-rw-r--r--addons/mrp/views/mrp_workorder_views.xml2
-rw-r--r--addons/payment_authorize/models/payment.py13
-rw-r--r--addons/point_of_sale/models/pos_session.py4
-rw-r--r--addons/point_of_sale/models/product.py12
-rw-r--r--addons/point_of_sale/static/src/js/models.js3
-rw-r--r--addons/portal/controllers/mail.py2
-rw-r--r--addons/portal/wizard/portal_wizard.py2
-rw-r--r--addons/product/models/product.py2
-rw-r--r--addons/product/models/product_attribute.py3
-rw-r--r--addons/product/tests/__init__.py2
-rw-r--r--addons/product/tests/test_seller.py28
-rw-r--r--addons/project/models/project.py2
-rw-r--r--addons/purchase/views/purchase_template.xml2
-rw-r--r--addons/repair/models/repair.py17
-rw-r--r--addons/repair/report/repair_templates_repair_order.xml3
-rw-r--r--addons/sale/report/report_all_channels_sales_views.xml2
-rw-r--r--addons/sale_timesheet/__init__.py12
-rw-r--r--addons/sale_timesheet/__manifest__.py1
-rw-r--r--addons/stock/models/product.py6
-rw-r--r--addons/stock/models/stock_move.py7
-rw-r--r--addons/stock/tests/test_move.py21
-rw-r--r--addons/stock_account/models/product.py63
-rw-r--r--addons/stock_account/models/stock.py3
-rw-r--r--addons/stock_account/tests/test_stockvaluation.py152
-rw-r--r--addons/website/models/ir_model.py4
-rw-r--r--addons/website_event/static/src/js/website_event.js4
-rw-r--r--addons/website_event_track/views/event_track_templates.xml4
-rw-r--r--addons/website_forum/models/forum.py5
-rw-r--r--addons/website_livechat/controllers/main.py10
-rw-r--r--addons/website_mass_mailing/controllers/main.py2
-rw-r--r--addons/website_sale_stock/models/product_product.py2
-rw-r--r--addons/website_sale_wishlist/models/product_wishlist.py7
-rw-r--r--doc/cla/corporate/adhoc.md1
-rw-r--r--doc/cla/corporate/modoolar.md19
-rw-r--r--doc/cla/corporate/shine-it.md17
-rw-r--r--doc/cla/corporate/tvtmarine-automation.md4
-rw-r--r--doc/cla/individual/Zelwak.md11
-rw-r--r--doc/cla/individual/mduvergey.md11
-rw-r--r--doc/cla/individual/schout-it.md11
-rw-r--r--doc/setup/enterprise.rst2
-rw-r--r--odoo/addons/base/data/res.country.state.csv2
-rw-r--r--odoo/addons/base/data/res_country_data.xml5
-rw-r--r--odoo/addons/base/models/ir_mail_server.py6
-rw-r--r--odoo/addons/base/models/qweb.py7
-rw-r--r--odoo/addons/base/models/res_users.py5
-rw-r--r--odoo/addons/base/rng/tree_view.rng2
-rw-r--r--odoo/addons/base/security/base_security.xml2
-rw-r--r--odoo/addons/base/tests/test_qweb.py21
-rw-r--r--odoo/addons/test_pylint/tests/test_pylint.py8
-rw-r--r--odoo/models.py4
-rw-r--r--odoo/service/server.py5
-rw-r--r--setup.py1
68 files changed, 542 insertions, 127 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 94c1fa485c7..a233c2947b0 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -396,7 +396,7 @@ class AccountInvoice(models.Model):
""" computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to
let the user manually change it.
"""
- if not self.env.user._is_admin():
+ if not self.env.user._is_system():
for invoice in self:
invoice.sequence_number_next_prefix = False
invoice.sequence_number_next = ''
@@ -1519,6 +1519,13 @@ class AccountInvoiceLine(models.Model):
return accounts['income']
return accounts['expense']
+ def _set_currency(self):
+ company = self.invoice_id.company_id
+ currency = self.invoice_id.currency_id
+ if company and currency:
+ if company.currency_id != currency:
+ self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
+
def _set_taxes(self):
""" Used in on_change to set taxes and price."""
if self.invoice_id.type in ('out_invoice', 'out_refund'):
@@ -1537,8 +1544,10 @@ class AccountInvoiceLine(models.Model):
prec = self.env['decimal.precision'].precision_get('Product Price')
if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0:
self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes)
+ self._set_currency()
else:
self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes)
+ self._set_currency()
@api.onchange('product_id')
def _onchange_product_id(self):
@@ -1587,8 +1596,6 @@ class AccountInvoiceLine(models.Model):
domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
if company and currency:
- if company.currency_id != currency:
- self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
if self.uom_id and self.uom_id.id != product.uom_id.id:
self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id)
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index d4593130d2c..231bae4b54b 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -14,7 +14,7 @@ class ResConfigSettings(models.TransientModel):
'account.journal',
related='company_id.currency_exchange_journal_id',
string="Exchange Gain or Loss Journal",
- domain=[('type', '=', 'general')],
+ domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
help='The accounting journal where automatic exchange differences will be registered')
has_chart_of_accounts = fields.Boolean(compute='_compute_has_chart_of_accounts', string='Company has a chart of accounts')
chart_template_id = fields.Many2one('account.chart.template', string='Template',
diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml
index 9c7f1aca551..27f879b8018 100644
--- a/addons/account/views/account_journal_dashboard_view.xml
+++ b/addons/account/views/account_journal_dashboard_view.xml
@@ -248,14 +248,16 @@
<span><t t-esc="dashboard.account_balance"/></span>
</div>
</div>
- <div class="row" name="latest_statement" t-if="dashboard.last_balance != dashboard.account_balance">
- <div class="col-xs-7">
+ <t t-if="dashboard.last_balance != dashboard.account_balance">
+ <div class="row" name="latest_statement">
+ <div class="col-xs-7">
<span title="Latest Statement">Latest Statement</span>
- </div>
- <div class="col-xs-5 text-right">
+ </div>
+ <div class="col-xs-5 text-right">
<span><t t-esc="dashboard.last_balance"/></span>
+ </div>
</div>
- </div>
+ </t>
</div>
</t>
<t t-name="JournalBodySalePurchase" id="account.JournalBodySalePurchase">
diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py
index 99fd0467418..ffbfba368aa 100644
--- a/addons/account_bank_statement_import/account_bank_statement_import.py
+++ b/addons/account_bank_statement_import/account_bank_statement_import.py
@@ -50,7 +50,11 @@ class AccountBankStatementImport(models.TransientModel):
# Create the bank statements
statement_ids, notifications = self._create_bank_statements(stmts_vals)
# Now that the import worked out, set it as the bank_statements_source of the journal
- journal.bank_statements_source = 'file_import'
+ if journal.bank_statements_source != 'file_import':
+ # Use sudo() because only 'account.group_account_manager'
+ # has write access on 'account.journal', but 'account.group_account_user'
+ # must be able to import bank statement files
+ journal.sudo().bank_statements_source = 'file_import'
# Finally dispatch to reconciliation interface
action = self.env.ref('account.action_bank_reconcile_bank_statements')
return {
diff --git a/addons/board/static/src/js/favorite_menu.js b/addons/board/static/src/js/favorite_menu.js
index 77cd3df4967..fa25ef6ac6e 100644
--- a/addons/board/static/src/js/favorite_menu.js
+++ b/addons/board/static/src/js/favorite_menu.js
@@ -95,7 +95,10 @@ FavoriteMenu.include({
})
.then(function (r) {
if (r) {
- self.do_notify(_.str.sprintf(_t("'%s' added to dashboard"), name), '');
+ self.do_notify(
+ _.str.sprintf(_t("'%s' added to dashboard"), name),
+ _t('Please refresh your browser for the changes to take effect.')
+ );
} else {
self.do_warn(_t("Could not add filter to dashboard"));
}
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index fb233a50da6..53c1094f997 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -236,6 +236,8 @@ class Lead(models.Model):
@api.model
def _onchange_user_values(self, user_id):
""" returns new values when user_id has changed """
+ if not user_id:
+ return {}
if user_id and self._context.get('team_id'):
team = self.env['crm.team'].browse(self._context['team_id'])
if user_id in team.member_ids.ids:
diff --git a/addons/event/models/res_partner.py b/addons/event/models/res_partner.py
index d534f3749f2..727de940fa1 100644
--- a/addons/event/models/res_partner.py
+++ b/addons/event/models/res_partner.py
@@ -10,7 +10,7 @@ class ResPartner(models.Model):
def _compute_event_count(self):
for partner in self:
- partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
+ partner.event_count = self.env['event.event'].sudo().search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
@api.multi
def action_event_view(self):
diff --git a/addons/event/report/event_event_templates.xml b/addons/event/report/event_event_templates.xml
index 76b64138f44..8490dc4aaca 100644
--- a/addons/event/report/event_event_templates.xml
+++ b/addons/event/report/event_event_templates.xml
@@ -18,7 +18,7 @@
<img t-att-src="'data:image/png;base64,%s' % to_text(o.event_id.organizer_id.company_id.logo_web)" style="max-height:1cm; max-width:4cm;"/>
</span>
<h5 t-field="o.event_id.name"/>
- <h5>( <i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="o.event_id.date_begin" t-options='{"hide_seconds": True}'> </span> <i>to</i> <span itemprop="endDate" t-field="o.event_id.date_end" t-options='{"hide_seconds": True}'> </span> )</h5>
+ <h5>( <i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="o.event_id.with_context(tz=o.event_id.date_tz).date_begin" t-options='{"hide_seconds": True}'> </span> <i>to</i> <span itemprop="endDate" t-field="o.event_id.with_context(tz=o.event_id.date_tz).date_end" t-options='{"hide_seconds": True}'> </span> )</h5>
</div>
<div class="row">
<div class="col-xs-12 text-center" id="o_event_name">
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 2ab11a91a82..ee9ab678a47 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -59,7 +59,7 @@ class HolidaysRequest(models.Model):
number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange')
meeting_id = fields.Many2one('calendar.event', string='Meeting')
- parent_id = fields.Many2one('hr.leave', string='Parent')
+ parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
linked_request_ids = fields.One2many('hr.leave', 'parent_id', string='Linked Requests')
department_id = fields.Many2one('hr.department', string='Department', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True,
diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js
index 4e9a572fc82..28aa528fe25 100644
--- a/addons/mail/static/src/js/systray.js
+++ b/addons/mail/static/src/js/systray.js
@@ -128,7 +128,7 @@ var MessagingMenu = Widget.extend({
if (channelID === 'channel_inbox') {
var resID = $(event.currentTarget).data('res_id');
var resModel = $(event.currentTarget).data('res_model');
- if (resModel && resID) {
+ if (resModel && resModel !== 'mail.channel' && resID) {
this.do_action({
type: 'ir.actions.act_window',
res_model: resModel,
@@ -136,7 +136,11 @@ var MessagingMenu = Widget.extend({
res_id: resID
});
} else {
- this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
+ var clientChatOptions = {clear_breadcrumbs: true};
+ if (resModel && resModel === 'mail.channel' && resID) {
+ clientChatOptions.active_id = resID;
+ }
+ this.do_action('mail.mail_channel_action_client_chat', clientChatOptions)
.then(function () {
self.trigger_up('hide_home_menu'); // we cannot 'go back to previous page' otherwise
core.bus.trigger('change_menu_section', self.call('chat_manager', 'getDiscussMenuID'));
diff --git a/addons/mass_mailing/static/src/js/mass_mailing.js b/addons/mass_mailing/static/src/js/mass_mailing.js
index e611d9dbe26..e1689b69e5b 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing.js
@@ -41,7 +41,7 @@ FieldTextHtml.include({
var datarecord = this._super();
if (this.model === 'mail.mass_mailing') {
// these fields can potentially get very long, let's remove them
- datarecord = _.omit(datarecord, ['mailing_domain', 'contact_list_ids', 'body_html']);
+ datarecord = _.omit(datarecord, ['mailing_domain', 'contact_list_ids', 'body_html', 'attachment_ids']);
}
return datarecord;
},
diff --git a/addons/mass_mailing_event_track/models/event.py b/addons/mass_mailing_event_track/models/event.py
index 156625391c0..e5747875f21 100644
--- a/addons/mass_mailing_event_track/models/event.py
+++ b/addons/mass_mailing_event_track/models/event.py
@@ -16,7 +16,7 @@ class Event(models.Model):
view_mode='form',
target='current',
context=dict(
- default_mailing_model=self.env.ref('website_event_track.model_event_track').id,
+ default_mailing_model_id=self.env.ref('website_event_track.model_event_track').id,
default_mailing_domain="[('event_id', 'in', %s), ('stage_id.is_cancel', '!=', True)]" % self.ids,
),
)
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index 5cdd1eace62..aea5ab08a6a 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -80,6 +80,8 @@ class MrpBom(models.Model):
def onchange_product_tmpl_id(self):
if self.product_tmpl_id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
+ if self.product_id.product_tmpl_id != self.product_tmpl_id:
+ self.product_id = False
@api.onchange('routing_id')
def onchange_routing_id(self):
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 945b2b7436b..7eb98e72c7d 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -364,6 +364,7 @@ class MrpWorkorder(models.Model):
move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
if move_line:
move_line.product_uom_qty += self.qty_producing
+ move_line.qty_done += self.qty_producing
else:
move_line.create({'move_id': production_move.id,
'product_id': production_move.product_id.id,
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 1099d8b549e..befe9ceb9d1 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -73,6 +73,13 @@ class StockMove(models.Model):
order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
+ def _unreserve_initial_demand(self, new_move):
+ # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
+ self.filtered(lambda m: m.production_id or m.raw_material_production_id)\
+ .mapped('move_line_ids')\
+ .filtered(lambda ml: ml.qty_done == 0.0)\
+ .write({'move_id': new_move, 'product_uom_qty': 0})
+
@api.depends('active_move_line_ids.qty_done', 'active_move_line_ids.product_uom_id')
def _compute_done_quantity(self):
super(StockMove, self)._compute_done_quantity()
diff --git a/addons/mrp/report/mrp_bom_structure_report.py b/addons/mrp/report/mrp_bom_structure_report.py
index a98f45b4754..c497d562f89 100644
--- a/addons/mrp/report/mrp_bom_structure_report.py
+++ b/addons/mrp/report/mrp_bom_structure_report.py
@@ -7,36 +7,49 @@ from odoo import api, models
class BomStructureReport(models.AbstractModel):
_name = 'report.mrp.mrp_bom_structure_report'
- def get_children(self, object, level=0):
+ @api.model
+ def _get_child_vals(self, record, level, qty, uom):
+ """Get bom.line values.
+
+ :param record: mrp.bom.line record
+ :param level: level of recursion
+ :param qty: quantity of the product
+ :param uom: unit of measurement of a product
+ """
+ child = {
+ 'pname': record.product_id.name_get()[0][1],
+ 'pcode': record.product_id.default_code,
+ 'puom': record.product_uom_id,
+ 'uname': record.product_uom_id.name,
+ 'level': level,
+ 'code': record.bom_id.code,
+ }
+ qty_per_bom = record.bom_id.product_qty
+ if uom:
+ if uom != record.bom_id.product_uom_id:
+ qty = uom._compute_quantity(qty, record.bom_id.product_uom_id)
+ child['pqty'] = (record.product_qty * qty) / qty_per_bom
+ else:
+ # for the first case, the ponderation is right
+ child['pqty'] = (record.product_qty * qty)
+ return child
+
+ def get_children(self, records, level=0):
result = []
- def _get_rec(object, level, qty=1.0, uom=False):
- for l in object:
- res = {}
- res['pname'] = l.product_id.name_get()[0][1]
- res['pcode'] = l.product_id.default_code
- qty_per_bom = l.bom_id.product_qty
- if uom:
- if uom != l.bom_id.product_uom_id:
- qty = uom._compute_quantity(qty, l.bom_id.product_uom_id)
- res['pqty'] = (l.product_qty *qty)/ qty_per_bom
- else:
- #for the first case, the ponderation is right
- res['pqty'] = (l.product_qty *qty)
- res['puom'] = l.product_uom_id
- res['uname'] = l.product_uom_id.name
- res['level'] = level
- res['code'] = l.bom_id.code
- result.append(res)
+ def _get_rec(records, level, qty=1.0, uom=False):
+ for l in records:
+ child = self._get_child_vals(l, level, qty, uom)
+ result.append(child)
if l.child_line_ids:
if level < 6:
level += 1
- _get_rec(l.child_line_ids, level, qty=res['pqty'], uom=res['puom'])
+ _get_rec(l.child_line_ids, level, qty=child['pqty'], uom=child['puom'])
if level > 0 and level < 6:
level -= 1
return result
- children = _get_rec(object, level)
+ children = _get_rec(records, level)
return children
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index 8add57293ae..a764585eede 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -196,7 +196,7 @@
<label for="duration"/>
<div>
<button style="pointer-events: none;" class="oe_inline label label-default">
- <field name="duration" widget="mrp_time_counter"/>
+ <field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
</button>
</div>
</group>
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index fe17ee4add9..89aa33aa249 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -59,6 +59,15 @@ class PaymentAcquirerAuthorize(models.Model):
@api.multi
def authorize_form_generate_values(self, values):
self.ensure_one()
+ # State code is only supported in US, use state name by default
+ # See https://developer.authorize.net/api/reference/
+ state = values['partner_state'].name if values.get('partner_state') else ''
+ if values.get('partner_country') and values.get('partner_country') == self.env.ref('base.us', False):
+ state = values['partner_state'].code if values.get('partner_state') else ''
+ billing_state = values['billing_partner_state'].name if values.get('billing_partner_state') else ''
+ if values.get('billing_partner_country') and values.get('billing_partner_country') == self.env.ref('base.us', False):
+ billing_state = values['billing_partner_state'].code if values.get('billing_partner_state') else ''
+
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
authorize_tx_values = dict(values)
temp_authorize_tx_values = {
@@ -83,7 +92,7 @@ class PaymentAcquirerAuthorize(models.Model):
'first_name': values.get('partner_first_name'),
'last_name': values.get('partner_last_name'),
'phone': values.get('partner_phone'),
- 'state': values.get('partner_state') and values['partner_state'].code or '',
+ 'state': state,
'billing_address': values.get('billing_partner_address'),
'billing_city': values.get('billing_partner_city'),
'billing_country': values.get('billing_partner_country') and values.get('billing_partner_country').name or '',
@@ -92,7 +101,7 @@ class PaymentAcquirerAuthorize(models.Model):
'billing_first_name': values.get('billing_partner_first_name'),
'billing_last_name': values.get('billing_partner_last_name'),
'billing_phone': values.get('billing_partner_phone'),
- 'billing_state': values.get('billing_partner_state') and values['billing_partner_state'].code or '',
+ 'billing_state': billing_state,
}
temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '')
temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values)
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index d9f38720fd4..f219847c8d7 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -38,8 +38,8 @@ class PosSession(models.Model):
paid=order.amount_paid,
))
order.action_pos_order_done()
- orders = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'])
- orders.sudo()._reconcile_payments()
+ orders_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id)
+ orders_to_reconcile.sudo()._reconcile_payments()
config_id = fields.Many2one(
'pos.config', string='Point of Sale',
diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py
index 1caded3870e..7c9b4c954fb 100644
--- a/addons/point_of_sale/models/product.py
+++ b/addons/point_of_sale/models/product.py
@@ -27,6 +27,18 @@ class ProductTemplate(models.Model):
self.available_in_pos = False
+class ProductProduct(models.Model):
+ _inherit = 'product.product'
+
+ @api.multi
+ def unlink(self):
+ product_ctx = dict(self.env.context or {}, active_test=False)
+ if self.env['pos.session'].search_count([('state', '!=', 'closed')]):
+ if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('product_tmpl_id.available_in_pos', '=', True)]):
+ raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
+ return super(ProductProduct, self).unlink()
+
+
class ProductUomCateg(models.Model):
_inherit = 'product.uom.categ'
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 3a636bc3225..4cd1dd2ddb8 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1406,6 +1406,7 @@ exports.Orderline = Backbone.Model.extend({
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
this.order.fix_tax_included_price(this);
}
+ this.trigger('change', this);
},
// return the quantity of product
get_quantity: function(){
@@ -2124,7 +2125,7 @@ exports.Order = Backbone.Model.extend({
company_registry: company.company_registry,
contact_address: company.partner_id[1],
vat: company.vat,
- vat_label: company.country.vat_label,
+ vat_label: company.country && company.country.vat_label || '',
name: company.name,
phone: company.phone,
logo: this.pos.company_logo_base64,
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index 9fb2c6b94ac..89dbded3676 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -100,7 +100,7 @@ class PortalChatter(http.Controller):
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
- domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
+ domain = expression.AND([['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index c96eb169915..18867d31a6d 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -134,7 +134,7 @@ class PortalWizardUser(models.TransientModel):
if wizard_user.partner_id.company_id:
company_id = wizard_user.partner_id.company_id.id
else:
- company_id = self.env['res.company']._company_default_get('res.users')
+ company_id = self.env['res.company']._company_default_get('res.users').id
user_portal = wizard_user.sudo().with_context(company_id=company_id)._create_user()
else:
user_portal = user
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index b00eaa33f62..9b28912fecc 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -222,6 +222,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
self.code = supplier_info.product_code or self.default_code
+ break
else:
self.code = self.default_code
@@ -230,6 +231,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
product_name = supplier_info.product_name or self.default_code
+ break
self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name)
else:
self.partner_ref = self.name_get()[0][1]
diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py
index e47297da52f..0b319b6d8ce 100644
--- a/addons/product/models/product_attribute.py
+++ b/addons/product/models/product_attribute.py
@@ -4,6 +4,7 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
+from odoo.osv import expression
class ProductAttribute(models.Model):
@@ -107,6 +108,6 @@ class ProductAttributeLine(models.Model):
# search on a m2o and one on a m2m, probably this will quickly become
# difficult to compute - check if performance optimization is required
if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
- args = ['|', ('attribute_id', operator, name), ('value_ids', operator, name)]
+ args = expression.AND([['|', ('attribute_id', operator, name), ('value_ids', operator, name)], args])
return self.search(args, limit=limit).name_get()
return super(ProductAttributeLine, self).name_search(name=name, args=args, operator=operator, limit=limit)
diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py
index cb24f04241d..c07bca51701 100644
--- a/addons/product/tests/__init__.py
+++ b/addons/product/tests/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import test_variants, test_uom, test_pricelist, test_product_pricelist
+from . import test_seller, test_variants, test_uom, test_pricelist, test_product_pricelist
diff --git a/addons/product/tests/test_seller.py b/addons/product/tests/test_seller.py
new file mode 100644
index 00000000000..36693317c34
--- /dev/null
+++ b/addons/product/tests/test_seller.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestSeller(TransactionCase):
+
+ def setUp(self):
+ super(TestSeller, self).setUp()
+ self.product_service = self.env.ref('product.product_product_2')
+ self.product_service.default_code = 'DEFCODE'
+ self.asustec = self.env.ref('base.res_partner_1')
+ self.camptocamp = self.env.ref('base.res_partner_12')
+
+ def test_10_sellers(self):
+ self.product_service.write({'seller_ids': [
+ (0, 0, {'name': self.asustec.id, 'product_code': 'ASUCODE'}),
+ (0, 0, {'name': self.camptocamp.id, 'product_code': 'C2CCODE'}),
+ ]})
+
+ default_code = self.product_service.code
+ self.assertEqual("DEFCODE", default_code, "Default code not used in product name")
+
+ context_code = self.product_service\
+ .with_context(partner_id=self.camptocamp.id)\
+ .code
+ self.assertEqual('C2CCODE', context_code, "Partner's code not used in product name with context set")
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 642fb37e62c..70fad5e6152 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -886,7 +886,7 @@ class Task(models.Model):
groups = [new_group] + groups
for group_name, group_method, group_data in groups:
- if group_name in ['customer', 'portal']:
+ if group_name == 'customer':
continue
group_data['has_button_access'] = True
diff --git a/addons/purchase/views/purchase_template.xml b/addons/purchase/views/purchase_template.xml
index 71d77830d6c..4c5c01f4ee7 100644
--- a/addons/purchase/views/purchase_template.xml
+++ b/addons/purchase/views/purchase_template.xml
@@ -5,7 +5,7 @@
<div>
<strong>The ordered quantity has been updated.</strong>
<ul>
- <li><t t-esc="line.order_id.product_id.display_name"/>:</li>
+ <li><t t-esc="line.product_id.display_name"/>:</li>
Ordered Quantity: <t t-esc="line.product_qty" /> -&gt; <t t-esc="float(product_qty)"/><br/>
<t t-if='line.order_id.product_id.type in ("consu", "product")'>
Received Quantity: <t t-esc="line.qty_received" /><br/>
diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py
index dd7c7ddf5f2..a54e96975ab 100644
--- a/addons/repair/models/repair.py
+++ b/addons/repair/models/repair.py
@@ -200,9 +200,11 @@ class Repair(models.Model):
def action_validate(self):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
- if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
- return self.action_repair_confirm()
+ available_qty_owner = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, owner_id=self.partner_id, strict=True)
+ available_qty_noown = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
+ for available_qty in [available_qty_owner, available_qty_noown]:
+ if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
+ return self.action_repair_confirm()
else:
return {
'name': _('Insufficient Quantity'),
@@ -428,8 +430,15 @@ class Repair(models.Model):
if self.filtered(lambda repair: not repair.repaired):
raise UserError(_("Repair must be repaired in order to make the product moves."))
res = {}
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
Move = self.env['stock.move']
for repair in self:
+ # Try to create move with the appropriate owner
+ owner_id = False
+ available_qty_owner = self.env['stock.quant']._get_available_quantity(repair.product_id, repair.location_id, repair.lot_id, owner_id=repair.partner_id, strict=True)
+ if float_compare(available_qty_owner, repair.product_qty, precision_digits=precision) >= 0:
+ owner_id = repair.partner_id.id
+
moves = self.env['stock.move']
for operation in repair.operations:
move = Move.create({
@@ -447,6 +456,7 @@ class Repair(models.Model):
'qty_done': operation.product_uom_qty,
'package_id': False,
'result_package_id': False,
+ 'owner_id': owner_id,
'location_id': operation.location_id.id, #TODO: owner stuff
'location_dest_id': operation.location_dest_id.id,})],
'repair_id': repair.id,
@@ -469,6 +479,7 @@ class Repair(models.Model):
'qty_done': repair.product_qty,
'package_id': False,
'result_package_id': False,
+ 'owner_id': owner_id,
'location_id': repair.location_id.id, #TODO: owner stuff
'location_dest_id': repair.location_id.id,})],
'repair_id': repair.id,
diff --git a/addons/repair/report/repair_templates_repair_order.xml b/addons/repair/report/repair_templates_repair_order.xml
index 210a6158245..04317ac6ad0 100644
--- a/addons/repair/report/repair_templates_repair_order.xml
+++ b/addons/repair/report/repair_templates_repair_order.xml
@@ -2,7 +2,7 @@
<odoo>
<data>
<template id="report_repairorder">
- <t t-foreach="docs" t-as="o">
+ <t t-set="o" t-value="doc"/>
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context({'lang': o.partner_id.lang})" />
<div class="page">
@@ -155,7 +155,6 @@
<div class="oe_structure"/>
</div>
</t>
- </t>
</template>
<template id="report_repairorder2">
diff --git a/addons/sale/report/report_all_channels_sales_views.xml b/addons/sale/report/report_all_channels_sales_views.xml
index 793b568528e..201e229a186 100644
--- a/addons/sale/report/report_all_channels_sales_views.xml
+++ b/addons/sale/report/report_all_channels_sales_views.xml
@@ -4,7 +4,7 @@
<field name="name">report.all.channels.sales.pivot</field>
<field name="model">report.all.channels.sales</field>
<field name="arch" type="xml">
- <pivot string="Sales by Channel Analysis">
+ <pivot string="Sales by Channel Analysis" disable_linking="True">
<field name="name" type="row"/>
<field name="price_total" string="Total Price" type="measure"/>
</pivot>
diff --git a/addons/sale_timesheet/__init__.py b/addons/sale_timesheet/__init__.py
index 8c67f893b37..fd389b7dde8 100644
--- a/addons/sale_timesheet/__init__.py
+++ b/addons/sale_timesheet/__init__.py
@@ -1,5 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, SUPERUSER_ID
+
from . import models
from . import controllers
+
+
+def uninstall_hook(cr, registry):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ env['product.template'].search([
+ ('service_type', '=', 'timesheet')
+ ]).write({'service_type': 'manual'})
+ env['product.product'].search([
+ ('service_type', '=', 'timesheet')
+ ]).write({'service_type': 'manual'})
diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py
index 0cb9db63bae..b043d1154f0 100644
--- a/addons/sale_timesheet/__manifest__.py
+++ b/addons/sale_timesheet/__manifest__.py
@@ -30,4 +30,5 @@ have real delivered quantities in sales orders.
'data/sale_service_demo.xml',
],
'auto_install': True,
+ 'uninstall_hook': 'uninstall_hook',
}
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 6373600d9b0..39958e6e616 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -98,13 +98,13 @@ class Product(models.Model):
domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc
domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc
- if lot_id:
+ if lot_id is not None:
domain_quant += [('lot_id', '=', lot_id)]
- if owner_id:
+ if owner_id is not None:
domain_quant += [('owner_id', '=', owner_id)]
domain_move_in += [('restrict_partner_id', '=', owner_id)]
domain_move_out += [('restrict_partner_id', '=', owner_id)]
- if package_id:
+ if package_id is not None:
domain_quant += [('package_id', '=', package_id)]
if dates_in_the_past:
domain_move_in_done = list(domain_move_in)
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index d88fbca0ceb..97a9082d776 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1074,6 +1074,9 @@ class StockMove(models.Model):
break
return extra_move
+ def _unreserve_initial_demand(self, new_move):
+ pass
+
def _action_done(self):
self.filtered(lambda move: move.state == 'draft')._action_confirm() # MRP allows scrapping draft moves
@@ -1114,9 +1117,7 @@ class StockMove(models.Model):
move_line.write({'product_uom_qty': move_line.qty_done})
except UserError:
pass
-
- # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
- move.move_line_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move, 'product_uom_qty': 0})
+ move._unreserve_initial_demand(new_move)
move.move_line_ids._action_done()
# Check the consistency of the result packages; there should be an unique location across
# the contained quants.
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index 8326328bc95..53948b477f9 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -1753,16 +1753,27 @@ class StockMove(TransactionCase):
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
backorder_wizard.with_context(debug=True).process()
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
- backordered_ml = backorder.move_line_ids
- quant_lot3 = self.env['stock.quant'].search([('lot_id', '=', backordered_ml.lot_id.id)])
+ backordered_move = backorder.move_lines
# due to the rounding, the backordered quantity is 0.999 ; we shoudln't be able to reserve
# 0.999 on a tracked by serial number quant
- self.assertEqual(backordered_ml.product_uom_qty, 0)
- self.assertTrue(backordered_ml.lot_id)
+ backordered_move._action_assign()
+ self.assertEqual(backordered_move.reserved_availability, 0)
# force the serial number and validate
- backordered_ml.qty_done = 1
+ lot3 = self.env['stock.production.lot'].search([('name', '=', "lot3")])
+ backorder.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product2.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': lot3.id,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': backordered_move.location_id.id,
+ 'location_dest_id': backordered_move.location_dest_id.id,
+ 'move_id': backordered_move.id,
+ })]})
overprocessed_wizard = backorder.button_validate()
overprocessed_wizard = self.env['stock.overprocessed.transfer'].browse(overprocessed_wizard['res_id'])
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index 10832aa8bde..2c028bd4fc2 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -198,39 +198,45 @@ class ProductProduct(models.Model):
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
for product in self:
- if product.cost_method in ['standard', 'average']:
- qty_available = product.with_context(company_owned=True).qty_available
- price_used = product.standard_price
- if to_date:
- price_used = product.get_history_price(
- self.env.user.company_id.id,
- date=to_date,
- )
- product.stock_value = price_used * qty_available
- product.qty_at_date = qty_available
- elif product.cost_method == 'fifo':
- if to_date:
- if product.product_tmpl_id.valuation == 'manual_periodic':
- domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
- moves = StockMove.search(domain)
+ if to_date:
+ price_used = product.get_history_price(
+ self.env.user.company_id.id,
+ date=to_date,
+ ) if product.cost_method in ['standard', 'average'] else 0.0
+ if product.product_tmpl_id.valuation == 'manual_periodic':
+ domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
+ moves = StockMove.search(domain)
+ product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
+ if product.cost_method == 'fifo':
product.stock_value = sum(moves.mapped('value'))
- product.qty_at_date = product.with_context(company_owned=True).qty_available
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
- elif product.product_tmpl_id.valuation == 'real_time':
- valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
- value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
+ elif product.cost_method in ['standard', 'average']:
+ product.stock_value = product.qty_at_date * price_used
+ elif product.product_tmpl_id.valuation == 'real_time':
+ valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
+ value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
+ product.qty_at_date = quantity
+ if product.cost_method == 'fifo':
product.stock_value = value
- product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
- else:
- product.stock_value, moves = product._sum_remaining_values()
- product.qty_at_date = product.with_context(company_owned=True).qty_available
- if product.product_tmpl_id.valuation == 'manual_periodic':
- product.stock_fifo_manual_move_ids = moves
- elif product.product_tmpl_id.valuation == 'real_time':
- valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
- value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
+ elif product.cost_method in ['standard', 'average']:
+ product.stock_value = quantity * price_used
+ else:
+ if product.product_tmpl_id.valuation == 'manual_periodic':
+ product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
+ if product.cost_method == 'fifo':
+ product.stock_value, product.stock_fifo_manual_move_ids = product._sum_remaining_values()
+ elif product.cost_method in ['standard', 'average']:
+ product.stock_value = product.qty_at_date * product.standard_price
+ elif product.product_tmpl_id.valuation == 'real_time':
+ valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
+ value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
+ product.qty_at_date = quantity
+ if product.cost_method == 'fifo':
+ product.stock_value = value
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
+ elif product.cost_method in ['standard', 'average']:
+ product.stock_value = quantity * product.standard_price
def action_valuation_at_date_details(self):
""" Returns an action with either a list view of all the valued stock moves of `self` if the
@@ -401,4 +407,3 @@ class ProductCategory(models.Model):
'message': _("Changing your cost method is an important change that will impact your inventory valuation. Are you sure you want to make that change?"),
}
}
-
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 2a7cb101c4e..af38ff50027 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -581,7 +581,8 @@ class StockMove(models.Model):
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id):
self.ensure_one()
AccountMove = self.env['account.move']
- quantity = self.env.context.get('forced_quantity', self.product_qty if self._is_in() else -1 * self.product_qty)
+ quantity = self.env.context.get('forced_quantity', self.product_qty)
+ quantity = quantity if self._is_in() else -1 * quantity
# Make an informative `ref` on the created account move to differentiate between classic
# movements, vacuum and edition of past moves.
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index 0450d66e9a1..ce88777d13d 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -2046,6 +2046,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertAlmostEqual(self.product1.qty_available, 10.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# ---------------------------------------------------------------------
@@ -2088,6 +2090,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move2.account_move_ids), 1)
+ self.assertAlmostEqual(self.product1.qty_available, 20.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 20.0)
self.assertEqual(self.product1.stock_value, 220)
# ---------------------------------------------------------------------
@@ -2132,6 +2136,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move3.account_move_ids), 1)
+ self.assertAlmostEqual(self.product1.qty_available, 12.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 12.0)
self.assertEqual(self.product1.stock_value, 140)
# ---------------------------------------------------------------------
@@ -2165,6 +2171,7 @@ class TestStockValuation(TransactionCase):
# Ending
# ---------------------------------------------------------------------
self.assertEqual(self.product1.qty_available, 6)
+ self.assertAlmostEqual(self.product1.qty_at_date, 6.0)
self.assertEqual(self.product1.stock_value, 72)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 220)
@@ -2520,8 +2527,33 @@ class TestStockValuation(TransactionCase):
move2.move_line_ids.qty_done = 1.0
move2._action_done()
+ self.assertAlmostEqual(self.product1.qty_available, 2.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 2.0)
self.assertAlmostEqual(self.product1.standard_price, 7.5)
+ def test_average_perpetual_5(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
+ self.assertAlmostEqual(self.product1.stock_value, 0.0)
+
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
@@ -2810,6 +2842,77 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move7.value, 100.0)
self.assertEqual(self.product1.standard_price, 10)
+ def test_average_manual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.product_tmpl_id.cost_method = 'average'
+ self.product1.product_tmpl_id.valuation = 'manual_periodic'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
+ self.assertAlmostEqual(self.product1.stock_value, 0.0)
+
+ def test_standard_perpetual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.product_tmpl_id.cost_method = 'standard'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
+ self.assertAlmostEqual(self.product1.stock_value, 0.0)
+
+ def test_standard_manual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.valuation = 'manual_periodic'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
+ self.assertAlmostEqual(self.product1.stock_value, 0.0)
+
def test_change_cost_method_1(self):
""" Change the cost method from FIFO to AVCO.
"""
@@ -2862,6 +2965,8 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 1.0
move3._action_done()
+ self.assertAlmostEqual(self.product1.qty_available, 19)
+ self.assertAlmostEqual(self.product1.qty_at_date, 19)
self.assertEqual(self.product1.stock_value, 240)
# ---------------------------------------------------------------------
@@ -2930,6 +3035,8 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 1.0
move3._action_done()
+ self.assertAlmostEqual(self.product1.qty_available, 19)
+ self.assertAlmostEqual(self.product1.qty_at_date, 19)
self.assertEqual(self.product1.stock_value, 240)
# ---------------------------------------------------------------------
@@ -3003,6 +3110,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move1.value, 10)
self.assertEqual(move1.remaining_value, 10)
self.assertEqual(move1.remaining_qty, 1)
+ self.assertAlmostEqual(self.product1.qty_available, 0.0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 2.0)
self.assertEqual(self.product1.stock_value, 10)
self.assertTrue(len(move1.account_move_ids), 1)
@@ -3152,8 +3261,10 @@ class TestStockValuation(TransactionCase):
move1.move_line_ids.qty_done = 10
move1._action_done()
move1.date = date2
+ move1.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 10)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 20
@@ -3170,8 +3281,10 @@ class TestStockValuation(TransactionCase):
move2.move_line_ids.qty_done = 20
move2._action_done()
move2.date = date3
+ move2.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, 30)
+ self.assertAlmostEqual(self.product1.qty_at_date, 30.0)
self.assertEqual(self.product1.stock_value, 300)
# send 15
@@ -3188,8 +3301,10 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 15
move3._action_done()
move3.date = date4
+ move3.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, 15)
+ self.assertAlmostEqual(self.product1.qty_at_date, 15.0)
self.assertEqual(self.product1.stock_value, 150)
# set the standard price to 5
@@ -3197,6 +3312,7 @@ class TestStockValuation(TransactionCase):
self.env['product.price.history'].search([('product_id', '=', self.product1.id)], order='datetime desc, id DESC', limit=1).datetime = date5
self.assertEqual(self.product1.qty_available, 15)
+ self.assertAlmostEqual(self.product1.qty_at_date, 15.0)
self.assertEqual(self.product1.stock_value, 75)
# send 20
@@ -3213,8 +3329,10 @@ class TestStockValuation(TransactionCase):
move4.move_line_ids.qty_done = 20
move4._action_done()
move4.date = date6
+ move4.account_move_ids.write({'date': date6})
self.assertEqual(self.product1.qty_available, -5)
+ self.assertAlmostEqual(self.product1.qty_at_date, -5.0)
self.assertEqual(self.product1.stock_value, -25)
# set the standard price to 7.5
@@ -3235,11 +3353,13 @@ class TestStockValuation(TransactionCase):
move5.move_line_ids.qty_done = 100
move5._action_done()
move5.date = date8
+ move5.account_move_ids.write({'date': date8})
self.assertEqual(self.product1.qty_available, 95)
+ self.assertAlmostEqual(self.product1.qty_at_date, 95.0)
self.assertEqual(self.product1.stock_value, 712.5)
- # Quantity at date
+ # Quantity available at date
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_available, 0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_available, 30)
@@ -3257,12 +3377,25 @@ class TestStockValuation(TransactionCase):
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).stock_value, 712.5)
+ # Quantity at date
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 0.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 30.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 15.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 15.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_at_date, -5.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_at_date, 95.0)
+
# edit the done quantity of move1, decrease it
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0)
move1.quantity_done = 5
+ move1.account_move_ids.write({'date': date2})
# the quantity at date will reflect the change directly
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 5)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 5.0)
# as when we decrease a quantity on a recreipt, we consider it as a out move with the price
# of today, the value will be decrease of 100 - (5*7.5)
@@ -3274,12 +3407,17 @@ class TestStockValuation(TransactionCase):
# edit move 4, send 15 instead of 20
# we now have +5 + 20 - 15 -20 = -10 * a standard price of 5
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -10.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -10.0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -50)
move4.quantity_done = 15
+ move4.account_move_ids.write({'date': date6})
# -(20*5) + (5*7.5)
self.assertEqual(move4.value, -62.5)
# we now have +5 + 20 - 15 -15 = -5 * a standard price of 5
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5.0)
+ self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25)
def test_at_date_fifo_1(self):
@@ -3315,6 +3453,7 @@ class TestStockValuation(TransactionCase):
move1.account_move_ids.write({'date': date1})
self.assertEqual(self.product1.qty_available, 10)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 10@12
@@ -3335,6 +3474,7 @@ class TestStockValuation(TransactionCase):
move2.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 20)
+ self.assertAlmostEqual(self.product1.qty_at_date, 20)
self.assertEqual(self.product1.stock_value, 220)
# send 15
@@ -3354,6 +3494,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, 5)
+ self.assertAlmostEqual(self.product1.qty_at_date, 5.0)
self.assertEqual(self.product1.stock_value, 60)
# send 20
@@ -3373,6 +3514,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, -15)
+ self.assertAlmostEqual(self.product1.qty_at_date, -15.0)
self.assertEqual(self.product1.stock_value, -180)
# receive 100@15
@@ -3393,6 +3535,7 @@ class TestStockValuation(TransactionCase):
move5.account_move_ids.write({'date': date5})
self.assertEqual(self.product1.qty_available, 85)
+ self.assertAlmostEqual(self.product1.qty_at_date, 85.0)
self.assertEqual(self.product1.stock_value, 1320)
# run the vacuum to compensate the negative stock move
@@ -3400,6 +3543,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids[0].write({'date': date6})
self.assertEqual(self.product1.qty_available, 85)
+ self.assertAlmostEqual(self.product1.qty_at_date, 85.0)
self.assertEqual(self.product1.stock_value, 1275)
# Edit the quantity done of move1, increase it.
@@ -3472,6 +3616,7 @@ class TestStockValuation(TransactionCase):
move1.account_move_ids.write({'date': date1})
self.assertEqual(self.product1.qty_available, 10)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 10@15
@@ -3492,6 +3637,7 @@ class TestStockValuation(TransactionCase):
move2.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 20)
+ self.assertAlmostEqual(self.product1.qty_at_date, 20.0)
self.assertEqual(self.product1.stock_value, 250)
# send 30
@@ -3511,6 +3657,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, -10)
+ self.assertAlmostEqual(self.product1.qty_at_date, -10.0)
self.assertEqual(self.product1.stock_value, -150)
# receive 10@20
@@ -3531,6 +3678,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, 0)
+ self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertEqual(self.product1.stock_value, 50)
# receive 10@10
@@ -3551,6 +3699,7 @@ class TestStockValuation(TransactionCase):
move5.account_move_ids.write({'date': date5})
self.assertEqual(self.product1.qty_available, 10)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 150)
# run the vacuum to compensate the negative stock move
@@ -3558,6 +3707,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids[0].write({'date': date6})
self.assertEqual(self.product1.qty_available, 10)
+ self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# ---------------------------------------------------------------------
diff --git a/addons/website/models/ir_model.py b/addons/website/models/ir_model.py
index 6d254374c38..84c6a0b1836 100644
--- a/addons/website/models/ir_model.py
+++ b/addons/website/models/ir_model.py
@@ -10,6 +10,6 @@ class IrModel(models.Model):
@api.multi
def unlink(self):
- self.env.cr.execute(
- "DELETE FROM ir_model_fields WHERE name='website_id'")
+ self.env.cr.execute("DELETE FROM ir_model_fields WHERE name='website_id'")
+ self.env.cr.execute("DELETE FROM res_config_settings WHERE website_id IS NOT NULL")
return super(IrModel, self).unlink()
diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js
index ebe38118559..53a304c707e 100644
--- a/addons/website_event/static/src/js/website_event.js
+++ b/addons/website_event/static/src/js/website_event.js
@@ -59,12 +59,16 @@ var EventRegistrationForm = Widget.extend({
$button.attr('disabled', true);
return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) {
var $modal = $(modal);
+ $modal.modal({backdrop: 'static', keyboard: false});
$modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19
$modal.insertAfter($form).modal();
$modal.on('click', '.js_goto_event', function () {
$modal.modal('hide');
$button.prop('disabled', false);
});
+ $modal.on('click', '.close', function () {
+ $button.prop('disabled', false);
+ });
});
}
},
diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml
index 70ed14e680a..dbfceeb8ca6 100644
--- a/addons/website_event_track/views/event_track_templates.xml
+++ b/addons/website_event_track/views/event_track_templates.xml
@@ -50,8 +50,8 @@
<input type="text" class="form-control" placeholder="Filter Tracks..." id="event_track_search"/>
</div>
</section>
- <t t-set="dayslist" t-value="days.keys()"/>
- <t t-set="dayslist2" t-value="list(dayslist).sort()"/> <!-- display days in the right order -->
+ <t t-set="dayslist" t-value="list(days.keys())"/>
+ <t t-set="dayslist2" t-value="dayslist.sort()"/> <!-- display days in the right order -->
<section class="container" t-foreach="dayslist" t-as="day">
<t t-set="locations" t-value="days[day]['locations']"/>
<t t-set="dates" t-value="days[day]['dates']"/>
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 857ea4f0a6e..c10a1d9efc4 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -422,6 +422,11 @@ class Post(models.Model):
raise KarmaError('User karma not sufficient to post an image or link.')
return content
+ @api.constrains('parent_id')
+ def _check_parent_id(self):
+ if not self._check_recursion():
+ raise ValidationError(_('You cannot create recursive forum posts.'))
+
@api.model
def create(self, vals):
if 'content' in vals and vals.get('forum_id'):
diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py
index 753e4eef1ae..31af89c2e63 100644
--- a/addons/website_livechat/controllers/main.py
+++ b/addons/website_livechat/controllers/main.py
@@ -20,13 +20,17 @@ class WebsiteLivechat(http.Controller):
@http.route('/livechat/channel/<model("im_livechat.channel"):channel>', type='http', auth='public', website=True)
def channel_rating(self, channel, **kw):
# get the last 100 ratings and the repartition per grade
- ratings = request.env['rating.rating'].search([('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids)], order='create_date desc', limit=100)
- repartition = channel.sudo().channel_ids.rating_get_grades()
+ domain = [
+ ('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids),
+ ('consumed', '=', True), ('rating', '>=', 1),
+ ]
+ ratings = request.env['rating.rating'].search(domain, order='create_date desc', limit=100)
+ repartition = channel.sudo().channel_ids.rating_get_grades(domain=domain)
# compute percentage
percentage = dict.fromkeys(['great', 'okay', 'bad'], 0)
for grade in repartition:
- percentage[grade] = repartition[grade] * 100 / sum(repartition.values()) if sum(repartition.values()) else 0
+ percentage[grade] = round(repartition[grade] * 100.0 / sum(repartition.values()), 1) if sum(repartition.values()) else 0
# the value dict to render the template
values = {
diff --git a/addons/website_mass_mailing/controllers/main.py b/addons/website_mass_mailing/controllers/main.py
index d1aa21f5cd4..455ab1bdae0 100644
--- a/addons/website_mass_mailing/controllers/main.py
+++ b/addons/website_mass_mailing/controllers/main.py
@@ -11,7 +11,7 @@ class MassMailController(MassMailController):
def mailing(self, mailing_id, email=None, res_id=None, **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
- if mailing.mailing_model_name == 'mail.mass_mailing.contact':
+ if mailing.mailing_model_real == 'mail.mass_mailing.contact':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', '=', email)])
return request.render('website_mass_mailing.page_unsubscribe', {
'contacts': contacts,
diff --git a/addons/website_sale_stock/models/product_product.py b/addons/website_sale_stock/models/product_product.py
index 829efd3f2dc..cfab171bb32 100644
--- a/addons/website_sale_stock/models/product_product.py
+++ b/addons/website_sale_stock/models/product_product.py
@@ -11,7 +11,7 @@ class ProductProduct(models.Model):
cart_qty = fields.Integer(compute='_compute_cart_qty')
def _compute_cart_qty(self):
- website = getattr(request, 'website', None)
+ website = request and getattr(request, 'website', None)
if not website:
return
cart = website.sale_get_order()
diff --git a/addons/website_sale_wishlist/models/product_wishlist.py b/addons/website_sale_wishlist/models/product_wishlist.py
index 6c73cf50608..381d5eceb45 100644
--- a/addons/website_sale_wishlist/models/product_wishlist.py
+++ b/addons/website_sale_wishlist/models/product_wishlist.py
@@ -36,14 +36,15 @@ class ProductWishlist(models.Model):
@api.model
def current(self):
- """Get all wishlist items that belong to current user or session."""
- return self.search([
+ """Get all wishlist items that belong to current user or session,
+ filter products that are unpublished."""
+ return self.sudo().search([
"|",
("partner_id", "=", self.env.user.partner_id.id),
"&",
("partner_id", "=", False),
("session", "=", self.env.user.current_session),
- ])
+ ]).filtered('product_id.product_tmpl_id.website_published')
@api.model
def _add_to_wishlist(self, pricelist_id, currency_id, website_id, price, product_id, partner_id=False, session=False):
diff --git a/doc/cla/corporate/adhoc.md b/doc/cla/corporate/adhoc.md
index 627065bafab..428f46b0056 100644
--- a/doc/cla/corporate/adhoc.md
+++ b/doc/cla/corporate/adhoc.md
@@ -13,3 +13,4 @@ List of contributors:
Damián Soriano ds@adhoc.com.ar https://github.com/damiansoriano
Juan José Scarafía jjs@adhoc.com.ar https://github.com/jjscarafia
Nicolás Mac Rouillon nmr@adhoc.com.ar https://github.com/nicomacr
+Katherine Zaoral kz@adhoc.com.ar https://github.com/zaoral
diff --git a/doc/cla/corporate/modoolar.md b/doc/cla/corporate/modoolar.md
new file mode 100644
index 00000000000..a3ee461fab4
--- /dev/null
+++ b/doc/cla/corporate/modoolar.md
@@ -0,0 +1,19 @@
+Serbia, 2018-06-11
+
+Modoolar 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,
+
+Petar Najman petar.najman@modoolar.com https://github.com/pnajman-modoolar
+
+List of contributors:
+
+- Aleksandar Gajić aleksandar.gajic@modoolar.com https://github.com/agajic-modoolar
+- Igor Jovanović igor.jovanovic@modoolar.com https://github.com/ijovanovic-modoolar
+- Mladen Meseldžija mladen.meseldzija@modoolar.com https://github.com/mmeseldzija-modoolar
+- Petar Najman petar.najman@modoolar.com https://github.com/pnajman-modoolar
+- Slađan Kantar sladjan.kantar@modoolar.com https://github.com/skantar
diff --git a/doc/cla/corporate/shine-it.md b/doc/cla/corporate/shine-it.md
new file mode 100644
index 00000000000..975f9d885f4
--- /dev/null
+++ b/doc/cla/corporate/shine-it.md
@@ -0,0 +1,17 @@
+China, 2018-04-28
+
+Shine 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,
+
+Tony Gu tony@openerp.cn https://github.com/digitalsatori
+
+List of contributors:
+Tony Gu tony@openerp.cn https://github.com/digitalsatori
+Joshua Jan joshua@openerp.cn https://github.com/joshuajan
+Xiao Guo xerxes@openerp.cn https://github.com/xerxesnoPT
+
diff --git a/doc/cla/corporate/tvtmarine-automation.md b/doc/cla/corporate/tvtmarine-automation.md
index de0906f0331..59e3c250ab3 100644
--- a/doc/cla/corporate/tvtmarine-automation.md
+++ b/doc/cla/corporate/tvtmarine-automation.md
@@ -13,7 +13,7 @@ David Tran david.tran@tvtmarine.com https://github.com/tvtma/
List of contributors:
David Tran david.tran@tvtmarine.com https://github.com/davidtranhp
-Long Do dhlong.1209@gmail.com https://github.com/hoanglong87
+Long Do long.do@tvtmarine.com https://github.com/hoanglong87
Hao Hoang hao.hoang@ma.tvtmarine.com https://github.com/hoanghao2001
Tommy Tran tu.tran@tvtmarine.com https://github.com/tutran81
-Emily Ha emily.ha@tvtmarine.com https://github.com/emilyha \ No newline at end of file
+Emily Ha emily.ha@tvtmarine.com https://github.com/emilyha
diff --git a/doc/cla/individual/Zelwak.md b/doc/cla/individual/Zelwak.md
new file mode 100644
index 00000000000..40c8f303d42
--- /dev/null
+++ b/doc/cla/individual/Zelwak.md
@@ -0,0 +1,11 @@
+
+
+France, 2018-06-07
+
+I here by 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,
+
+Flavien CHÊNE me@flavienchene.fr https://github.com/Zelwak
diff --git a/doc/cla/individual/mduvergey.md b/doc/cla/individual/mduvergey.md
new file mode 100644
index 00000000000..3b9422711ec
--- /dev/null
+++ b/doc/cla/individual/mduvergey.md
@@ -0,0 +1,11 @@
+France, 2018-05-24
+
+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,
+
+Martin Duvergey martin.duvergey@gpsante.fr https://github.com/mduvergey
diff --git a/doc/cla/individual/schout-it.md b/doc/cla/individual/schout-it.md
new file mode 100644
index 00000000000..35451db875d
--- /dev/null
+++ b/doc/cla/individual/schout-it.md
@@ -0,0 +1,11 @@
+Belgium, 2018-05-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,
+
+Rod Schouteden rod@schout-it.be https://github.com/schout-it
diff --git a/doc/setup/enterprise.rst b/doc/setup/enterprise.rst
index 49ba8af4c84..66f8fe82671 100644
--- a/doc/setup/enterprise.rst
+++ b/doc/setup/enterprise.rst
@@ -46,7 +46,7 @@ On Linux, using an installer
.. code-block:: console
- $ python3 /usr/bin/odoo.py -d <database_name> -i web_enterprise --stop-after-init
+ $ python3 /usr/bin/odoo-bin -d <database_name> -i web_enterprise --stop-after-init
* You should be able to connect to your Odoo Enterprise instance using your usual mean of identification.
You can then link your database with your Odoo Enterprise Subscription by entering the code you received
diff --git a/odoo/addons/base/data/res.country.state.csv b/odoo/addons/base/data/res.country.state.csv
index 4b445f52cf9..b64c8f78f12 100644
--- a/odoo/addons/base/data/res.country.state.csv
+++ b/odoo/addons/base/data/res.country.state.csv
@@ -450,7 +450,7 @@ state_es_m,es,"Madrid","M"
state_es_ma,es,"Málaga","MA"
state_es_ml,es,"Melilla","ME"
state_es_mu,es,"Murcia","MU"
-state_es_na,es,"Nafarroa (Navarra)","NA"
+state_es_na,es,"Navarra (Nafarroa)","NA"
state_es_or,es,"Ourense (Orense)","OR"
state_es_p,es,"Palencia","P"
state_es_po,es,"Pontevedra","PO"
diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml
index 0e865be0fdf..1648d24430c 100644
--- a/odoo/addons/base/data/res_country_data.xml
+++ b/odoo/addons/base/data/res_country_data.xml
@@ -271,6 +271,7 @@
<field eval="'%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s'" name="address_format" />
<field name="currency_id" ref="CAD" />
<field eval="1" name="phone_code" />
+ <field name="vat_label">HST</field>
</record>
<record id="cc" model="res.country">
<field name="name">Cocos (Keeling) Islands</field>
@@ -440,8 +441,10 @@
<field name="name">Dominican Republic</field>
<field name="code">do</field>
<field file="base/static/img/country_flags/do.png" name="image" type="base64" />
+ <field eval="'%(street)s\n%(street2)s\n%(city)s %(state_name)s %(zip)s\n%(country_name)s'" name="address_format" />
<field name="currency_id" ref="DOP" />
- <field eval="0" name="phone_code" />
+ <field eval="1" name="phone_code" />
+ <field name="vat_label">RNC</field>
</record>
<record id="dz" model="res.country">
<field name="name">Algeria</field>
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index 96edff27263..454e2dc0780 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -22,6 +22,8 @@ from odoo.tools import ustr, pycompat
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
+SMTP_TIMEOUT = 60
+
class MailDeliveryException(except_orm):
"""Specific exception subclass for mail delivery errors"""
@@ -225,9 +227,9 @@ class IrMailServer(models.Model):
"You could use STARTTLS instead. "
"If SSL is needed, an upgrade to Python 2.6 on the server-side "
"should do the trick."))
- connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
+ connection = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
else:
- connection = smtplib.SMTP(smtp_server, smtp_port)
+ connection = smtplib.SMTP(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
connection.set_debuglevel(smtp_debug)
if smtp_encryption == 'starttls':
# starttls() will perform ehlo() if needed first
diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py
index 98697b87738..692db1bf0f3 100644
--- a/odoo/addons/base/models/qweb.py
+++ b/odoo/addons/base/models/qweb.py
@@ -308,8 +308,9 @@ class QWeb(object):
raise e
except Exception as e:
path = _options['last_path_node']
- node = element.getroottree().xpath(path)
- raise QWebException("Error when compiling AST", e, path, etree.tostring(node[0], encoding='unicode'), name)
+ element, document = self.get_template(template, options)
+ node = element.getroottree().xpath(path) if ':' not in path else None
+ raise QWebException("Error when compiling AST", e, path, node and etree.tostring(node[0], encoding='unicode'), name)
astmod.body.extend(_options['ast_calls'])
if 'profile' in options:
@@ -344,7 +345,7 @@ class QWeb(object):
except Exception as e:
path = log['last_path_node']
element, document = self.get_template(template, options)
- node = element.getroottree().xpath(path)
+ node = element.getroottree().xpath(path) if ':' not in path else None
raise QWebException("Error to render compiling AST", e, path, node and etree.tostring(node[0], encoding='unicode'), name)
return _compiled_fn
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 02f9d90562c..204d4751eb6 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -789,6 +789,11 @@ class GroupsView(models.Model):
xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")
+ if not view.check_access_rights('write', raise_exception=False):
+ # erp manager has the rights to update groups/users but not
+ # to modify ir.ui.view
+ if self.env.user.has_group('base.group_erp_manager'):
+ view = view.sudo()
new_context = dict(view._context)
new_context.pop('install_mode_data', None) # don't set arch_fs for this computed view
diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng
index 1dec8e9ab11..dc41ee5005c 100644
--- a/odoo/addons/base/rng/tree_view.rng
+++ b/odoo/addons/base/rng/tree_view.rng
@@ -17,6 +17,8 @@
<rng:optional><rng:attribute name="duplicate"/></rng:optional>
<rng:optional><rng:attribute name="import"/></rng:optional>
<rng:optional><rng:attribute name="class"/></rng:optional>
+ <!-- Allows to take a custom View widget for handling. Defined in view_manager. -->
+ <rng:optional><rng:attribute name="js_class"/></rng:optional>
<rng:optional><rng:attribute name="colors"/></rng:optional>
<rng:optional><rng:attribute name="fonts"/></rng:optional>
<rng:optional><rng:attribute name="default_order"/></rng:optional>
diff --git a/odoo/addons/base/security/base_security.xml b/odoo/addons/base/security/base_security.xml
index f3bf2f908dd..21401d88b47 100644
--- a/odoo/addons/base/security/base_security.xml
+++ b/odoo/addons/base/security/base_security.xml
@@ -78,7 +78,7 @@
<field name="name">res.partner.rule.private.employee</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain_force">
- [('type', '!=', 'private')]
+ ['|', ('type', '!=', 'private'), ('type', '=', False)]
</field>
<field name="groups" eval="[
(4, ref('base.group_user')),
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index bea0c74cabe..310c5220897 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -5,6 +5,7 @@ import collections
import json
import os.path
import re
+import sys
from lxml import etree
@@ -465,6 +466,26 @@ class TestQWebNS(TransactionCase):
expected_result
)
+ def test_render_dynamic_xml_with_code_error(self):
+ """ Test that, when rendering a template containing a namespaced node
+ that evaluates code with errors, the proper exception is raised
+ """
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': u"""
+ <t t-name="base.dummy">
+ <Invoice xmlns:od="http://odoo.com/od">
+ <od:name t-att-test="'a' + 1"/>
+ </Invoice>
+ </t>
+ """
+ })
+ error_msg = "Can't convert 'int' object to str implicitly"
+ if sys.version_info >= (3, 6):
+ error_msg = "must be str, not int"
+ with self.assertRaisesRegexp(QWebException, error_msg):
+ view1.render()
from copy import deepcopy
class FileSystemLoader(object):
diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py
index 5681e013d38..e067c87a5fe 100644
--- a/odoo/addons/test_pylint/tests/test_pylint.py
+++ b/odoo/addons/test_pylint/tests/test_pylint.py
@@ -10,6 +10,7 @@ import subprocess
from distutils.version import LooseVersion
import os
from os.path import join
+import sys
from odoo.tests.common import TransactionCase
from odoo import tools
@@ -114,8 +115,11 @@ class TestPyLint(TransactionCase):
def test_pylint(self):
if pylint is None:
self._skip_test('please install pylint')
- if LooseVersion(getattr(pylint, '__version__', '0.0.1')) < LooseVersion('1.6.4'):
- self._skip_test('please upgrade pylint to >= 1.6.4')
+ required_pylint_version = LooseVersion('1.6.4')
+ if sys.version_info >= (3, 6):
+ required_pylint_version = LooseVersion('1.7.0')
+ if LooseVersion(getattr(pylint, '__version__', '0.0.1')) < required_pylint_version:
+ self._skip_test('please upgrade pylint to >= %s' % required_pylint_version)
paths = [tools.config['root_path']]
for module in get_modules():
diff --git a/odoo/models.py b/odoo/models.py
index 6b93f68972e..e97d8aea01b 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -2022,7 +2022,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
for field in many2onefields:
ids_set = {d[field] for d in data if d[field]}
m2o_records = self.env[self._fields[field].comodel_name].browse(ids_set)
- data_dict = dict(m2o_records.name_get())
+ data_dict = dict(m2o_records.sudo().name_get())
for d in data:
d[field] = (d[field], data_dict[d[field]]) if d[field] else False
@@ -3951,7 +3951,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
vals = self.copy_data(default)[0]
# To avoid to create a translation in the lang of the user, copy_translation will do it
new = self.with_context(lang=None).create(vals)
- self.copy_translations(new)
+ self.with_context(from_copy_translation=True).copy_translations(new)
return new
@api.multi
diff --git a/odoo/service/server.py b/odoo/service/server.py
index d246a3d80f1..c42c8d1343a 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -364,7 +364,10 @@ class GeventServer(CommonServer):
def start(self):
import gevent
- from gevent.wsgi import WSGIServer
+ try:
+ from gevent.pywsgi import WSGIServer
+ except ImportError:
+ from gevent.wsgi import WSGIServer
if os.name == 'posix':
diff --git a/setup.py b/setup.py
index a85c9e3fcdb..86920fccbc9 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@ def py2exe_options():
'dist_dir': 'dist',
'packages': [
'asynchat', 'asyncore',
+ 'BeautifulSoup',
'commands',
'dateutil',
'decimal',