[MERGE] forward port branch 11.0 up to d67b410dfc

This commit is contained in:
Christophe Simonis
2018-03-07 18:46:38 +01:00
71 changed files with 814 additions and 145 deletions
+1
View File
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
@classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the invoice, but provided
# a valid access token, redirect him to the front-end view.
+1 -1
View File
@@ -318,7 +318,7 @@ class AccountMoveLine(models.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state')
def _amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
+7 -1
View File
@@ -4,7 +4,7 @@ from datetime import timedelta, datetime
import calendar
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round, float_is_zero
@@ -130,6 +130,12 @@ Best Regards,'''))
if values.get('cash_account_code_prefix'):
new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code)
#forbid the change of currency_id if there are already some accounting entries existing
if 'currency_id' in values and values['currency_id'] != company.currency_id.id:
if self.env['account.move.line'].search([('company_id', '=', company.id)]):
raise UserError(_('You cannot change the currency of the company since some journal items already exist'))
return super(ResCompany, self).write(values)
@api.model
+3 -1
View File
@@ -20,7 +20,9 @@ class TestPayment(AccountingTestCase):
self.currency_chf_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_eur_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id])
self.product = self.env.ref("product.product_product_4")
self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in")
self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out")
+2 -1
View File
@@ -27,7 +27,8 @@ class TestReconciliation(AccountingTestCase):
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
@@ -155,7 +155,7 @@ FormController.include({
id: candidate.id,
data: candidateChanges,
};
return this.model.notifyChanges(this.handle, changes);
return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
@@ -211,6 +211,8 @@ FormController.include({
* @param {OdooEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
* @param {boolean} [event.data.notifyChange] optional for x2many sub field
* do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
@@ -225,7 +227,9 @@ FormController.include({
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
@@ -319,7 +323,7 @@ FormController.include({
// only catch the event if we're not focused in
// another field and it's a number
if (!$(event.target).is('body') || !/[0-9]/.test(character)) {
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
return;
}
+14 -12
View File
@@ -971,6 +971,8 @@ class Lead(models.Model):
'nb_opportunities': 0,
}
today = fields.Date.from_string(fields.Date.context_today(self))
opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)])
for opp in opportunities:
@@ -978,28 +980,28 @@ class Lead(models.Model):
if opp.activity_date_deadline:
if opp.date_deadline:
date_deadline = fields.Date.from_string(opp.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['closing']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['closing']['next_7_days'] += 1
if date_deadline < date.today() and not opp.date_closed:
if date_deadline < today and not opp.date_closed:
result['closing']['overdue'] += 1
# Next activities
for activity in opp.activity_ids:
date_deadline = fields.Date.from_string(activity.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['activity']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['activity']['next_7_days'] += 1
if date_deadline < date.today():
if date_deadline < today:
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100:
date_closed = fields.Date.from_string(opp.date_closed)
if date.today().replace(day=1) <= date_closed <= date.today():
if today.replace(day=1) <= date_closed <= today:
if opp.planned_revenue:
result['won']['this_month'] += opp.planned_revenue
elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1):
if opp.planned_revenue:
result['won']['last_month'] += opp.planned_revenue
@@ -1024,9 +1026,9 @@ class Lead(models.Model):
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
if date.today().replace(day=1) <= date_act <= date.today():
if today.replace(day=1) <= date_act <= today:
result['done']['this_month'] += 1
elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1):
result['done']['last_month'] += 1
# Meetings
@@ -1041,9 +1043,9 @@ class Lead(models.Model):
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
if start == date.today():
if start == today:
result['meeting']['today'] += 1
if date.today() <= start <= date.today() + timedelta(days=7):
if today <= start <= today + timedelta(days=7):
result['meeting']['next_7_days'] += 1
result['done']['target'] = self.env.user.target_sales_done
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import res_config_settings
from . import delivery_carrier
from . import delivery_grid
from . import product_template
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def set_values(self):
super(ResConfigSettings, self).set_values()
rule = self.env.ref('delivery.delivery_carrier_comp_rule', False)
if rule:
rule.write({'active': not bool(self.company_share_product)})
@@ -6,6 +6,7 @@
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field name="active" eval="False"/>
</record>
</data>
</odoo>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="delivery_comp_rule" model="ir.rule">
<field name="name">Delivery multi-company</field>
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('product_id.company_id','=',user.company_id.id),('product_id.company_id','=',False)]</field>
<field name="active" eval="False"/>
</record>
</data>
</odoo>
+1 -1
View File
@@ -88,7 +88,7 @@ class HolidaysRequest(models.Model):
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.manager_id = self.employee_id and self.employee_id.parent_id
self.department_id = self.employee_id and self.employee_id.department_id
self.department_id = self.employee_id.department_id
@api.multi
@api.depends('number_of_days_temp')
+1 -1
View File
@@ -14,7 +14,7 @@ class AccountAnalyticLine(models.Model):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
task_id = fields.Many2one('project.task', 'Task')
task_id = fields.Many2one('project.task', 'Task', index=True)
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
+2 -1
View File
@@ -110,7 +110,8 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/history', type="json", auth="public")
def history_pages(self, pid, channel_uuid, page_history=None):
channel = request.env['mail.channel'].search([('uuid', '=', channel_uuid)])
partner_ids = (pid, request.env.user.partner_id.id)
channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid), ('channel_partner_ids', 'in', partner_ids)])
if channel:
channel._send_history_message(pid, page_history)
return True
@@ -238,7 +238,7 @@ class ImLivechatChannelRule(models.Model):
"""
def _match(rules):
for rule in rules:
if re.search(rule.regex_url, url):
if re.search(rule.regex_url or '', url):
return rule
return False
# first, search the country specific rules (the first match is returned)
@@ -15,9 +15,9 @@
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label string="New Car Request"/>
<div class="text-muted o_row">
<div class="text-muted content-group mt16">
<span>An employee cannot request a new car if there are more than </span>
<field name="max_unused_cars"/>
<field name="max_unused_cars" class="text-center oe_inline" />
<span> existing vehicles availables.</span>
</div>
</div>
+5 -5
View File
@@ -73,21 +73,21 @@
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cgst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_sgst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_igst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_igst_id in tax_data['tag_ids']">
<t t-esc="tax_data['amount']"/>
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
@@ -95,10 +95,10 @@
<td class="text-right" t-if="is_cess">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cess_id in tax_data['tag_ids']">
<t t-set="total_cess" t-value="total_cess + tax_data['amount']"/>
<t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
<t t-esc="total_cess"/>
<t t-esc="total_cess" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</td>
</t>
</tr>
+9
View File
@@ -347,6 +347,15 @@ class MailActivityMixin(models.AbstractModel):
def _search_activity_summary(self, operator, operand):
return [('activity_ids.summary', operator, operand)]
@api.multi
def write(self, vals):
# Delete activities of archived record.
if 'active' in vals and vals['active'] is False:
self.env['mail.activity'].sudo().search(
[('res_model', '=', self._name), ('res_id', 'in', self.ids)]
).unlink()
return super(MailActivityMixin, self).write(vals)
@api.multi
def unlink(self):
""" Override unlink to delete records activities through (res_model, res_id). """
+1 -1
View File
@@ -271,7 +271,7 @@
</t>
<t t-name="mail.Attachment">
<t t-set="type" t-value="attachment.mimetype.split('/').shift()"/>
<t t-set="type" t-value="attachment.mimetype and attachment.mimetype.split('/').shift()"/>
<div t-attf-class="o_attachment #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
<t t-if="type == 'image'">
<div class="o_image_box">
+1 -1
View File
@@ -11,4 +11,4 @@ class NotePad(models.Model):
_pad_fields = ['note_pad']
note_pad_url = fields.Char('Pad Url', pad_content_field='memo')
note_pad_url = fields.Char('Pad Url', pad_content_field='memo', copy=False)
+9 -21
View File
@@ -101,16 +101,19 @@ class PadCommon(models.AbstractModel):
@api.multi
def write(self, vals):
self._set_pad_value(vals)
self._set_field_to_pad(vals)
self._set_pad_to_field(vals)
return super(PadCommon, self).write(vals)
@api.model
def create(self, vals):
self._set_pad_value(vals)
# Case of a regular creation: we receive the pad url, so we need to update the
# corresponding field
self._set_pad_to_field(vals)
pad = super(PadCommon, self).create(vals)
# In case the pad is created programmatically, the content is not filled in yet since it is
# normally initialized by the JS layer
# Case of a programmatical creation (e.g. copy): we receive the field content, so we need
# to create the corresponding pad
if self.env.context.get('pad_no_create', False):
return pad
for k, field in self._fields.items():
@@ -124,9 +127,7 @@ class PadCommon(models.AbstractModel):
pad[k] = pad_info.get('url')
return pad
# Set the pad content in vals
def _set_pad_value(self, vals):
def _set_field_to_pad(self, vals):
# Update the pad if the `pad_content_field` is modified
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
@@ -135,22 +136,9 @@ class PadCommon(models.AbstractModel):
path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
def _set_pad_to_field(self, vals):
# Update the `pad_content_field` if the pad is modified
for k, v in list(vals.items()):
field = self._fields.get(k)
if hasattr(field, 'pad_content_field'):
vals[field.pad_content_field] = self.pad_get_content(v)
@api.multi
def copy(self, default=None):
self.ensure_one()
if not default:
default = {}
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field'):
if self.env.context.get('pad_no_create', False):
default[k] = ''
else:
pad = self.pad_generate_url()
default[k] = pad.get('url')
return super(PadCommon, self).copy(default)
+1 -7
View File
@@ -8,7 +8,7 @@ class ProjectTask(models.Model):
_name = "project.task"
_inherit = ["project.task", 'pad.common']
description_pad = fields.Char('Pad URL', pad_content_field='description')
description_pad = fields.Char('Pad URL', pad_content_field='description', copy=False)
use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad")
@api.model
@@ -19,12 +19,6 @@ class ProjectTask(models.Model):
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).create(vals)
@api.multi
def copy(self, default=None):
if not self.use_pad:
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).copy(default)
class ProjectProject(models.Model):
_inherit = "project.project"
+4 -2
View File
@@ -73,7 +73,8 @@ class WebsitePayment(http.Controller):
return tx
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',], type='json', auth='public')
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
@@ -100,7 +101,8 @@ class WebsitePayment(http.Controller):
return acquirer.sudo().render(reference, float(amount), int(currency_id), values=render_values)
@http.route(['/website_payment/token/<string:reference>/<float:amount>/<int:currency_id>'], type='http', auth='public', website=True)
@http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
@@ -1,6 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_acquirer_all,payment.acquirer.all,model_payment_acquirer,,1,0,0,0
payment_acquirer_user,payment.acquirer.user,model_payment_acquirer,base.group_user,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0,0
payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 payment_acquirer_all payment.acquirer.all model_payment_acquirer 1 0 0 0
payment_acquirer_user payment.acquirer.user model_payment_acquirer base.group_user 1 1 1 0
3 payment_acquirer_system payment.acquirer.system model_payment_acquirer base.group_system 1 1 1 1
4 payment_transaction_all payment.transaction.all model_payment_transaction 1 0 0 0
5 payment_transaction_user payment.transaction.user model_payment_transaction base.group_user 1 1 1 0
@@ -55,8 +55,8 @@
<p><b>Amount:</b> <t t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': currency}"/></p>
<t t-call="payment.payment_tokens_list" t-if="reference and amount and currency">
<t t-set="mode" t-value="'payment'"/>
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
<t t-set="form_action" t-value="'/website_payment/token/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
<t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
</t>
<div t-if="not (s2s_acquirers or form_acquirers)" class="alert alert-danger">
<p>No payment acquirer found.</p>
@@ -109,8 +109,8 @@ odoo.define('payment_stripe.stripe', function(require) {
if ($('.o_website_payment').length !== 0) {
var invoice_num = $("input[name='invoice_num']").val();
var url = _.str.sprintf("/website_payment/transaction/%s/%f/%s",
invoice_num, amount, currency_id);
var url = _.str.sprintf("/website_payment/transaction/v2/%f/%s/%s",
amount, currency_id, invoice_num);
var create_tx = ajax.jsonRpc(url, 'call', {
acquirer_id: acquirer_id
+3 -2
View File
@@ -160,13 +160,14 @@ class PosOrder(models.Model):
"""
Prepare the dict of values to create the new invoice for a pos order.
"""
invoice_type = 'out_invoice' if self.amount_total >= 0 else 'out_refund'
return {
'name': self.name,
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'company_id': self.company_id.id,
'type': 'out_invoice',
'type': invoice_type,
'reference': self.name,
'partner_id': self.partner_id.id,
'comment': self.note or '',
@@ -208,7 +209,7 @@ class PosOrder(models.Model):
inv_line = {
'invoice_id': invoice_id,
'product_id': line.product_id.id,
'quantity': line.qty,
'quantity': line.qty if self.amount_total >= 0 else -line.qty,
'account_analytic_id': self._prepare_analytic_account(line),
'name': inv_name,
}
+1 -1
View File
@@ -526,7 +526,7 @@ class Task(models.Model):
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
parent_id = fields.Many2one('project.task', string='Parent Task')
parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
+2 -2
View File
@@ -34,14 +34,14 @@ class StockMove(models.Model):
@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
distinct_fields.append('purchase_line_id')
distinct_fields += ['purchase_line_id', 'created_purchase_line_id']
return distinct_fields
@api.model
def _prepare_merge_move_sort_method(self, move):
move.ensure_one()
keys_sorted = super(StockMove, self)._prepare_merge_move_sort_method(move)
keys_sorted.append(move.purchase_line_id.id)
keys_sorted += [move.purchase_line_id.id, move.created_purchase_line_id.id]
return keys_sorted
@api.multi
@@ -224,3 +224,92 @@ class TestCreatePicking(common.TestProductCommon):
# check the delivered quantity
self.assertEqual(po.order_line.qty_received, 3.0)
def test_04_mto_multiple_po(self):
""" Simulate a mto chain with 2 purchase order.
Create a move with qty 1, confirm the RFQ then create a new
move that will not be merged in the first one(simulate an increase
order quantity on a SO). It should generate a new RFQ, validate
and receipt the picking then try to reserve the delivery
picking.
"""
stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
# route buy should be there by default
partner = self.env['res.partner'].create({
'name': 'Jhon'
})
seller = self.env['product.supplierinfo'].create({
'name': partner.id,
'price': 12.0,
})
product = self.env['product.product'].create({
'name': 'product',
'type': 'product',
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase.route_warehouse0_buy'))],
'seller_ids': [(6, 0, [seller.id])],
'categ_id': self.env.ref('product.product_category_all').id,
})
# A picking is require since only moves inside the same picking are merged.
customer_picking = self.env['stock.picking'].create({
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'partner_id': partner.id,
'picking_type_id': picking_type_out.id,
})
customer_move = self.env['stock.move'].create({
'name': 'move out',
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'product_id': product.id,
'product_uom': product.uom_id.id,
'product_uom_qty': 80.0,
'procure_method': 'make_to_order',
'picking_id': customer_picking.id,
})
customer_move._action_confirm()
purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
self.assertTrue(purchase_order, 'No purchase order created.')
# Check purchase order line data.
purchase_order_line = purchase_order.order_line
self.assertEqual(purchase_order_line.product_id, product, 'The product on the purchase order line is not correct.')
self.assertEqual(purchase_order_line.price_unit, seller.price, 'The purchase order line price should be the same than the seller.')
self.assertEqual(purchase_order_line.product_qty, customer_move.product_uom_qty, 'The purchase order line qty should be the same than the move.')
purchase_order.button_confirm()
customer_move_2 = self.env['stock.move'].create({
'name': 'move out',
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'product_id': product.id,
'product_uom': product.uom_id.id,
'product_uom_qty': 20.0,
'procure_method': 'make_to_order',
'picking_id': customer_picking.id,
})
customer_move_2._action_confirm()
self.assertTrue(customer_move_2.exists(), 'The second customer move should not be merged in the first.')
self.assertEqual(sum(customer_picking.move_lines.mapped('product_uom_qty')), 100.0)
purchase_order_2 = self.env['purchase.order'].search([('partner_id', '=', partner.id), ('state', '=', 'draft')])
self.assertTrue(purchase_order_2, 'No purchase order created.')
purchase_order_2.button_confirm()
purchase_order.picking_ids.move_lines.quantity_done = 80.0
purchase_order.picking_ids.button_validate()
purchase_order_2.picking_ids.move_lines.quantity_done = 20.0
purchase_order_2.picking_ids.button_validate()
self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
+2 -1
View File
@@ -16,7 +16,8 @@ def uninstall_hook(cr, registry):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env['account.invoice.report'].init()
if 'account.invoice.report' in env:
env['account.invoice.report'].init()
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
+1
View File
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
@classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the sales order, but provided
# a valid access token, redirect him to the front-end view.
+1 -2
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
@@ -31,7 +30,7 @@ class CrmLead(models.Model):
@api.model
def retrieve_sales_dashboard(self):
res = super(CrmLead, self).retrieve_sales_dashboard()
date_today = date.today()
date_today = fields.Date.from_string(fields.Date.context_today(self))
res['invoiced'] = {
'this_month': 0,
+3 -1
View File
@@ -51,7 +51,9 @@ class AccountInvoiceLine(models.Model):
qty_to_consider = invoiced_qty - qty_done
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
qty_delivered += qty_to_consider
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + (-1 * move.price_unit) * qty_to_consider) / qty_delivered
# `move.price_unit` is negative if the move is out and positive if the move is
# dropshipped. Use its absolute value to compute the average price unit.
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
+1 -1
View File
@@ -137,7 +137,7 @@ class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
qty_delivered_method = fields.Selection(selection_add=[('timesheet', 'Timesheets')])
task_id = fields.Many2one('project.task', 'Task', help="Task generated by the sales order item")
task_id = fields.Many2one('project.task', 'Task', index=True, help="Task generated by the sales order item")
is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
analytic_line_ids = fields.One2many(domain=[('project_id', '=', False)]) # only analytic lines, not timesheets (since this field determine if SO line came from expense)
-7
View File
@@ -419,13 +419,6 @@
obj().env.ref('stock.incomming_chicago_warehouse3').id]"/>
</function>
<function model="stock.picking" name="force_assign">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse').id,
obj().env.ref('stock.outgoing_chicago_warehouse3').id,
obj().env.ref('stock.outgoing_shipment_main_warehouse2').id]"/>
</function>
<function model="stock.picking" name="do_transfer">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse1').id,
+1 -1
View File
@@ -236,7 +236,7 @@ class ProcurementGroup(models.Model):
self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id)
# Search all confirmed stock_moves and try to assign them
confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc')
confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc')
for moves_chunk in split_every(100, confirmed_moves.ids):
self.env['stock.move'].browse(moves_chunk)._action_assign()
if use_new_cursor:
+8
View File
@@ -159,6 +159,7 @@ class StockMove(models.Model):
is_initial_demand_editable = fields.Boolean('Is initial demand editable', compute='_compute_is_initial_demand_editable')
is_quantity_done_editable = fields.Boolean('Is quantity done editable', compute='_compute_is_quantity_done_editable')
reference = fields.Char(compute='_compute_reference', string="Reference", store=True)
has_move_lines = fields.Boolean(compute='_compute_has_move_lines')
@api.depends('picking_id.is_locked')
def _compute_is_locked(self):
@@ -218,6 +219,8 @@ class StockMove(models.Model):
move.is_quantity_done_editable = False
elif move.show_details_visible:
move.is_quantity_done_editable = False
elif move.show_operations:
move.is_quantity_done_editable = False
else:
move.is_quantity_done_editable = True
@@ -226,6 +229,11 @@ class StockMove(models.Model):
for move in self:
move.reference = move.picking_id.name if move.picking_id else move.name
@api.depends('move_line_ids')
def _compute_has_move_lines(self):
for move in self:
move.has_move_lines = bool(move.move_line_ids)
@api.one
@api.depends('product_id', 'product_uom', 'product_uom_qty')
def _compute_product_qty(self):
+1 -1
View File
@@ -167,7 +167,7 @@ class StockMoveLine(models.Model):
# If the move line is directly create on the picking view.
# If this picking is already done we should generate an
# associated done move.
if 'picking_id' in vals and 'move_id' not in vals:
if 'picking_id' in vals and not vals.get('move_id'):
picking = self.env['stock.picking'].browse(vals['picking_id'])
if picking.state == 'done':
product = self.env['product.product'].browse(vals['product_id'])
+12 -1
View File
@@ -514,7 +514,18 @@ class Picking(models.Model):
if after_vals:
self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
if vals.get('move_lines'):
self._autoconfirm_picking()
# Do not run autoconfirm if any of the moves has an initial demand. If an initial demand
# is present in any of the moves, it means the picking was created through the "planned
# transfer" mechanism.
pickings_to_not_autoconfirm = self.env['stock.picking']
for picking in self:
if picking.state != 'draft':
continue
for move in picking.move_lines:
if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
pickings_to_not_autoconfirm |= picking
break
(self - pickings_to_not_autoconfirm)._autoconfirm_picking()
return res
@api.multi
+45
View File
@@ -185,6 +185,51 @@ class TestPickShip(TestStockCommon):
# the client picking should not be assigned anymore, as we returned partially what we took
self.assertEqual(picking_client.state, 'confirmed')
def test_mto_resupply_cancel_ship(self):
""" This test simulates a pick pack ship with a resupply route
set. Pick and pack are validated, ship is cancelled. This test
ensure that new picking are not created from the cancelled
ship after the scheduler task. The supply route is only set in
order to make the scheduler run without mistakes (no next
activity).
"""
picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
stock_location = self.env['stock.location'].browse(self.stock_location)
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
warehouse_1.write({'delivery_steps': 'pick_pack_ship'})
warehouse_2 = self.env['stock.warehouse'].create({
'name': 'Small Warehouse',
'code': 'SWH'
})
warehouse_1.write({
'default_resupply_wh_id': warehouse_2.id,
'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
})
resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
self.assertTrue(resupply_route)
self.productA.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
picking_pick.action_assign()
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
picking_pick.action_done()
picking_pack.action_assign()
picking_pack.move_lines[0].move_line_ids[0].qty_done = 10.0
picking_pack.action_done()
picking_ship.action_cancel()
picking_ship.move_lines.write({'procure_method': 'make_to_order'})
self.env['procurement.group'].run_scheduler()
next_activity = self.env['mail.activity'].search([('res_model', '=', 'product.template'), ('res_id', '=', self.productA.product_tmpl_id.id)])
self.assertEqual(picking_ship.state, 'cancel')
self.assertFalse(next_activity, 'If a next activity has been created if means that scheduler failed\
and the end of this test do not have sense.')
self.assertEqual(len(picking_ship.move_lines.mapped('move_orig_ids')), 0,
'Scheduler should not create picking pack and pick since ship has been manually cancelled.')
def test_no_backorder_1(self):
""" Check the behavior of doing less than asked in the picking pick and chosing not to
create a backorder. In this behavior, the second picking should obviously only be able to
+6 -4
View File
@@ -257,7 +257,8 @@
<page string="Detailed Operations" attrs="{'invisible': [('show_operations', '=', False)]}">
<field name="move_line_ids" attrs="{'readonly': [('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree editable="bottom" decoration-muted="(state == 'done' and is_locked == True)" decoration-danger="qty_done&gt;product_uom_qty and state!='done'" decoration-success="qty_done==product_uom_qty and state!='done' and not result_package_id">
<field name="product_id" required="1"/>
<field name="product_id" required="1" attrs="{'readonly': ['|', ('state', '=', 'done'), ('move_id', '!=', False)]}"/>
<field name="move_id" invisible="1"/>
<field name="in_entire_package" invisible="1"/>
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="product.group_uom"/>
<field name="package_id" attrs="{'readonly': [('in_entire_package', '=', True)]}" groups="stock.group_tracking_lot" domain="[('location_id', '=', location_id)]"/>
@@ -270,7 +271,7 @@
<field name="lot_id" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', True)], 'invisible': [('lots_visible', '=', False)], 'readonly': [('in_entire_package', '=', True)]}" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
<field name="lot_name" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', False)], 'invisible': [('lots_visible', '=', False)], 'readonly': [('in_entire_package', '=', True)]}" context="{'default_product_id': product_id}"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="product_uom_qty" attrs="{'readonly': ['|', ('is_initial_demand_editable', '=', False), ('in_entire_package', '=', True)], 'required': [('product_id', '!=', False)]}"/>
<field name="product_uom_qty" readonly="1"/>
<field name="is_locked" invisible="1"/>
<field name="qty_done" attrs="{'readonly': ['|', ('state', 'in', ('done', 'cancel')), ('is_locked', '=', True), ('in_entire_package', '=', True)]}" force_save="1"/>
</tree>
@@ -280,7 +281,7 @@
</page>
<page string="Operations">
<field name="id" invisible="1"/>
<field name="move_lines" attrs="{'readonly': ['|', ('show_operations', '=', True), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<field name="move_lines" attrs="{'readonly': ['|', '&amp;', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability and show_reserved_availability" decoration-muted="scrapped == True or state == 'cancel' or (state == 'done' and is_locked == True)" string="Stock Moves" editable="bottom">
<field name="name" invisible="1"/>
<field name="date_expected" invisible="1"/>
@@ -295,8 +296,9 @@
<field name="show_reserved_availability" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
<field name="has_move_lines" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="product_id" required="1" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
<field name="product_id" required="1" attrs="{'readonly': ['|', '&amp;', ('state', '!=', 'draft'), ('additional', '=', False), ('has_move_lines', '=', True)]}"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
+4 -3
View File
@@ -66,13 +66,14 @@ def _configure_journals(cr, registry):
'company_id': company_id.id,
'fields_id': field_id,
'value': value,
'res_id': 'product.category,'+str(env.ref('product.product_category_all').id),
}
properties = env['ir.property'].search([
('name', '=', record),
('company_id', '=', company_id.id),
('value_reference', '!=', False)])
if not properties:
])
if properties:
properties.write(vals)
else:
# create the property
env['ir.property'].create(vals)
+25 -2
View File
@@ -200,6 +200,14 @@ class StockMove(models.Model):
return True
return False
def _is_dropshipped(self):
""" Check if the move should be considered as a dropshipping move so that the cost method
will be able to apply the correct logic.
:return: True if the move is a dropshipping one else False
"""
return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer'
@api.model
def _run_fifo(self, move, quantity=None):
move.ensure_one()
@@ -298,6 +306,21 @@ class StockMove(models.Model):
'value': value if quantity is None else self.value + value,
'price_unit': value / valued_quantity,
})
elif self._is_dropshipped():
curr_rounding = self.company_id.currency_id.rounding
if self.product_id.cost_method in ['fifo']:
price_unit = self._get_price_unit()
# see test_dropship_fifo_perpetual_anglosaxon_ordered
self.product_id.standard_price = price_unit
else:
price_unit = self.product_id.standard_price
value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding)
# In move have a positive value, out move have a negative value, let's arbitrary say
# dropship are positive.
self.write({
'value': value,
'price_unit': price_unit,
})
def _action_done(self):
self.product_price_update_before_done()
@@ -319,7 +342,7 @@ class StockMove(models.Model):
if company_src and company_dst and company_src.id != company_dst.id:
raise UserError(_("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location."))
move._run_valuation()
for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out())):
for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped())):
move._account_entry_move()
return res
@@ -589,7 +612,7 @@ class StockMove(models.Model):
else:
self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_dest, journal_id)
if self.company_id.anglo_saxon_accounting and self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer':
if self.company_id.anglo_saxon_accounting and self._is_dropshipped():
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_src, acc_dest, journal_id)
@@ -5,3 +5,5 @@ from . import test_crossdock
from . import test_dropship
from . import test_lifo_price
from . import test_procurement_exception
from . import test_stockvaluation
@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
class TestStockValuation(AccountingTestCase):
def setUp(self):
super(TestStockValuation, self).setUp()
self.supplier_location = self.env.ref('stock.stock_location_suppliers')
self.stock_location = self.env.ref('stock.stock_location_stock')
self.partner_id = self.env.ref('base.res_partner_1')
self.product1 = self.env.ref('product.product_product_8')
self.acc_payable = self.env['account.account'].search([('name', '=', 'Account Payable')]).id
self.acc_expense = self.env['account.account'].search([('name', '=', 'Expenses')]).id
self.acc_receivable = self.env['account.account'].search([('name', '=', 'Account Receivable')]).id
self.acc_sale = self.env['account.account'].search([('name', '=', 'Product Sales')]).id
self.acc_stock_in = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Received)')]).id
self.acc_stock_out = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Delivered)')]).id
def _dropship_product1(self):
# enable the dropship and MTO route on the product
dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping')
mto_route = self.env.ref('stock.route_warehouse0_mto')
self.product1.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]})
# add a vendor
vendor1 = self.env['res.partner'].create({'name': 'vendor1'})
seller1 = self.env['product.supplierinfo'].create({
'name': vendor1.id,
'price': 8,
})
self.product1.write({'seller_ids': [(6, 0, [seller1.id])]})
# sell one unit of this product
customer1 = self.env['res.partner'].create({'name': 'customer1'})
sale_order1 = self.env['sale.order'].create({
'partner_id': customer1.id,
'partner_invoice_id': customer1.id,
'partner_shipping_id': customer1.id,
'order_line': [(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_uom_qty': 1,
'product_uom': self.product1.uom_id.id,
'price_unit': 12,
})],
'pricelist_id': self.env.ref('product.list0').id,
'picking_policy': 'direct',
})
sale_order1.action_confirm()
# confirm the purchase order
purchase_order1 = self.env['purchase.order'].search([('group_id', '=', sale_order1.procurement_group_id.id)])
purchase_order1.button_confirm()
# validate the dropshipping picking
self.assertEqual(len(sale_order1.picking_ids), 1)
self.assertEqual(sale_order1.picking_ids.move_lines._is_dropshipped(), True)
wizard = sale_order1.picking_ids.button_validate()
immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id'])
immediate_transfer.process()
self.assertEqual(sale_order1.picking_ids.state, 'done')
# create the vendor bill
vendor_bill1 = self.env['account.invoice'].create({
'partner_id': vendor1.id,
'purchase_id': purchase_order1.id,
'account_id': vendor1.property_account_payable_id.id,
'type': 'in_invoice',
})
vendor_bill1.purchase_order_change()
vendor_bill1.action_invoice_open()
# create the customer invoice
customer_invoice1_id = sale_order1.action_invoice_create()
customer_invoice1 = self.env['account.invoice'].browse(customer_invoice1_id)
customer_invoice1.action_invoice_open()
all_amls = vendor_bill1.move_id.line_ids + customer_invoice1.move_id.line_ids
if sale_order1.picking_ids.move_lines.account_move_ids:
all_amls |= sale_order1.picking_ids.move_lines.account_move_ids.line_ids
return all_amls
def _check_results(self, expected_aml, expected_aml_count, all_amls):
# Construct a dict similar to `expected_aml` with `all_amls` in order to
# compare them.
result_aml = {}
for aml in all_amls:
account_id = aml.account_id.id
if result_aml.get(account_id):
debit = result_aml[account_id][0]
credit = result_aml[account_id][1]
result_aml[account_id] = (debit + aml.debit, credit + aml.credit)
else:
result_aml[account_id] = (aml.debit, aml.credit)
self.assertEqual(len(all_amls), expected_aml_count)
for k, v in expected_aml.items():
self.assertEqual(result_aml[k], v)
# -------------------------------------------------------------------------
# Continental
# -------------------------------------------------------------------------
def test_dropship_standard_perpetual_continental_ordered(self):
self.env.user.company_id.anglo_saxon_accounting = False
self.product1.product_tmpl_id.cost_method = 'standard'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
}
self._check_results(expected_aml, 4, all_amls)
def test_dropship_standard_perpetual_continental_delivered(self):
self.env.user.company_id.anglo_saxon_accounting = False
self.product1.product_tmpl_id.cost_method = 'standard'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
}
self._check_results(expected_aml, 4, all_amls)
def test_dropship_fifo_perpetual_continental_ordered(self):
self.env.user.company_id.anglo_saxon_accounting = False
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
}
self._check_results(expected_aml, 4, all_amls)
def test_dropship_fifo_perpetual_continental_delivered(self):
self.env.user.company_id.anglo_saxon_accounting = False
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
}
self._check_results(expected_aml, 4, all_amls)
# -------------------------------------------------------------------------
# Anglosaxon
# -------------------------------------------------------------------------
def test_dropship_standard_perpetual_anglosaxon_ordered(self):
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.product_tmpl_id.cost_method = 'standard'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (10.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
self.acc_stock_in: (8.0, 10.0),
self.acc_stock_out: (10.0, 10.0),
}
# Interim IN is not balanced because because there's a difference between the po line
# price unit and the standard price. We could set a price difference account on the
# category to compensate.
self._check_results(expected_aml, 8, all_amls)
def test_dropship_standard_perpetual_anglosaxon_delivered(self):
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.product_tmpl_id.cost_method = 'standard'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (10.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
self.acc_stock_in: (8.0, 10.0),
self.acc_stock_out: (10.0, 10.0),
}
# Interim IN is not balanced because because there's a difference between the po line
# price unit and the standard price. We could set a price difference account on the
# category to compensate.
self._check_results(expected_aml, 8, all_amls)
def test_dropship_fifo_perpetual_anglosaxon_ordered(self):
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'order'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
self.acc_stock_in: (8.0, 8.0),
self.acc_stock_out: (8.0, 8.0),
}
self._check_results(expected_aml, 8, all_amls)
def test_dropship_fifo_perpetual_anglosaxon_delivered(self):
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.standard_price = 10
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
all_amls = self._dropship_product1()
expected_aml = {
self.acc_payable: (0.0, 8.0),
self.acc_expense: (8.0, 0.0),
self.acc_receivable: (12.0, 0.0),
self.acc_sale: (0.0, 12.0),
self.acc_stock_in: (8.0, 8.0),
self.acc_stock_out: (8.0, 8.0),
}
self._check_results(expected_aml, 8, all_amls)
@@ -22,7 +22,7 @@
<group>
<group>
<field name="date"/>
<field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('state', '=', 'done')]"/>
<field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('state', '=', 'done'), ('picking_type_code', '!=', 'internal')]"/>
</group>
<group>
<field name="account_journal_id"/>
+1
View File
@@ -680,6 +680,7 @@ class SurveyLabel(models.Model):
value = fields.Char('Suggested value', translate=True, required=True)
quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
@api.one
@api.constrains('question_id', 'question_id_2')
def _check_question_not_empty(self):
"""Ensure that field question_id XOR field question_id_2 is not null"""
+6 -1
View File
@@ -358,7 +358,12 @@ QWeb2.Engine = (function() {
req.open('GET', s, async);
if (async) {
req.addEventListener("load", function() {
if (req.status == 200) {
// 0, not being a valid HTTP status code, is used by browsers
// to indicate success for a non-http xhr response
// (for example, using the file:// protocol)
// https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest
// https://bugzilla.mozilla.org/show_bug.cgi?id=331610
if (req.status == 200 || req.status == 0) {
callback(null, self._parse_from_request(req));
} else {
callback(new Error("Can't load template " + s + ", http status " + req.status));
@@ -126,6 +126,7 @@ var PivotController = AbstractController.extend({
var table = this.model.exportData();
if(table.measure_row.length + 1 > 256) {
crash_manager.show_message(_t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."));
framework.unblockUI();
return;
}
framework.blockUI();
@@ -135,7 +135,7 @@ var PivotModel = AbstractModel.extend({
exportData: function () {
var measureNbr = this.data.measures.length;
var headers = this._computeHeaders();
var measureRow = measureNbr > 1 ? _.last(headers) : [];
var measureRow = measureNbr >= 1 ? _.last(headers) : [];
var rows = this._computeRows();
var i, j, value;
headers[0].splice(0,1);
@@ -455,7 +455,7 @@ var DataExport = Dialog.extend({
Dialog.alert(this, _t("Please select fields to export..."));
return;
}
exported_fields.unshift({name: 'id', label: 'External ID'});
exported_fields.unshift({name: 'id', label: _t('External ID')});
var export_format = this.$export_format_inputs.filter(':checked').val();
+3 -1
View File
@@ -165,7 +165,9 @@
.o-flex-flow(row, wrap);
> .btn {
margin-right: 3px;
@o-statusbar-buttons-vmargin: 4px;
min-height: @odoo-statusbar-height - 2 * @o-statusbar-buttons-vmargin;
margin: @o-statusbar-buttons-vmargin 3px @o-statusbar-buttons-vmargin 0;
padding-top: 2px;
padding-bottom: 2px;
}
+9 -7
View File
@@ -171,7 +171,8 @@ class WebsiteForm(http.Controller):
return data
def insert_record(self, request, model, values, custom, meta=None):
record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values)
model_name = model.sudo().model
record = request.env[model_name].sudo().with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
default_field = model.website_form_default_field_id
@@ -183,13 +184,13 @@ class WebsiteForm(http.Controller):
# If there is a default field configured for this model, use it.
# If there isn't, put the custom data in a message instead
if default_field.name:
if default_field.ttype == 'html' or model.model == 'mail.mail':
if default_field.ttype == 'html' or model_name == 'mail.mail':
custom_content = nl2br(custom_content)
record.update({default_field.name: custom_content})
else:
values = {
'body': nl2br(custom_content),
'model': model.model,
'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': record.id,
@@ -201,7 +202,8 @@ class WebsiteForm(http.Controller):
# Link all files attached on the form
def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = []
record = model.env[model.model].browse(id_record)
model_name = model.sudo().model
record = model.env[model_name].browse(id_record)
authorized_fields = model.sudo()._get_form_writable_fields()
for file in files:
custom_field = file.field_name not in authorized_fields
@@ -209,7 +211,7 @@ class WebsiteForm(http.Controller):
'name': file.filename,
'datas': base64.encodestring(file.read()),
'datas_fname': file.filename,
'res_model': model.model,
'res_model': model_name,
'res_id': record.id,
}
attachment_id = request.env['ir.attachment'].sudo().create(attachment_value)
@@ -221,10 +223,10 @@ class WebsiteForm(http.Controller):
# If some attachments didn't match a field on the model,
# we create a mail.message to link them to the record
if orphan_attachment_ids:
if model.model != 'mail.mail':
if model_name != 'mail.mail':
values = {
'body': _('<p>Attached files : </p>'),
'model': model.model,
'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': id_record,
+1 -1
View File
@@ -151,7 +151,7 @@ class WebsiteSale(http.Controller):
else:
price = variant.website_public_price / quantity
visible_attribute_ids = [v.id for v in variant.attribute_value_ids if v.attribute_id.id in visible_attrs_ids]
attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price, price])
attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price / quantity, price])
return attribute_value_ids
def _get_search_order(self, post):
+10
View File
@@ -548,6 +548,16 @@ Weight: 31 grams</field>
<field name="compute_price">formula</field>
</record>
<record id="item2" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">1_product</field>
<field name="pricelist_id" ref="product.list0"/>
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
<field name="price_discount">40</field>
<field name="min_quantity">2</field>
<field name="compute_price">formula</field>
</record>
<!-- product.public.category -->
<record id="Components" model="product.public.category">
@@ -88,18 +88,34 @@ odoo.define('website_sale.website_sale', function (require) {
$(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) {
var product_ids = [];
var product_dom = $(".js_product .js_add_cart_variants[data-attribute_value_ids]");
var product_dom = $(event.target).closest(".js_product").find("ul.js_add_cart_variants");
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
if (!product_dom.length) {
// if variants in list view, update variant price based on quantity
var $list_products = $(event.target).closest('.js_product').find('.js_product_change');
if ($list_products.length) {
ajax.jsonRpc("/shop/get_unit_price", 'call', {
'product_ids': _.map($list_products, function (variant) {
return parseInt(variant.value);
}),
'add_qty': parseInt(qty)
}).then(function (data) {
_.each(data, function (value, key) {
$list_products.filter('[value="'+key+'"]').data('price', value);
});
$list_products.filter(':checked').change();
});
}
return;
}
var attribute_value_ids = product_dom.data("attribute_value_ids");
if(_.isString(attribute_value_ids)) {
attribute_value_ids = JSON.parse(attribute_value_ids.replace(/'/g, '"'));
// set permanent reference so modifications are kept
product_dom.data('attribute_value_ids', attribute_value_ids);
}
_.each(attribute_value_ids, function(entry) {
product_ids.push(entry[0]);});
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
if ($("#product_detail").length) {
// display the reduction from the pricelist in function of the quantity
@@ -109,7 +125,7 @@ odoo.define('website_sale.website_sale', function (require) {
for(var j=0; j < current.length; j++){
current[j][2] = data[current[j][0]];
}
product_dom.attr("data-attribute_value_ids", JSON.stringify(current)).trigger("change");
product_dom.trigger("change");
});
}
});
@@ -210,8 +226,7 @@ odoo.define('website_sale.website_sale', function (require) {
$('input[name="'+$input.attr("name")+'"]').add($input).filter(function () {
var $prod = $(this).closest('*:has(input[name="product_id"])');
return !$prod.length || +$prod.find('input[name="product_id"]').val() === product_id;
}).val(new_qty);
$input.change();
}).val(new_qty).change();
return false;
});
@@ -380,7 +395,7 @@ odoo.define('website_sale.website_sale', function (require) {
});
$('div.js_product', oe_website_sale).each(function () {
$('input.js_product_change', this).first().trigger('change');
$('input.js_product_change', this).first().prop('checked', 'checked').trigger('change');
});
$('.js_add_cart_variants', oe_website_sale).each(function () {
+8 -5
View File
@@ -458,8 +458,7 @@
<div class="col-sm-5 col-lg-4 col-lg-offset-1" id="product_details">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
<span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/>
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<form action="/shop/cart/update" class="js_add_cart_variants" t-att-data-attribute_value_ids="attribute_value_ids" method="POST">
<form action="/shop/cart/update" class="js_add_cart_variants" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<div class="js_product" t-if="product.product_variant_ids">
<t t-placeholder="select">
@@ -603,20 +602,24 @@
<input type="hidden" t-if="len(product.product_variant_ids) == 1" class="product_id" name="product_id" t-att-value="product.product_variant_id.id"/>
<t t-if="len(product.product_variant_ids) &gt; 1">
<label label-default="label-default" class="radio" t-foreach="product.product_variant_ids" t-as="variant_id">
<input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else ''" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/>
<input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else None" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/>
<span t-esc="variant_id.name_get()[0][1]"/>
<t t-set="diff_price" t-value="variant_id.price - variant_id.product_tmpl_id.price"/>
<span class="badge" t-if="diff_price != 0">
<t t-esc="diff_price > 0 and '+' or '-'"/><span t-esc="abs(diff_price)" t-esc-options='{"widget": "monetary", "display_currency": "website.currency_id"}'/>
<t t-esc="diff_price > 0 and '+' or '-'"/><span t-esc="abs(diff_price)" t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
</span>
</label>
</t>
<t t-else="">
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<ul class="hidden js_add_cart_variants" t-att-data-attribute_value_ids="json.dumps(attribute_value_ids)"/>
</t>
</xpath>
</template>
<template id="variants">
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_value_ids="attribute_value_ids">
<ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_value_ids="json.dumps(attribute_value_ids)">
<t t-foreach="product.attribute_line_ids.sorted(key=lambda x: x.attribute_id.sequence)" t-as="variant_id">
<li t-if="len(variant_id.value_ids) > 1">
@@ -35,5 +35,7 @@ class ResCountry(models.Model):
states = res
break
states |= carrier.state_ids
if not states:
states = states.search([('country_id', '=', self.id)])
res = res & states
return res
@@ -45,10 +45,103 @@ tour.register('shop_customize', {
extra_trigger: 'body:not(:has(.oe_website_sale .oe_product_cart:eq(2)))',
trigger: '.oe_product_cart a:contains("iPad")',
},
{
content: "open customize menu",
trigger: '#customize-menu > a',
extra_trigger: "#product_detail",
},
{
content: "click on 'List View of Variants'",
trigger: "#customize-menu a:contains(List View of Variants)",
},
{
content: "check page loaded after list of variant customization enabled",
trigger: '.js_product_change',
run: function () {}, // it's a check
},
{
content: "check price is 600 and set quantity to 2",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(600.00)",
run: "text 2",
},
{
content: "verify pricelist based on quantity has effect",
trigger: ".product_price .oe_price .oe_currency_value:containsExact(450.00)",
run: function () {}, // it's a check
},
{
content: "check pricelit has been applied and switch to 32 GB variant",
trigger: ".js_product label:contains('32 GB')",
},
{
content: "verify that price has changed when changing variant",
trigger: ".product_price .oe_price .oe_currency_value:not(:containsExact(450.00))",
run: function () {}, // it's a check
},
{
content: "open customize menu",
trigger: '#customize-menu > a',
},
{
content: "remove 'List View of Variants'",
trigger: "#customize-menu a:contains(List View of Variants):has(input:checked)",
},
{
content: "check page loaded after list of variant customization disabled",
trigger: ".js_product:not(:has(.js_product_change))",
run: function () {}, // it's a check
},
{
content: "check price is 600 and set quantity to 2",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(600.00)",
run: "text 2",
},
{
content: "verify pricelist based on quantity has effect",
trigger: ".product_price .oe_price .oe_currency_value:containsExact(450.00)",
run: function () {}, // it's a check
},
{
content: "switch to 32 GB variant",
trigger: ".js_product label:contains('32 GB')",
},
{
content: "verify that price has changed when changing variant",
trigger: ".product_price .oe_price .oe_currency_value:not(:containsExact(450.00))",
run: function () {}, // it's a check
},
{
content: "switch back to 16 GB variant",
trigger: ".js_product label:contains('16 GB')",
},
{
content: "click on 'Add to Cart' button",
trigger: "a:contains(Add to Cart)",
},
{
content: "price is lowered by pricelist and not multiplied by quantity",
trigger: "#product_confirmation .oe_price .oe_currency_value:containsExact(450.00)",
extra_trigger: "#product_confirmation input.quantity:propValue(2)",
run: function () {}, // it's a check
},
{
content: "set quantity to 1",
trigger: "#product_confirmation .js_add_cart_json .fa-minus",
},
{
content: "check that product page has been updated",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(600.00)",
run: function () {}, // it's a check
},
{
content: "check that add to cart modal has been updated",
trigger: "#product_confirmation .oe_price .oe_currency_value:containsExact(600.00)",
extra_trigger: "#product_confirmation input.quantity:propValue(1)",
run: function () {}, // it's a check
},
{
content: "add an optional Warranty",
trigger: ".js_product:contains(Warranty) a:contains(Add to Cart)",
@@ -67,7 +67,7 @@
</div>
</td>
<td class="text-center td-price" name="price">
<ul class="hidden js_add_cart_variants" t-att-data-attribute_value_ids="main_product_attr_ids"></ul>
<ul class="hidden js_add_cart_variants" t-att-data-attribute_value_ids="json.dumps(main_product_attr_ids)"></ul>
<div class="hidden oe_unchanged_value_ids" t-att-data-unchanged_value_ids="main_product_attr_ids[0][1]" ></div>
<span t-attf-class="text-danger oe_default_price oe_striked_price {{ (compute_currency(product.lst_price) - product.website_price) &lt; 0.01 and 'hidden' }}"
t-field="product.lst_price"
@@ -39,7 +39,10 @@ $('.oe_website_sale').each(function() {
$(oe_website_sale).on('change', 'ul[data-attribute_value_ids]', function(event) {
var $ul = $(event.target).closest('.js_add_cart_variants');
var $parent = $ul.closest('.js_product');
var variant_ids = JSON.parse($ul.data("attribute_value_ids").replace(/'/g, '"'));
var variant_ids = $ul.data("attribute_value_ids");
if(_.isString(variant_ids)) {
variant_ids = JSON.parse(variant_ids.replace(/'/g, '"'));
}
var values = [];
$parent.find('input.js_variant_change:checked, select.js_variant_change').each(function() {
values.push(+$(this).val());
@@ -47,7 +50,8 @@ $('.oe_website_sale').each(function() {
var qty = $parent.find('input[name="add_qty"]').val();
for (var k in variant_ids) {
if (_.isEmpty(_.difference(variant_ids[k][1], values))) {
var info = variant_ids[k][4];
// clone so permanent object is not modified
var info = _.clone(variant_ids[k][4]);
if(_.contains(['always', 'threshold'], info['inventory_availability'])) {
info['virtual_available'] -= parseInt(info['cart_qty']);
if (info['virtual_available'] < 0) {
+1 -1
View File
@@ -348,7 +348,7 @@ class Slide(models.Model):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for record in self:
if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']):
record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (base_url, record.id, 315, 420)
record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (base_url, record.id, 315, 420)
elif record.slide_type == 'video' and record.document_id:
if not record.mime_type:
# embed youtube video
@@ -109,11 +109,12 @@ var page_widgets = {};
},
});
$('iframe').ready(function() {
$('iframe.o_wslides_iframe_viewer').ready(function() {
// TODO : make it work. For now, once the iframe is loaded, the value of #page_count is
// still now set (the pdf is still loading)
var max_page = $('iframe').contents().find('#page_count').val();
new SlideSocialEmbed($(this), max_page).setElement($('.oe_slide_js_embed_code_widget'));
var $iframe = $(this);
var max_page = $iframe.contents().find('#page_count').val();
new SlideSocialEmbed($iframe, max_page).setElement($('.oe_slide_js_embed_code_widget'));
});
+13
View File
@@ -0,0 +1,13 @@
Russia, 2018-02-09
D2C 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,
Denis Zaripov mastappl@gmail.com https://github.com/mastappl
List of contributors:
Denis Zaripov mastappl@gmail.com https://github.com/mastappl
+18
View File
@@ -0,0 +1,18 @@
PromptEQUATION, 2017-09-03
PromptEquation/ OdooGap 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,
Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap
List of contributors:
Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap
Diogo Duarte diogocarvalhoduarte@gmail.com https://github.com/dduarte-odoogap
Carlos Fonseca cfonseca@odoogap.com https://github.com/cfonseca-odoogap
Carlos Fonseca carlosmigueldafonseca@gmail.com https://github.com/cpintofonseca
+11
View File
@@ -0,0 +1,11 @@
India, 18-02-2018
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,
Hilar AK hilarak@gmail.com https://github.com/hilarak
+1 -1
View File
@@ -8,4 +8,4 @@ declaration.
Signed,
Laurent Bélorgey lb@lalieutenante.com https://github.com/lbelorgey
Laurent Bélorgey lb@lalieutenante.com https://github.com/lbelorgey
+5 -1
View File
@@ -17,7 +17,11 @@ class AutoVacuum(models.AbstractModel):
for mname in self.env:
model = self.env[mname]
if model.is_transient():
model._transient_vacuum(force=True)
try:
with self._cr.savepoint():
model._transient_vacuum(force=True)
except Exception as e:
_logger.warning("Failed to clean transient model %s\n%s", model, str(e))
@api.model
def _gc_user_logs(self):
@@ -733,16 +733,17 @@ class TestFields(common.TransactionCase):
env = self.env(user=self.env.ref('base.user_demo'))
self.assertEqual(env.user.login, "demo")
# create a new message as demo user
discussion = self.env.ref('test_new_api.discussion_0')
message = env['test_new_api.message'].new({'discussion': discussion})
self.assertEqual(message.discussion, discussion)
with self.env.do_in_onchange():
# create a new message as demo user
discussion = self.env.ref('test_new_api.discussion_0')
message = env['test_new_api.message'].new({'discussion': discussion})
self.assertEqual(message.discussion, discussion)
# read the related field discussion_name
self.assertEqual(message.discussion.env, env)
self.assertEqual(message.discussion_name, discussion.name)
with self.assertRaises(AccessError):
message.discussion.name
# read the related field discussion_name
self.assertEqual(message.discussion.env, env)
self.assertEqual(message.discussion_name, discussion.name)
with self.assertRaises(AccessError):
message.discussion.name
@mute_logger('odoo.addons.base.models.ir_model')
def test_42_new_related(self):
@@ -755,14 +756,15 @@ class TestFields(common.TransactionCase):
env = self.env(user=self.env.ref('base.user_demo'))
self.assertEqual(env.user.login, "demo")
# create a new discussion and a new message as demo user
discussion = env['test_new_api.discussion'].new({'name': 'Stuff'})
message = env['test_new_api.message'].new({'discussion': discussion})
self.assertEqual(message.discussion, discussion)
with self.env.do_in_onchange():
# create a new discussion and a new message as demo user
discussion = env['test_new_api.discussion'].new({'name': 'Stuff'})
message = env['test_new_api.message'].new({'discussion': discussion})
self.assertEqual(message.discussion, discussion)
# read the related field discussion_name
self.assertNotEqual(message.sudo().env, message.env)
self.assertEqual(message.discussion_name, discussion.name)
# read the related field discussion_name
self.assertNotEqual(message.sudo().env, message.env)
self.assertEqual(message.discussion_name, discussion.name)
def test_50_defaults(self):
""" test default values. """
+1 -1
View File
@@ -561,7 +561,7 @@ class Field(MetaField('DummyField', (object,), {})):
# when related_sudo, bypass access rights checks when reading values
others = records.sudo() if self.related_sudo else records
# copy the cache of draft records into others' cache
if records.env != others.env:
if records.env.in_onchange and records.env != others.env:
copy_cache(records - records.filtered('id'), others.env)
#
# Traverse fields one by one for all records, in order to take advantage