[MERGE] forward port branch 11.0 up to d67b410dfc
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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')
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). """
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>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), '&', ('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': ['|', '&', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&', ('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': ['&', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
|
||||
<field name="product_id" required="1" attrs="{'readonly': ['|', '&', ('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)]}"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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) > 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) < 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) {
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user