summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-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
48 files changed, 414 insertions, 111 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):