[MERGE] forward port branch 11.0 up to dbb713c2f8
This commit is contained in:
@@ -27,7 +27,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
res = []
|
||||
total = []
|
||||
cr = self.env.cr
|
||||
user_company = self.env.user.company_id.id
|
||||
company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id,))
|
||||
move_state = ['draft', 'posted']
|
||||
if target_move == 'posted':
|
||||
move_state = ['posted']
|
||||
@@ -41,7 +41,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
if reconciled_after_date:
|
||||
reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)'
|
||||
arg_list += (tuple(reconciled_after_date),)
|
||||
arg_list += (date_from, user_company)
|
||||
arg_list += (date_from, tuple(company_ids))
|
||||
query = '''
|
||||
SELECT DISTINCT l.partner_id, UPPER(res_partner.name)
|
||||
FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am
|
||||
@@ -51,7 +51,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
AND (account_account.internal_type IN %s)
|
||||
AND ''' + reconciliation_clause + '''
|
||||
AND (l.date <= %s)
|
||||
AND l.company_id = %s
|
||||
AND l.company_id IN %s
|
||||
ORDER BY UPPER(res_partner.name)'''
|
||||
cr.execute(query, arg_list)
|
||||
|
||||
@@ -76,8 +76,8 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
AND (COALESCE(l.date_maturity,l.date) > %s)\
|
||||
AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
|
||||
AND (l.date <= %s)
|
||||
AND l.company_id = %s'''
|
||||
cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, user_company))
|
||||
AND l.company_id IN %s'''
|
||||
cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, tuple(company_ids)))
|
||||
aml_ids = cr.fetchall()
|
||||
aml_ids = aml_ids and [x[0] for x in aml_ids] or []
|
||||
for line in self.env['account.move.line'].browse(aml_ids):
|
||||
@@ -117,7 +117,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
else:
|
||||
dates_query += ' <= %s)'
|
||||
args_list += (periods[str(i)]['stop'],)
|
||||
args_list += (date_from, user_company)
|
||||
args_list += (date_from, tuple(company_ids))
|
||||
|
||||
query = '''SELECT l.id
|
||||
FROM account_move_line AS l, account_account, account_move am
|
||||
@@ -127,7 +127,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
|
||||
AND ''' + dates_query + '''
|
||||
AND (l.date <= %s)
|
||||
AND l.company_id = %s'''
|
||||
AND l.company_id IN %s'''
|
||||
cr.execute(query, args_list)
|
||||
partners_amount = {}
|
||||
aml_ids = cr.fetchall()
|
||||
|
||||
@@ -62,9 +62,9 @@ class BaseGengoTranslations(models.TransientModel):
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'res.company',
|
||||
'res_id': self.env.user.company_id.id,
|
||||
'res_model': 'res.config.settings',
|
||||
'target': 'current',
|
||||
'context': {'module' : 'general_settings'},
|
||||
}
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -76,7 +76,8 @@ bus.Bus = Widget.extend({
|
||||
this.last_partners_presence_check = now;
|
||||
}
|
||||
var data = {channels: self.channels, last: self.last, options: options};
|
||||
session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
|
||||
// The backend has a maximum cycle time of 50 seconds so give +10 seconds
|
||||
session.rpc('/longpolling/poll', data, {shadow : true, timeout: 60000}).then(function(result) {
|
||||
self.on_notification(result);
|
||||
if(!self.stop){
|
||||
self.poll();
|
||||
|
||||
@@ -34,16 +34,6 @@
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_rule_lead_assigned_activities" model="ir.rule">
|
||||
<field name="name">Leads with assigned activities</field>
|
||||
<field ref="model_crm_lead" name="model_id"/>
|
||||
<field name="domain_force">[('activity_ids.user_id','=',user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
<record id="crm_rule_all_lead" model="ir.rule">
|
||||
<field name="name">All Leads</field>
|
||||
<field ref="model_crm_lead" name="model_id"/>
|
||||
|
||||
@@ -524,7 +524,7 @@ class HrExpenseSheet(models.Model):
|
||||
|
||||
@api.onchange('employee_id')
|
||||
def _onchange_employee_id(self):
|
||||
self.address_id = self.employee_id.address_home_id
|
||||
self.address_id = self.employee_id.sudo().address_home_id
|
||||
self.department_id = self.employee_id.department_id
|
||||
|
||||
@api.depends('expense_line_ids.total_amount_company')
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<field name="reference"/>
|
||||
<field name="date"/>
|
||||
<field name="account_id" domain="[('internal_type', '=', 'other')]" groups="account.group_account_user"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_id" groups="hr_expense.group_hr_expense_user"/>
|
||||
<field name="sheet_id" invisible="1"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
|
||||
@@ -218,7 +218,12 @@ class Employee(models.Model):
|
||||
|
||||
def write(self, values):
|
||||
res = super(Employee, self).write(values)
|
||||
if 'parent_id' in values:
|
||||
if 'parent_id' in values or 'department_id' in values:
|
||||
holidays = self.env['hr.holidays'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
|
||||
holidays.write({'manager_id': values['parent_id']})
|
||||
hr_vals = {}
|
||||
if values.get('parent_id') is not None:
|
||||
hr_vals['manager_id'] = values['parent_id']
|
||||
if values.get('department_id') is not None:
|
||||
hr_vals['department_id'] = values['department_id']
|
||||
holidays.write(hr_vals)
|
||||
return res
|
||||
|
||||
@@ -217,6 +217,7 @@ class Holidays(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
|
||||
|
||||
@api.multi
|
||||
@api.depends('number_of_days_temp', 'type')
|
||||
|
||||
@@ -158,7 +158,6 @@ class HrContract(models.Model):
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_inherit = 'hr.payslip.run'
|
||||
_description = 'Payslip Run'
|
||||
|
||||
journal_id = fields.Many2one('account.journal', 'Salary Journal', states={'draft': [('readonly', False)]}, readonly=True,
|
||||
required=True, default=lambda self: self.env['account.journal'].search([('type', '=', 'general')], limit=1))
|
||||
|
||||
@@ -545,7 +545,6 @@ result = -contract.car_atn
|
||||
ref('hr_salary_rule_representation_fees'),
|
||||
ref('hr_payroll_rules_suppl_net'),
|
||||
ref('hr_payroll_rules_retained_net'),
|
||||
ref('hr_salary_rule_commission_on_target'),
|
||||
ref('hr_salary_rule_pp_reduction_employees'),
|
||||
|
||||
])]" name="rule_ids"/>
|
||||
@@ -571,7 +570,6 @@ result = -contract.car_atn
|
||||
ref('hr_salary_rule_representation_fees'),
|
||||
ref('hr_payroll_rules_suppl_net'),
|
||||
ref('hr_payroll_rules_retained_net'),
|
||||
ref('hr_salary_rule_commission_on_target'),
|
||||
ref('hr_salary_rule_pp_reduction_workers')
|
||||
])]" name="rule_ids"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
|
||||
@@ -116,8 +116,8 @@ class HrContract(models.Model):
|
||||
@api.depends('commission_on_target')
|
||||
def _compute_warrants_cost(self):
|
||||
for contract in self:
|
||||
contract.warrants_cost = contract.commission_on_target * 1.326 / 1.05 * 12.0
|
||||
contract.warrant_value_employee = contract.warrants_cost * 0.54
|
||||
contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0
|
||||
contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0
|
||||
|
||||
@api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost',
|
||||
'internet', 'mobile', 'mobile_plus')
|
||||
@@ -230,7 +230,7 @@ class HrContract(models.Model):
|
||||
- 12.0 * contract.internet \
|
||||
- 12.0 * (contract.mobile + contract.mobile_plus) \
|
||||
- 12.0 * contract.transport_employer_cost \
|
||||
- (1.326 / 1.05 * 12.0) * contract.commission_on_target \
|
||||
- contract.warrants_cost \
|
||||
- 220.0 * contract.meal_voucher_paid_by_employer
|
||||
gross = remaining_for_gross / (13.0 + 13.0 * 0.3507 + 0.92)
|
||||
return gross
|
||||
|
||||
@@ -74,6 +74,6 @@ class IrModelField(models.Model):
|
||||
|
||||
def _instanciate_attrs(self, field_data):
|
||||
attrs = super(IrModelField, self)._instanciate_attrs(field_data)
|
||||
if field_data.get('track_visibility'):
|
||||
if attrs and field_data.get('track_visibility'):
|
||||
attrs['track_visibility'] = field_data['track_visibility']
|
||||
return attrs
|
||||
|
||||
@@ -211,7 +211,7 @@ class Channel(models.Model):
|
||||
# http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
|
||||
headers['X-Auto-Response-Suppress'] = 'OOF'
|
||||
if self.alias_domain and self.alias_name:
|
||||
headers['List-Id'] = '%s.%s' % (self.alias_name, self.alias_domain)
|
||||
headers['List-Id'] = '<%s.%s>' % (self.alias_name, self.alias_domain)
|
||||
headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name, self.alias_domain)
|
||||
# Avoid users thinking it was a personal message
|
||||
# X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
|
||||
@@ -505,7 +505,15 @@ class Channel(models.Model):
|
||||
partners_to_add = partners - channel.channel_partner_ids
|
||||
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
|
||||
for partner in partners_to_add:
|
||||
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
||||
if partner.id != self.env.user.partner_id.id:
|
||||
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
|
||||
'author': self.env.user.display_name,
|
||||
'new_partner': partner.display_name,
|
||||
'channel_id': channel.id,
|
||||
'channel_name': channel.name,
|
||||
}
|
||||
else:
|
||||
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
|
||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
|
||||
|
||||
# broadcast the channel header to the added partner
|
||||
|
||||
@@ -11,7 +11,7 @@ from odoo.exceptions import UserError, AccessError
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/]{3,}=*)([\'"])', re.I)
|
||||
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])', re.I)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
|
||||
@@ -210,6 +210,10 @@ var Thread = Widget.extend({
|
||||
}
|
||||
},
|
||||
on_click_redirect: function (event) {
|
||||
// ignore inherited branding
|
||||
if ($(event.target).data('oe-field') !== undefined) {
|
||||
return;
|
||||
}
|
||||
var id = $(event.target).data('oe-id');
|
||||
if (id) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -60,7 +60,7 @@ function _parse_and_transform(nodes, transform_function) {
|
||||
|
||||
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
|
||||
var url_regexp = /\b(?:https?:\/\/|(www\.))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi;
|
||||
var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&//=]*)/gi;
|
||||
function linkify(text, attrs) {
|
||||
attrs = attrs || {};
|
||||
if (attrs.target === undefined) {
|
||||
@@ -70,7 +70,7 @@ function linkify(text, attrs) {
|
||||
return key + '="' + _.escape(value) + '"';
|
||||
}).join(' ');
|
||||
return text.replace(url_regexp, function (url) {
|
||||
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
|
||||
var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url;
|
||||
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
odoo.define('mail.mail_utils_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var utils = require('mail.utils');
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
|
||||
QUnit.module('Mail utils');
|
||||
|
||||
QUnit.test('add_link utility function', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var testInputs = {
|
||||
'http://admin:password@example.com:8/%2020': true,
|
||||
'https://admin:password@example.com/test': true,
|
||||
'www.example.com:8/test': true,
|
||||
'https://127.0.0.5:8069': true,
|
||||
'www.127.0.0.5': false,
|
||||
'should.notmatch': false,
|
||||
'fhttps://test.example.com/test': false,
|
||||
};
|
||||
|
||||
_.each(testInputs, function (willLinkify, content) {
|
||||
var output = utils.parse_and_transform(content, utils.add_link);
|
||||
if (willLinkify) {
|
||||
assert.strictEqual(output.indexOf('<a '), 0, "There should be a link");
|
||||
} else {
|
||||
assert.strictEqual(output.indexOf('<a '), -1, "There should be no link");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@
|
||||
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/mail_utils_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
|
||||
</xpath>
|
||||
|
||||
@@ -81,6 +81,11 @@ class MrpBom(models.Model):
|
||||
if self.product_tmpl_id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
|
||||
@api.onchange('routing_id')
|
||||
def onchange_routing_id(self):
|
||||
for line in self.bom_line_ids:
|
||||
line.operation_id = False
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self]
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="tracking" invisible="1" attrs="{'readonly': 1}"/>
|
||||
<field name="product_id" domain="[('type', 'in', ['product', 'consu'])]"/>
|
||||
<label for="product_qty"/>
|
||||
<div class="o_row">
|
||||
@@ -116,7 +116,7 @@
|
||||
<field name="product_uom_qty" string="Quantity"/>
|
||||
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="tax_id" widget="many2many_tags"/>
|
||||
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
|
||||
<field name="price_subtotal" widget="monetary"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -31,49 +31,110 @@ class WebsitePayment(http.Controller):
|
||||
currency_id = currency_id and int(currency_id) or user.company_id.currency_id.id
|
||||
currency = env['res.currency'].browse(currency_id)
|
||||
|
||||
# Try default one then fallback on first
|
||||
acquirer_id = acquirer_id and int(acquirer_id) or \
|
||||
env['ir.default'].get('payment.transaction', 'acquirer_id', company_id=user.company_id.id) or \
|
||||
env['payment.acquirer'].search([('website_published', '=', True), ('company_id', '=', user.company_id.id)])[0].id
|
||||
|
||||
acquirer = env['payment.acquirer'].with_context(submit_class='btn btn-primary pull-right',
|
||||
submit_txt=_('Pay Now')).browse(acquirer_id)
|
||||
acquirers = None
|
||||
if acquirer_id:
|
||||
acquirers = env['payment.acquirer'].browse(int(acquirer_id))
|
||||
if not acquirers:
|
||||
acquirers = env['payment.acquirer'].search([('website_published', '=', True), ('company_id', '=', user.company_id.id)])
|
||||
# auto-increment reference with a number suffix if the reference already exists
|
||||
reference = request.env['payment.transaction'].get_next_reference(reference)
|
||||
|
||||
partner_id = user.partner_id.id if not user._is_public() else False
|
||||
|
||||
payment_form = acquirer.sudo().render(reference, float(amount), currency.id, values={'return_url': '/website_payment/confirm', 'partner_id': partner_id})
|
||||
values = {
|
||||
'reference': reference,
|
||||
'acquirer': acquirer,
|
||||
'currency': currency,
|
||||
'amount': float(amount),
|
||||
'payment_form': payment_form,
|
||||
'return_url': '/website_payment/confirm',
|
||||
'partner_id': partner_id,
|
||||
'bootstrap_formatting': True,
|
||||
'error_msg': kw.get('error_msg')
|
||||
}
|
||||
# TODO: remove return when payment is implemented with refactored payments, i.e. use of
|
||||
# payment.payment_tokens_list in the template.
|
||||
# return request.render('payment.pay', values)
|
||||
return request.render('website.404')
|
||||
|
||||
@http.route(['/website_payment/transaction'], type='json', auth="public", website=True)
|
||||
def transaction(self, reference, amount, currency_id, acquirer_id):
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s']
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form']
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
|
||||
|
||||
return request.render('payment.pay', values)
|
||||
|
||||
def _get_existing_transaction(self, reference, amount, partner_id, currency_id, acquirer_id, tx_id):
|
||||
PaymentTransaction = request.env['payment.transaction']
|
||||
tx = None
|
||||
if tx_id:
|
||||
tx = PaymentTransaction.sudo().browse(tx_id)
|
||||
if not tx.exists() or tx.reference != reference or tx.acquirer_id.id != acquirer_id:
|
||||
tx = None
|
||||
|
||||
if not tx:
|
||||
tx = PaymentTransaction.sudo().search([('reference', '=', reference), ('acquirer_id', '=', acquirer_id)])
|
||||
|
||||
if tx and (tx.state != 'draft' or tx.partner_id.id != partner_id or tx.amount != amount or tx.currency_id.id != currency_id):
|
||||
tx = None
|
||||
|
||||
return tx
|
||||
|
||||
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',], 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)
|
||||
|
||||
tx = self._get_existing_transaction(reference, float(amount), partner_id,
|
||||
int(currency_id), int(acquirer_id), request.session.get('website_payment_tx_id'))
|
||||
|
||||
if not tx:
|
||||
values = {
|
||||
'acquirer_id': int(acquirer_id),
|
||||
'reference': reference,
|
||||
'amount': float(amount),
|
||||
'currency_id': currency_id,
|
||||
'partner_id': partner_id,
|
||||
}
|
||||
|
||||
tx = request.env['payment.transaction'].sudo().create(values)
|
||||
request.session['website_payment_tx_id'] = tx.id
|
||||
|
||||
render_values = {
|
||||
'return_url': '/website_payment/confirm?tx_id=%d' % tx.id,
|
||||
'partner_id': partner_id,
|
||||
}
|
||||
|
||||
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)
|
||||
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
|
||||
token = request.env['payment.token'].browse(int(pm_id))
|
||||
|
||||
if not token:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Cannot setup the payment.'))
|
||||
|
||||
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
|
||||
|
||||
values = {
|
||||
'acquirer_id': int(acquirer_id),
|
||||
'acquirer_id': token.acquirer_id.id,
|
||||
'reference': reference,
|
||||
'amount': float(amount),
|
||||
'currency_id': int(currency_id),
|
||||
'partner_id': partner_id,
|
||||
'payment_token_id': pm_id
|
||||
}
|
||||
|
||||
tx = request.env['payment.transaction'].sudo().create(values)
|
||||
request.session['website_payment_tx_id'] = tx.id
|
||||
return tx.id
|
||||
|
||||
try:
|
||||
res = tx.s2s_do_transaction()
|
||||
except Exception as e:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Payment transaction failed.'))
|
||||
|
||||
valid_state = 'authorized' if tx.acquirer_id.capture_manually else 'done'
|
||||
if not res or tx.state != valid_state:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Payment transaction failed.'))
|
||||
|
||||
return request.redirect(return_url if return_url else '/website_payment/confirm?tx_id=%d' % tx.id)
|
||||
|
||||
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
|
||||
def confirm(self, **kw):
|
||||
tx_id = request.session.pop('website_payment_tx_id', False)
|
||||
tx_id = int(kw.get('tx_id', 0)) or request.session.pop('website_payment_tx_id', 0)
|
||||
if tx_id:
|
||||
tx = request.env['payment.transaction'].browse(tx_id)
|
||||
if tx.state == 'done':
|
||||
|
||||
@@ -1,24 +1,2 @@
|
||||
odoo.define('payment.website_payment', function (require) {
|
||||
"use strict";
|
||||
|
||||
require('web.dom_ready');
|
||||
var ajax = require('web.ajax');
|
||||
|
||||
if (!$('.o_website_payment').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.o_website_payment'");
|
||||
}
|
||||
|
||||
// When clicking on payment button: create the tx using json then continue to the acquirer
|
||||
var $payment = $(".o_website_payment_form");
|
||||
$payment.on("click", 'button[type="submit"],button[name="submit"]', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
$(ev.currentTarget).attr('disabled', true);
|
||||
$(ev.currentTarget).prepend('<i class="fa fa-refresh fa-spin"></i> ');
|
||||
var $form = $(ev.currentTarget).parents('form');
|
||||
var data =$("div[class~='o_website_payment_new_payment']").data();
|
||||
ajax.jsonRpc('/website_payment/transaction/', 'call', data).then(function (result) {
|
||||
$form.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
// This file is kept to avoid breaking assets compilation on instance that didn't run "-u payment".
|
||||
// TBE TODO: Remove me in master
|
||||
@@ -46,32 +46,22 @@
|
||||
<div class="wrap">
|
||||
<div class="container o_website_payment">
|
||||
<h1>Payment</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="oe_structure"></div>
|
||||
<div>
|
||||
<div class="form-horizontal row o_website_payment_new_payment" t-att-data-acquirer_id="acquirer.id" t-att-data-currency_id="currency.id" t-att-data-reference="reference" t-att-data-amount="amount">
|
||||
<div class="form-group">
|
||||
<label for="form_reference" class="col-sm-3 control-label">Reference</label>
|
||||
<div class="col-sm-9">
|
||||
<span name="form_reference" class="form-control" t-esc="reference"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="form_amount" class="col-sm-3 control-label">Amount</label>
|
||||
<div class="col-sm-9">
|
||||
<span name="form_amount" class="form-control" t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9 col-sm-offset-3 text-muted mt16" t-raw="acquirer.pre_msg"/>
|
||||
<div class="col-sm-12 o_website_payment_form" t-raw="payment_form"/>
|
||||
<div class="col-sm-6 col-sm-offset-6 text-muted mt16">
|
||||
<span t-field="acquirer.image_small" t-att-title="acquirer.name" t-options='{"widget": "image", "style":"max-width: 60px; display: inline-block"}'/>
|
||||
<span>Processed by <t t-esc="acquirer.name"/>.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="error_msg" class="alert alert-danger col-md-7">
|
||||
<t t-esc="error_msg"/>
|
||||
</div>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-md-7">
|
||||
<p><b>Reference:</b> <t t-esc="reference"/></p>
|
||||
<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>
|
||||
<div t-if="not (s2s_acquirers or form_acquirers)" class="alert alert-danger">
|
||||
<p>No payment acquirer found.</p>
|
||||
<p>Please configure a payment acquirer.</p>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,11 +108,14 @@ odoo.define('payment_stripe.stripe', function(require) {
|
||||
|
||||
|
||||
if ($('.o_website_payment').length !== 0) {
|
||||
var create_tx = ajax.jsonRpc('/website_payment/transaction', 'call', {
|
||||
reference: $("input[name='invoice_num']").val(),
|
||||
amount: amount, // exact amount, not stripe cents
|
||||
currency_id: currency_id,
|
||||
var invoice_num = $("input[name='invoice_num']").val();
|
||||
var url = _.str.sprintf("/website_payment/transaction/%s/%f/%s",
|
||||
invoice_num, amount, currency_id);
|
||||
|
||||
var create_tx = ajax.jsonRpc(url, 'call', {
|
||||
acquirer_id: acquirer_id
|
||||
}).then(function (data) {
|
||||
try { provider_form[0].innerHTML = data; } catch (e) {}
|
||||
});
|
||||
}
|
||||
else if ($('.o_website_quote').length !== 0) {
|
||||
|
||||
@@ -227,10 +227,14 @@ class PosOrder(models.Model):
|
||||
return InvoiceLine.sudo().create(inv_line)
|
||||
|
||||
def _create_account_move_line(self, session=None, move=None):
|
||||
def _flatten_tax_and_children(taxes):
|
||||
def _flatten_tax_and_children(taxes, group_done=None):
|
||||
children = self.env['account.tax']
|
||||
for tax in taxes:
|
||||
children |= _flatten_tax_and_children(tax.children_tax_ids)
|
||||
if group_done is None:
|
||||
group_done = set()
|
||||
for tax in taxes.filtered(lambda t: t.amount_type == 'group'):
|
||||
if tax.id not in group_done:
|
||||
group_done.add(tax.id)
|
||||
children |= _flatten_tax_and_children(tax.children_tax_ids, group_done)
|
||||
return taxes + children
|
||||
|
||||
# Tricky, via the workflow, we only have one id in the ids variable
|
||||
|
||||
@@ -729,6 +729,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
|
||||
deferred.resolve();
|
||||
};
|
||||
img.crossOrigin = 'use-credentials';
|
||||
img.src = url;
|
||||
|
||||
return deferred;
|
||||
|
||||
@@ -1170,8 +1170,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
var order = this.pos.get_order();
|
||||
if( this.has_client_changed() ){
|
||||
var default_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': this.pos.config.default_fiscal_position_id[0]});
|
||||
if ( this.new_client ) {
|
||||
order.fiscal_position = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
|
||||
if ( this.new_client && this.new_client.property_account_position_id ) {
|
||||
var client_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
|
||||
order.fiscal_position = client_fiscal_position_id || default_fiscal_position_id;
|
||||
order.set_pricelist(_.findWhere(this.pos.pricelists, {'id': this.new_client.property_product_pricelist[0]}) || this.pos.default_pricelist);
|
||||
} else {
|
||||
order.fiscal_position = default_fiscal_position_id;
|
||||
@@ -1277,9 +1278,14 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
.then(function(partner_id){
|
||||
self.saved_client_details(partner_id);
|
||||
},function(type,err){
|
||||
var error_body = _t('Your Internet connection is probably down.');
|
||||
if (err.data) {
|
||||
var except = err.data;
|
||||
error_body = except.arguments && except.arguments[0] || except.message || error_body;
|
||||
}
|
||||
self.gui.show_popup('error',{
|
||||
'title': _t('Error: Could not Save Changes'),
|
||||
'body': _t('Your Internet connection is probably down.'),
|
||||
'body': error_body,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<TranType><t t-esc='transaction_type'/></TranType>
|
||||
<TranCode><t t-esc='transaction_code'/></TranCode>
|
||||
<InvoiceNo><t t-esc='invoice_no'/></InvoiceNo>
|
||||
<Frequency>OneTime</Frequency>
|
||||
<Frequency t-translation="off">OneTime</Frequency>
|
||||
</template>
|
||||
|
||||
<template id='mercury_transaction'>
|
||||
<TStream>
|
||||
<TStream t-translation="off">
|
||||
<Transaction>
|
||||
<t t-call='pos_mercury.mercury_common'/>
|
||||
<OperatorID><t t-esc='operator_id'/></OperatorID>
|
||||
@@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<template id='mercury_voidsale'>
|
||||
<TStream>
|
||||
<TStream t-translation="off">
|
||||
<Transaction>
|
||||
<t t-call='pos_mercury.mercury_common'/>
|
||||
<RefNo><t t-esc='ref_no'/></RefNo>
|
||||
@@ -52,7 +52,7 @@
|
||||
</template>
|
||||
|
||||
<template id='mercury_return'>
|
||||
<TStream>
|
||||
<TStream t-translation="off">
|
||||
<Transaction>
|
||||
<t t-call='pos_mercury.mercury_common'/>
|
||||
<OperatorID><t t-esc='operator_id'/></OperatorID>
|
||||
|
||||
@@ -14,10 +14,12 @@ class CustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(CustomerPortal, self)._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'project_count': request.env['project.project'].search_count([('privacy_visibility', '=', 'portal')]),
|
||||
'task_count': request.env['project.task'].search_count([('project_id.privacy_visibility', '=', 'portal')])
|
||||
})
|
||||
Project = request.env['project.project']
|
||||
Task = request.env['project.task']
|
||||
# portal users can't view projects they don't follow
|
||||
projects = Project.sudo().search([('privacy_visibility', '=', 'portal')])
|
||||
values['project_count'] = Project.search_count([('id', 'in', projects.ids)])
|
||||
values['task_count'] = Task.search_count([('project_id', 'in', projects.ids)])
|
||||
return values
|
||||
|
||||
@http.route(['/my/projects', '/my/projects/page/<int:page>'], type='http', auth="user", website=True)
|
||||
@@ -77,8 +79,6 @@ class CustomerPortal(CustomerPortal):
|
||||
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
domain = [('project_id.privacy_visibility', '=', 'portal')]
|
||||
|
||||
searchbar_sortings = {
|
||||
'date': {'label': _('Newest'), 'order': 'create_date desc'},
|
||||
'name': {'label': _('Title'), 'order': 'name'},
|
||||
@@ -96,7 +96,9 @@ class CustomerPortal(CustomerPortal):
|
||||
'all': {'input': 'all', 'label': _('Search in All')},
|
||||
}
|
||||
# extends filterby criteria with project (criteria name is the project id)
|
||||
projects = request.env['project.project'].search([('privacy_visibility', '=', 'portal')])
|
||||
# Note: portal users can't view projects they don't follow
|
||||
projects = request.env['project.project'].sudo().search([('privacy_visibility', '=', 'portal')])
|
||||
domain = [('project_id', 'in', projects.ids)]
|
||||
for proj in projects:
|
||||
searchbar_filters.update({
|
||||
str(proj.id): {'label': proj.name, 'domain': [('project_id', '=', proj.id)]}
|
||||
|
||||
@@ -593,14 +593,12 @@ class Task(models.Model):
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project(self):
|
||||
default_partner_id = self.env.context.get('default_partner_id')
|
||||
default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else self.env['res.partner']
|
||||
if self.project_id:
|
||||
self.partner_id = self.project_id.partner_id or default_partner
|
||||
if self.project_id.partner_id:
|
||||
self.partner_id = self.project_id.partner_id
|
||||
if self.project_id not in self.stage_id.project_ids:
|
||||
self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
|
||||
else:
|
||||
self.partner_id = default_partner
|
||||
self.stage_id = False
|
||||
|
||||
@api.onchange('user_id')
|
||||
|
||||
@@ -192,13 +192,13 @@ class AccountInvoice(models.Model):
|
||||
if child.type_tax_use != 'none':
|
||||
tax_ids.append((4, child.id, None))
|
||||
price_before = line.get('price', 0.0)
|
||||
line.update({'price': company_currency.round(valuation_price_unit * line['quantity'])})
|
||||
line.update({'price': inv.currency_id.round(valuation_price_unit * line['quantity'])})
|
||||
diff_res.append({
|
||||
'type': 'src',
|
||||
'name': i_line.name[:64],
|
||||
'price_unit': company_currency.round(price_unit - valuation_price_unit),
|
||||
'price_unit': inv.currency_id.round(price_unit - valuation_price_unit),
|
||||
'quantity': line['quantity'],
|
||||
'price': company_currency.round(price_before - line.get('price', 0.0)),
|
||||
'price': inv.currency_id.round(price_before - line.get('price', 0.0)),
|
||||
'account_id': acc,
|
||||
'product_id': line['product_id'],
|
||||
'uom_id': line['uom_id'],
|
||||
|
||||
@@ -695,7 +695,7 @@ class PurchaseOrderLine(models.Model):
|
||||
qty = 0.0
|
||||
price_unit = self._get_stock_move_price_unit()
|
||||
for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
|
||||
qty += move.product_qty
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
template = {
|
||||
'name': self.name or '',
|
||||
'product_id': self.product_id.id,
|
||||
@@ -719,7 +719,14 @@ class PurchaseOrderLine(models.Model):
|
||||
}
|
||||
diff_quantity = self.product_qty - qty
|
||||
if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
||||
template['product_uom_qty'] = diff_quantity
|
||||
quant_uom = self.product_id.uom_id
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
if self.product_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
|
||||
product_qty = self.product_uom._compute_quantity(diff_quantity, quant_uom, rounding_method='HALF-UP')
|
||||
template['product_uom'] = quant_uom.id
|
||||
template['product_uom_qty'] = product_qty
|
||||
else:
|
||||
template['product_uom_qty'] = diff_quantity
|
||||
res.append(template)
|
||||
return res
|
||||
|
||||
|
||||
@@ -174,3 +174,53 @@ class TestCreatePicking(common.TestProductCommon):
|
||||
|
||||
customer_move._action_assign()
|
||||
self.assertEqual(customer_move.state, 'assigned', 'Reservation should work with the new quantity provided by the PO.')
|
||||
|
||||
def test_03_uom(self):
|
||||
""" Buy a dozen of products stocked in units. Check that the quantities on the purchase order
|
||||
lines as well as the received quantities are handled in dozen while the moves themselves
|
||||
are handled in units. Edit the ordered quantities, check that the quantites are correctly
|
||||
updated on the moves. Edit the ir.config_parameter to propagate the uom of the purchase order
|
||||
lines to the moves and edit a last time the ordered quantities. Receive, check the quantities.
|
||||
"""
|
||||
uom_unit = self.env.ref('product.product_uom_unit')
|
||||
uom_dozen = self.env.ref('product.product_uom_dozen')
|
||||
|
||||
self.assertEqual(self.product_id_1.uom_po_id.id, uom_unit.id)
|
||||
|
||||
# buy a dozen
|
||||
po = self.env['purchase.order'].create(self.po_vals)
|
||||
|
||||
po.order_line.product_qty = 1
|
||||
po.order_line.product_uom = uom_dozen.id
|
||||
po.button_confirm()
|
||||
|
||||
# the move should be 12 units
|
||||
# note: move.product_qty = computed field, always in the uom of the quant
|
||||
# move.product_uom_qty = stored field representing the initial demand in move.product_uom
|
||||
move1 = po.picking_ids.move_lines[0]
|
||||
self.assertEqual(move1.product_uom_qty, 12)
|
||||
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# edit the so line, sell 2 dozen, the move should now be 24 units
|
||||
po.order_line.product_qty = 2
|
||||
self.assertEqual(move1.product_uom_qty, 24)
|
||||
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||
self.assertEqual(move1.product_qty, 24)
|
||||
|
||||
# force the propagation of the uom, sell 3 dozen
|
||||
self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
|
||||
po.order_line.product_qty = 3
|
||||
move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
|
||||
self.assertEqual(move2.product_uom_qty, 1)
|
||||
self.assertEqual(move2.product_uom.id, uom_dozen.id)
|
||||
self.assertEqual(move2.product_qty, 12)
|
||||
|
||||
# deliver everything
|
||||
move1.quantity_done = 24
|
||||
move2.quantity_done = 1
|
||||
po.picking_ids.button_validate()
|
||||
|
||||
# check the delivered quantity
|
||||
self.assertEqual(po.order_line.qty_received, 3.0)
|
||||
|
||||
|
||||
@@ -198,3 +198,76 @@ class TestStockValuation(TransactionCase):
|
||||
self.assertEquals(move1.price_unit, price_unit_usd_new_rate)
|
||||
|
||||
self.assertAlmostEqual(self.product1.stock_value, price_unit_usd_new_rate * 10, delta=0.1)
|
||||
|
||||
def test_extra_move_fifo_1(self):
|
||||
""" Check that the extra move when over processing a receipt is correctly merged back in
|
||||
the original move.
|
||||
"""
|
||||
self.product1.product_tmpl_id.cost_method = 'fifo'
|
||||
po1 = self.env['purchase.order'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.product1.name,
|
||||
'product_id': self.product1.id,
|
||||
'product_qty': 10.0,
|
||||
'product_uom': self.product1.uom_po_id.id,
|
||||
'price_unit': 100.0,
|
||||
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
}),
|
||||
],
|
||||
})
|
||||
po1.button_confirm()
|
||||
|
||||
picking1 = po1.picking_ids[0]
|
||||
move1 = picking1.move_lines[0]
|
||||
move1.quantity_done = 15
|
||||
res_dict = picking1.button_validate()
|
||||
self.assertEqual(res_dict['res_model'], 'stock.overprocessed.transfer')
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.action_confirm()
|
||||
|
||||
# there should be only one move
|
||||
self.assertEqual(len(picking1.move_lines), 1)
|
||||
self.assertEqual(move1.price_unit, 100)
|
||||
self.assertEqual(move1.product_qty, 15)
|
||||
self.assertEqual(self.product1.stock_value, 1500)
|
||||
|
||||
def test_backorder_fifo_1(self):
|
||||
""" Check that the backordered move when under processing a receipt correctly keep the
|
||||
price unit of the original move.
|
||||
"""
|
||||
self.product1.product_tmpl_id.cost_method = 'fifo'
|
||||
po1 = self.env['purchase.order'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.product1.name,
|
||||
'product_id': self.product1.id,
|
||||
'product_qty': 10.0,
|
||||
'product_uom': self.product1.uom_po_id.id,
|
||||
'price_unit': 100.0,
|
||||
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
}),
|
||||
],
|
||||
})
|
||||
po1.button_confirm()
|
||||
|
||||
picking1 = po1.picking_ids[0]
|
||||
move1 = picking1.move_lines[0]
|
||||
move1.quantity_done = 5
|
||||
res_dict = picking1.button_validate()
|
||||
self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation')
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
|
||||
self.assertEqual(len(picking1.move_lines), 1)
|
||||
self.assertEqual(move1.price_unit, 100)
|
||||
self.assertEqual(move1.product_qty, 5)
|
||||
|
||||
picking2 = po1.picking_ids.filtered(lambda p: p.backorder_id)
|
||||
move2 = picking2.move_lines[0]
|
||||
self.assertEqual(len(picking2.move_lines), 1)
|
||||
self.assertEqual(move2.price_unit, 100)
|
||||
self.assertEqual(move2.product_qty, 5)
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.multi
|
||||
def _sale_postprocess(self, values, additional_so_lines=None):
|
||||
if 'so_line' not in values: # allow to force a False value for so_line
|
||||
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no').with_context(sale_analytic_norecompute=True)._sale_determine_order_line()
|
||||
# only take the AAL from expense or vendor bill, meaning having a negative amount
|
||||
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0).with_context(sale_analytic_norecompute=True)._sale_determine_order_line()
|
||||
self.filtered(lambda aal: aal.amount <= 0).with_context(sale_analytic_norecompute=True)._sale_determine_order_line()
|
||||
|
||||
if any(field_name in values for field_name in self._sale_get_fields_delivered_qty()):
|
||||
if not self._context.get('sale_analytic_norecompute'):
|
||||
|
||||
@@ -166,8 +166,8 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
mnf_product_a = self.env['mrp.production'].search([('product_id', '=', product_a.id)])
|
||||
|
||||
self.assertTrue(mnf_product_a, 'Manufacturing order not created.')
|
||||
self.assertEqual(mnf_product_a.product_qty, 10, 'Wrong product quantity in manufacturing order.')
|
||||
self.assertEqual(mnf_product_a.product_uom_id, self.uom_dozen, 'Wrong unit of measure in manufacturing order.')
|
||||
self.assertEqual(mnf_product_a.product_qty, 120, 'Wrong product quantity in manufacturing order.')
|
||||
self.assertEqual(mnf_product_a.product_uom_id, self.uom_unit, 'Wrong unit of measure in manufacturing order.')
|
||||
self.assertEqual(mnf_product_a.state, 'confirmed', 'Manufacturing order should be confirmed.')
|
||||
|
||||
# ------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -238,7 +238,7 @@ class SaleOrderLine(models.Model):
|
||||
continue
|
||||
qty = 0.0
|
||||
for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += move.product_qty
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
|
||||
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
|
||||
continue
|
||||
|
||||
@@ -263,8 +263,16 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
values = line._prepare_procurement_values(group_id=group_id)
|
||||
product_qty = line.product_uom_qty - qty
|
||||
|
||||
procurement_uom = line.product_uom
|
||||
quant_uom = line.product_id.uom_id
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
|
||||
product_qty = line.product_uom._compute_quantity(product_qty, quant_uom, rounding_method='HALF-UP')
|
||||
procurement_uom = quant_uom
|
||||
|
||||
try:
|
||||
self.env['procurement.group'].run(line.product_id, product_qty, line.product_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, values)
|
||||
self.env['procurement.group'].run(line.product_id, product_qty, procurement_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, values)
|
||||
except UserError as error:
|
||||
errors.append(error.name)
|
||||
if errors:
|
||||
|
||||
@@ -244,8 +244,8 @@ class TestSaleStock(TestSale):
|
||||
the new move lines.
|
||||
"""
|
||||
# sell two products
|
||||
item1 = self.products['prod_order']
|
||||
item2 = self.products['prod_del']
|
||||
item1 = self.products['prod_order'] # consumable
|
||||
item2 = self.products['prod_del'] # stockable
|
||||
|
||||
self.so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
@@ -256,6 +256,66 @@ class TestSaleStock(TestSale):
|
||||
})
|
||||
self.so.action_confirm()
|
||||
|
||||
# deliver them
|
||||
# One of the move is for a consumable product, thus is assigned. The second one is for a
|
||||
# stockable product, thus is unavailable. Hitting `button_validate` will first ask to
|
||||
# process all the reserved quantities and, if the user chose to process, a second wizard
|
||||
# will ask to create a backorder for the unavailable product.
|
||||
self.assertEquals(len(self.so.picking_ids), 1)
|
||||
res_dict = self.so.picking_ids[0].button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
self.assertEqual(wizard._name, 'stock.immediate.transfer')
|
||||
res_dict = wizard.process()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
self.assertEqual(wizard._name, 'stock.backorder.confirmation')
|
||||
wizard.process()
|
||||
|
||||
# Now, the original picking is done and there is a new one (the backorder).
|
||||
self.assertEquals(len(self.so.picking_ids), 2)
|
||||
for picking in self.so.picking_ids:
|
||||
move = picking.move_lines
|
||||
if picking.backorder_id:
|
||||
self.assertEqual(move.product_id.id, item2.id)
|
||||
self.assertEqual(move.state, 'confirmed')
|
||||
else:
|
||||
self.assertEqual(picking.move_lines.product_id.id, item1.id)
|
||||
self.assertEqual(move.state, 'done')
|
||||
|
||||
# update the two original sale order lines
|
||||
self.so.write({
|
||||
'order_line': [
|
||||
(1, self.so.order_line[0].id, {'product_uom_qty': 2}),
|
||||
(1, self.so.order_line[1].id, {'product_uom_qty': 2}),
|
||||
]
|
||||
})
|
||||
# a single picking should be created for the new delivery
|
||||
self.assertEquals(len(self.so.picking_ids), 2)
|
||||
backorder = self.so.picking_ids.filtered(lambda p: p.backorder_id)
|
||||
self.assertEqual(len(backorder.move_lines), 2)
|
||||
for backorder_move in backorder.move_lines:
|
||||
if backorder_move.product_id.id == item1.id:
|
||||
self.assertEqual(backorder_move.product_qty, 1)
|
||||
elif backorder_move.product_id.id == item2.id:
|
||||
self.assertEqual(backorder_move.product_qty, 2)
|
||||
|
||||
def test_05_create_picking_update_saleorderline(self):
|
||||
""" Same test than test_04 but only with enough products in stock so that the reservation
|
||||
is successful.
|
||||
"""
|
||||
# sell two products
|
||||
item1 = self.products['prod_order'] # consumable
|
||||
item2 = self.products['prod_del'] # stockable
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(item2, self.env.ref('stock.stock_location_stock'), 2)
|
||||
self.so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
|
||||
(0, 0, {'name': item2.name, 'product_id': item2.id, 'product_uom_qty': 1, 'product_uom': item2.uom_id.id, 'price_unit': item2.list_price}),
|
||||
],
|
||||
})
|
||||
self.so.action_confirm()
|
||||
|
||||
# deliver them
|
||||
self.assertEquals(len(self.so.picking_ids), 1)
|
||||
self.so.picking_ids[0].force_assign()
|
||||
@@ -303,3 +363,61 @@ class TestSaleStock(TestSale):
|
||||
self.assertEqual(len(so1.picking_ids), 2)
|
||||
picking2 = so1.picking_ids.filtered(lambda p: p.state != 'cancel')
|
||||
self.assertEqual(picking2.partner_id.id, partner2)
|
||||
|
||||
def test_06_uom(self):
|
||||
""" Sell a dozen of products stocked in units. Check that the quantities on the sale order
|
||||
lines as well as the delivered quantities are handled in dozen while the moves themselves
|
||||
are handled in units. Edit the ordered quantities, check that the quantites are correctly
|
||||
updated on the moves. Edit the ir.config_parameter to propagate the uom of the sale order
|
||||
lines to the moves and edit a last time the ordered quantities. Deliver, check the
|
||||
quantities.
|
||||
"""
|
||||
uom_unit = self.env.ref('product.product_uom_unit')
|
||||
uom_dozen = self.env.ref('product.product_uom_dozen')
|
||||
item1 = self.products['prod_order']
|
||||
|
||||
self.assertEqual(item1.uom_id.id, uom_unit.id)
|
||||
|
||||
# sell a dozen
|
||||
so1 = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': item1.name,
|
||||
'product_id': item1.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': uom_dozen.id,
|
||||
'price_unit': item1.list_price,
|
||||
})],
|
||||
})
|
||||
so1.action_confirm()
|
||||
|
||||
# the move should be 12 units
|
||||
# note: move.product_qty = computed field, always in the uom of the quant
|
||||
# move.product_uom_qty = stored field representing the initial demand in move.product_uom
|
||||
move1 = so1.picking_ids.move_lines[0]
|
||||
self.assertEqual(move1.product_uom_qty, 12)
|
||||
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# edit the so line, sell 2 dozen, the move should now be 24 units
|
||||
so1.order_line.product_uom_qty = 2
|
||||
self.assertEqual(move1.product_uom_qty, 24)
|
||||
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||
self.assertEqual(move1.product_qty, 24)
|
||||
|
||||
# force the propagation of the uom, sell 3 dozen
|
||||
self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
|
||||
so1.order_line.product_uom_qty = 3
|
||||
move2 = so1.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
|
||||
self.assertEqual(move2.product_uom_qty, 1)
|
||||
self.assertEqual(move2.product_uom.id, uom_dozen.id)
|
||||
self.assertEqual(move2.product_qty, 12)
|
||||
|
||||
# deliver everything
|
||||
move1.quantity_done = 24
|
||||
move2.quantity_done = 1
|
||||
so1.picking_ids.button_validate()
|
||||
|
||||
# check the delivered quantity
|
||||
self.assertEqual(so1.order_line.qty_delivered, 3.0)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class ProductTemplate(models.Model):
|
||||
def _inverse_service_policy(self):
|
||||
for product in self:
|
||||
policy = product.service_policy
|
||||
if not policy:
|
||||
if not policy and not product.invoice_policy =='delivery':
|
||||
product.invoice_policy = 'order'
|
||||
product.service_type = 'manual'
|
||||
elif policy == 'ordered_timesheet':
|
||||
|
||||
@@ -232,7 +232,7 @@ class CrmTeam(models.Model):
|
||||
values[index][y_field] = data_item.get('y_value')
|
||||
|
||||
elif self.dashboard_graph_group == 'week':
|
||||
weeks_in_start_year = int(date(start_date.year, 12, 31).isocalendar()[1])
|
||||
weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years
|
||||
for week in range(0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1):
|
||||
short_name = get_week_name(start_date + relativedelta(days=7 * week), locale)
|
||||
values.append({x_field: short_name, y_field: 0})
|
||||
|
||||
@@ -365,8 +365,14 @@ class Product(models.Model):
|
||||
|
||||
def write(self, values):
|
||||
res = super(Product, self).write(values)
|
||||
if 'active' in values and not values['active'] and self.mapped('orderpoint_ids').filtered(lambda r: r.active):
|
||||
raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.'))
|
||||
if 'active' in values and not values['active']:
|
||||
products = self.mapped('orderpoint_ids').filtered(lambda r: r.active).mapped('product_id')
|
||||
if products:
|
||||
msg = _('You still have some active reordering rules on this product. Please archive or delete them first.')
|
||||
msg += '\n\n'
|
||||
for product in products:
|
||||
msg += '- %s \n' % product.display_name
|
||||
raise UserError(msg)
|
||||
return res
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
|
||||
@@ -952,7 +952,8 @@ class StockMove(models.Model):
|
||||
def _prepare_extra_move_vals(self, qty):
|
||||
vals = {
|
||||
'product_uom_qty': qty,
|
||||
'picking_id': self.picking_id.id
|
||||
'picking_id': self.picking_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
}
|
||||
return vals
|
||||
|
||||
@@ -1080,6 +1081,7 @@ class StockMove(models.Model):
|
||||
'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
|
||||
'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
|
||||
'origin_returned_move_id': self.origin_returned_move_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
}
|
||||
return vals
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import time
|
||||
from itertools import groupby
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools.float_utils import float_compare, float_round
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
from operator import itemgetter
|
||||
@@ -443,7 +443,7 @@ class Picking(models.Model):
|
||||
for picking in self:
|
||||
if self._context.get('planned_picking') and picking.state == 'draft':
|
||||
picking.show_validate = False
|
||||
elif picking.state not in ('draft', 'confirmed', 'assigned') or not picking.is_locked:
|
||||
elif picking.state not in ('draft', 'waiting', 'confirmed', 'assigned') or not picking.is_locked:
|
||||
picking.show_validate = False
|
||||
else:
|
||||
picking.show_validate = True
|
||||
@@ -594,8 +594,7 @@ class Picking(models.Model):
|
||||
@return: True
|
||||
"""
|
||||
# TDE FIXME: remove decorator when migration the remaining
|
||||
# TDE FIXME: draft -> automatically done, if waiting ?? CLEAR ME
|
||||
todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'partially_available', 'assigned', 'confirmed'])
|
||||
todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'waiting', 'partially_available', 'assigned', 'confirmed'])
|
||||
# Check if there are ops not linked to moves yet
|
||||
for pick in self:
|
||||
# # Explode manually added packages
|
||||
@@ -684,10 +683,10 @@ class Picking(models.Model):
|
||||
|
||||
# If no lots when needed, raise error
|
||||
picking_type = self.picking_type_id
|
||||
no_quantities_done = all(line.qty_done == 0.0 for line in self.move_line_ids)
|
||||
no_initial_demand = all(move.product_uom_qty == 0.0 for move in self.move_lines)
|
||||
if no_initial_demand and no_quantities_done:
|
||||
raise UserError(_('You cannot validate a transfer if you have not processed any quantity.'))
|
||||
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
if no_reserved_quantities and no_quantities_done:
|
||||
raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.'))
|
||||
|
||||
if picking_type.use_create_lots or picking_type.use_existing_lots:
|
||||
lines_to_check = self.move_line_ids
|
||||
@@ -832,7 +831,7 @@ class Picking(models.Model):
|
||||
|
||||
def _put_in_pack(self):
|
||||
package = False
|
||||
for pick in self:
|
||||
for pick in self.filtered(lambda p: p.state not in ('done', 'cancel')):
|
||||
operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id)
|
||||
operation_ids = self.env['stock.move.line']
|
||||
if operations:
|
||||
|
||||
@@ -42,17 +42,3 @@ class ProductionLot(models.Model):
|
||||
# We only care for the quants in internal or transit locations.
|
||||
quants = self.quant_ids.filtered(lambda q: q.location_id.usage in ['internal', 'transit'])
|
||||
self.product_qty = sum(quants.mapped('quantity'))
|
||||
|
||||
@api.multi
|
||||
def action_traceability(self):
|
||||
move_ids = self.mapped('quant_ids').mapped('history_ids').ids
|
||||
if not move_ids:
|
||||
return False
|
||||
return {
|
||||
'domain': [('id', 'in', move_ids)],
|
||||
'name': _('Traceability'),
|
||||
'view_mode': 'tree,form',
|
||||
'view_type': 'form',
|
||||
'context': {'tree_view_ref': 'stock.view_move_tree'},
|
||||
'res_model': 'stock.move',
|
||||
'type': 'ir.actions.act_window'}
|
||||
|
||||
+118
-70
@@ -2356,19 +2356,18 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location, lot_id=lot2, package_id=package1), 1.0)
|
||||
|
||||
def test_immediate_validate_1(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
Move line quantity should be set to their reservation quantity automatically
|
||||
""" In a picking with a single available move, clicking on validate without filling any
|
||||
quantities should open a wizard asking to process all the reservation (so, the whole move).
|
||||
"""
|
||||
owner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
partner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'partner_id': owner.id,
|
||||
'partner_id': partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
})
|
||||
# move from shelf1
|
||||
self.env['stock.move'].create({
|
||||
'name': 'test_edit_moveline_1',
|
||||
'name': 'test_immediate_validate_1',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'picking_id': picking.id,
|
||||
@@ -2379,29 +2378,28 @@ class StockMove(TransactionCase):
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
res_dict = picking.button_validate()
|
||||
self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
|
||||
|
||||
def test_immediate_validate_2(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
Move line quantity should be set to their reservation quantity automatically.
|
||||
If the Initial demand quantity is greater than the available quantity. It should only
|
||||
set the quantity done to the available(reserved) quantity and create a backorder with the
|
||||
remaining quantity.
|
||||
""" In a picking with a single partially available move, clicking on validate without
|
||||
filling any quantities should open a wizard asking to process all the reservation (so, only
|
||||
a part of the initial demand). Validating this wizard should open another one asking for
|
||||
the creation of a backorder. If the backorder is created, it should contain the quantities
|
||||
not processed.
|
||||
"""
|
||||
owner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
partner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 5.0)
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': owner.id,
|
||||
'partner_id': partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
# move from shelf1
|
||||
self.env['stock.move'].create({
|
||||
'name': 'test_edit_moveline_1',
|
||||
'name': 'test_immediate_validate_2',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_id': picking.id,
|
||||
@@ -2411,67 +2409,96 @@ class StockMove(TransactionCase):
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
# Only 5 products are reserved on the move of 10, click on `button_validate`.
|
||||
res_dict = picking.button_validate()
|
||||
self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
res_dict_for_back_order = wizard.process()
|
||||
self.assertEqual(res_dict_for_back_order.get('res_model'), 'stock.backorder.confirmation')
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
# Chose to create a backorder.
|
||||
backorder_wizard.process()
|
||||
|
||||
self.assertEqual(len(picking.move_lines.move_line_ids), 1)
|
||||
# Only 5 products should be processed on the initial move.
|
||||
self.assertEqual(picking.move_lines.state, 'done')
|
||||
self.assertEqual(picking.move_lines.quantity_done, 5.0)
|
||||
self.assertEqual(picking.move_lines.move_line_ids.qty_done, 5.0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0)
|
||||
|
||||
# The backoder should contain a move for the other 5 produts.
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
|
||||
self.assertEqual(len(backorder), 1.0)
|
||||
self.assertEqual(backorder.move_lines.product_uom_qty, 5.0)
|
||||
|
||||
def test_immediate_validate_3(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
In case of a force assign it should ignore the reserved quantity and set
|
||||
the quantity done to the initial demand. It should also create the move line.
|
||||
""" In a picking with two moves, one partially available and one unavailable, clicking
|
||||
on validate without filling any quantities should open a wizard asking to process all the
|
||||
reservation (so, only a part of one of the moves). Validating this wizard should open
|
||||
another one asking for the creation of a backorder. If the backorder is created, it should
|
||||
contain the quantities not processed.
|
||||
"""
|
||||
owner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
product5 = self.env['product.product'].create({
|
||||
'name': 'Product 5',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
|
||||
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': owner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
# move from shelf1
|
||||
self.env['stock.move'].create({
|
||||
'name': 'test_edit_moveline_1',
|
||||
product1_move = self.env['stock.move'].create({
|
||||
'name': 'product1_move',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_id': picking.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10.0,
|
||||
'product_uom_qty': 100,
|
||||
})
|
||||
product5_move = self.env['stock.move'].create({
|
||||
'name': 'product3_move',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_id': picking.id,
|
||||
'product_id': product5.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 100,
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.force_assign()
|
||||
res_dict = picking.button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
picking.action_assign()
|
||||
|
||||
self.assertEqual(picking.move_lines.quantity_done, 10.0)
|
||||
# Check move_lines data
|
||||
self.assertEqual(len(picking.move_lines.move_line_ids), 1)
|
||||
self.assertEqual(picking.move_lines.move_line_ids.qty_done, 10.0)
|
||||
self.assertEqual(picking.move_lines.move_line_ids.product_qty, 0.0)
|
||||
# Check quants data
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, allow_negative=True), -10.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||
# product1_move should be partially available (1/100), product5_move should be totally
|
||||
# unavailable (0/100)
|
||||
self.assertEqual(product1_move.state, 'partially_available')
|
||||
self.assertEqual(product5_move.state, 'confirmed')
|
||||
|
||||
action = picking.button_validate()
|
||||
self.assertEqual(action.get('res_model'), 'stock.immediate.transfer')
|
||||
wizard = self.env[(action.get('res_model'))].browse(action.get('res_id'))
|
||||
action = wizard.process()
|
||||
self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
|
||||
self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
|
||||
wizard = self.env[(action.get('res_model'))].browse(action.get('res_id'))
|
||||
wizard.process()
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
|
||||
self.assertEqual(len(backorder), 1.0)
|
||||
|
||||
# The backorder should contain 99 product1 and 100 product5.
|
||||
for backorder_move in backorder.move_lines:
|
||||
if backorder_move.product_id.id == self.product1.id:
|
||||
self.assertEqual(backorder_move.product_qty, 99)
|
||||
elif backorder_move.product_id.id == product5.id:
|
||||
self.assertEqual(backorder_move.product_qty, 100)
|
||||
|
||||
def test_immediate_validate_4(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
If the product is tracked by lot. The user should set the lot
|
||||
and the quantity done himself before validate. Otherwise it should
|
||||
return a UserError.
|
||||
""" In a picking with a single available tracked by lot move, clicking on validate without
|
||||
filling any quantities should open an UserError.
|
||||
"""
|
||||
owner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
partner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
lot1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.product3.id,
|
||||
@@ -2480,12 +2507,12 @@ class StockMove(TransactionCase):
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': owner.id,
|
||||
'partner_id': partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
# move from shelf1
|
||||
self.env['stock.move'].create({
|
||||
'name': 'test_edit_moveline_1',
|
||||
'name': 'test_immediate_validate_4',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_id': picking.id,
|
||||
@@ -2495,10 +2522,11 @@ class StockMove(TransactionCase):
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
# No quantites/lot filled, it should raise.
|
||||
with self.assertRaises(UserError):
|
||||
picking.button_validate()
|
||||
picking.move_lines.move_line_ids[0].qty_done = 5.0
|
||||
# We do not need to catch wizard since everything is already defined and the action_done will be processed immediately
|
||||
# All the information are present (lots and quantities), the wizard won't be opened.
|
||||
picking.button_validate()
|
||||
|
||||
self.assertEqual(picking.move_lines.quantity_done, 5.0)
|
||||
@@ -2535,9 +2563,9 @@ class StockMove(TransactionCase):
|
||||
return picking
|
||||
|
||||
def test_immediate_validate_5(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
Test that tracked products can be received without specifying a serial
|
||||
number when the picking type is configured that way.
|
||||
""" In a receipt with a single tracked by serial numbers move, clicking on validate without
|
||||
filling any quantities nor lot should open an UserError except if the picking type is
|
||||
configured to allow otherwise.
|
||||
"""
|
||||
picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
product_id = self.product2
|
||||
@@ -2555,23 +2583,17 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(picking.state, 'done')
|
||||
|
||||
def test_immediate_validate_6(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
This tests three cases:
|
||||
- if a user has processed no quantities and at least one product is tracked,
|
||||
he should specify lots
|
||||
- if a user has processed some quantities then it's ok because a backorder
|
||||
will be created for the ones not filled in
|
||||
- if a user overprocesses a move he will be asked to confirm if this is ok
|
||||
""" In a receipt picking with two moves, one tracked and one untracked, clicking on
|
||||
validate without filling any quantities should displays an UserError as long as no quantity
|
||||
done and lot_name is set on the tracked move. Now if the user validates the picking, the
|
||||
wizard telling the user all reserved quantities will be processed will NOT be opened. This
|
||||
wizard is only opene if no quantities were filled. So validating the picking at this state
|
||||
will open another wizard asking for the creation of a backorder. Now, if the user processed
|
||||
on the second move more than the reservation, a wizard will ask him to confirm.
|
||||
"""
|
||||
picking_type = self.env.ref('stock.picking_type_in')
|
||||
picking_type.use_create_lots = True
|
||||
picking_type.use_existing_lots = False
|
||||
lot1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.product3.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(self.product3, self.stock_location, 1, lot_id=lot1)
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
@@ -2597,16 +2619,15 @@ class StockMove(TransactionCase):
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
picking.button_validate()
|
||||
|
||||
product3_move.move_line_ids[0].qty_done = 1
|
||||
# should still complain about lots because no lot_{name,id} was set
|
||||
with self.assertRaises(UserError):
|
||||
picking.button_validate()
|
||||
|
||||
product3_move.move_line_ids[0].lot_name = '271828'
|
||||
action = picking.button_validate() # should open backorder wizard
|
||||
|
||||
self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
|
||||
self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
|
||||
|
||||
@@ -2615,6 +2636,33 @@ class StockMove(TransactionCase):
|
||||
self.assertTrue(isinstance(action, dict), 'Should open overprocessing wizard')
|
||||
self.assertEqual(action.get('res_model'), 'stock.overprocessed.transfer')
|
||||
|
||||
def test_immediate_validate_7(self):
|
||||
""" In a picking with a single unavailable move, clicking on validate without filling any
|
||||
quantities should display an UserError telling the user he cannot process a picking without
|
||||
any processed quantity.
|
||||
"""
|
||||
partner = self.env['res.partner'].create({'name': 'Jean'})
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'partner_id': partner.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': 'test_immediate_validate_2',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_id': picking.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10.0,
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
# No products are reserved on the move of 10, click on `button_validate`.
|
||||
with self.assertRaises(UserError):
|
||||
picking.button_validate()
|
||||
|
||||
def test_set_quantity_done_1(self):
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_set_quantity_done_1',
|
||||
|
||||
@@ -23,11 +23,8 @@ class StockImmediateTransfer(models.TransientModel):
|
||||
if picking.state != 'assigned':
|
||||
raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
|
||||
for move in picking.move_lines:
|
||||
if move.move_line_ids:
|
||||
for move_line in move.move_line_ids:
|
||||
move_line.qty_done = move_line.product_uom_qty
|
||||
else:
|
||||
move.quantity_done = move.product_uom_qty
|
||||
for move_line in move.move_line_ids:
|
||||
move_line.qty_done = move_line.product_uom_qty
|
||||
if picking._check_backorder():
|
||||
pick_to_backorder |= picking
|
||||
continue
|
||||
|
||||
@@ -30,13 +30,13 @@ class AccountInvoice(models.Model):
|
||||
company_currency = inv.company_id.currency_id
|
||||
price_unit = i_line._get_anglo_saxon_price_unit()
|
||||
if inv.currency_id != company_currency:
|
||||
currency_id = inv.currency_id.id
|
||||
currency = inv.currency_id
|
||||
amount_currency = i_line._get_price(company_currency, price_unit)
|
||||
else:
|
||||
currency_id = False
|
||||
currency = False
|
||||
amount_currency = False
|
||||
|
||||
return self.env['product.product']._anglo_saxon_sale_move_lines(i_line.name, i_line.product_id, i_line.uom_id, i_line.quantity, price_unit, currency=currency_id, amount_currency=amount_currency, fiscal_position=inv.fiscal_position_id, account_analytic=i_line.account_analytic_id, analytic_tags=i_line.analytic_tag_ids)
|
||||
return self.env['product.product']._anglo_saxon_sale_move_lines(i_line.name, i_line.product_id, i_line.uom_id, i_line.quantity, price_unit, currency=currency, amount_currency=amount_currency, fiscal_position=inv.fiscal_position_id, account_analytic=i_line.account_analytic_id, analytic_tags=i_line.analytic_tag_ids)
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
|
||||
@@ -234,7 +234,7 @@ class ProductProduct(models.Model):
|
||||
'product_id': product.id,
|
||||
'uom_id': uom.id,
|
||||
'account_analytic_id': account_analytic and account_analytic.id,
|
||||
'analytic_tag_ids': analytic_tags and analytic_tag.ids and [(6, 0, analytic_tag.ids)] or False,
|
||||
'analytic_tag_ids': analytic_tags and analytic_tags.ids and [(6, 0, analytic_tags.ids)] or False,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -259,7 +259,7 @@ class ProductProduct(models.Model):
|
||||
price = self.standard_price
|
||||
if not uom or self.uom_id.id == uom.id:
|
||||
return price
|
||||
return self.uom_id._compute_price(self.uom_id.id, price, to_uom_id=uom.id)
|
||||
return self.uom_id._compute_price(price, uom)
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
|
||||
@@ -1382,7 +1382,7 @@ class ExportFormat(object):
|
||||
model, fields, ids, domain, import_compat = \
|
||||
operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params)
|
||||
|
||||
Model = request.env[model].with_context(**params.get('context', {}))
|
||||
Model = request.env[model].with_context(import_compat=import_compat, **params.get('context', {}))
|
||||
records = Model.browse(ids) or Model.search(domain, offset=0, limit=False, order=False)
|
||||
|
||||
if not Model._is_an_ordinary_table():
|
||||
|
||||
@@ -59,7 +59,7 @@ return AbstractWebClient.extend({
|
||||
update_logo: function(reload) {
|
||||
var company = session.company_id;
|
||||
var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
|
||||
this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "#" + Date.now() : ''));
|
||||
this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
|
||||
this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
|
||||
},
|
||||
logo_edit: function(ev) {
|
||||
@@ -83,13 +83,8 @@ return AbstractWebClient.extend({
|
||||
action_buttons: true,
|
||||
headless: true,
|
||||
};
|
||||
self.action_manager.do_action(result).then(function () {
|
||||
var form = self.action_manager.dialog_widget.views.form.controller;
|
||||
form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
|
||||
form.on('record_saved', self, function() {
|
||||
self.action_manager.dialog_stop();
|
||||
self.update_logo();
|
||||
});
|
||||
self.action_manager.do_action(result, {
|
||||
on_close: self.update_logo.bind(self, true),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ var Domain = collections.Tree.extend({
|
||||
*/
|
||||
stringToArray: function (domain, evalContext) {
|
||||
if (!_.isString(domain)) return _.clone(domain);
|
||||
return pyeval.eval("domain", domain || "[]", evalContext);
|
||||
return pyeval.eval("domain", domain ? domain.replace(/%%/g, '%') : "[]", evalContext);
|
||||
},
|
||||
/**
|
||||
* Makes implicit "&" operators explicit in the given JS prefix-array
|
||||
|
||||
@@ -392,6 +392,23 @@ var utils = {
|
||||
array[i2] = elem1;
|
||||
array[i1] = elem2;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {boolean} allow_mailto
|
||||
* @returns boolean
|
||||
*/
|
||||
is_email: function (value, allow_mailto) {
|
||||
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
var re;
|
||||
if (allow_mailto) {
|
||||
re = /^(mailto:)?(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||
} else {
|
||||
re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||
}
|
||||
return re.test(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {any} str
|
||||
* @param {any} elseValues
|
||||
|
||||
@@ -332,7 +332,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
}
|
||||
self.model.discardChanges(recordID);
|
||||
if (self.model.isNew(recordID)) {
|
||||
self._abandonRecord(recordID);
|
||||
if (self.model.canBeAbandoned(recordID)) {
|
||||
self._abandonRecord(recordID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return self._confirmSave(recordID);
|
||||
|
||||
@@ -477,6 +477,30 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
return _t("New");
|
||||
},
|
||||
/**
|
||||
* Returns true if a record can be abandoned from a list datapoint.
|
||||
*
|
||||
* A record cannot be abandonned if it has been registered as "added"
|
||||
* in the parent's savepoint, otherwise it can be abandonned.
|
||||
*
|
||||
* This is useful when discarding changes on this record, as it means that
|
||||
* we must keep the record even if some fields are invalids (e.g. required
|
||||
* field is empty).
|
||||
*
|
||||
* @param {string} id id for a local resource
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canBeAbandoned: function (id) {
|
||||
var data = this.localData[id];
|
||||
var parent = this.localData[data.parentID];
|
||||
var abandonable = true;
|
||||
if (parent) {
|
||||
abandonable = !_.some(parent._savePoint, function (entry) {
|
||||
return entry.operation === 'ADD' && entry.id === id;
|
||||
});
|
||||
}
|
||||
return abandonable;
|
||||
},
|
||||
/**
|
||||
* Returns true if a record is dirty. A record is considered dirty if it has
|
||||
* some unsaved changes, marked by the _isDirty property on the record or
|
||||
@@ -1428,25 +1452,23 @@ var BasicModel = AbstractModel.extend({
|
||||
defs.push(self._applyOnChange(command[2], rec));
|
||||
} else if (command[0] === 4) {
|
||||
// LINK TO
|
||||
rec = self.localData[list._cache[command[1]]];
|
||||
if (rec) {
|
||||
// modifications done on a record are discarded if
|
||||
// the onchange uses a LINK TO
|
||||
self.discardChanges(rec.id);
|
||||
}
|
||||
// the dataPoint id will be set when the record will be
|
||||
// fetched (for now, this dataPoint may not exist yet)
|
||||
list._changes.push({
|
||||
operation: 'ADD',
|
||||
id: rec ? rec.id : null,
|
||||
resID: command[1],
|
||||
});
|
||||
linkRecord(list, command[1]);
|
||||
} else if (command[0] === 5) {
|
||||
// DELETE ALL
|
||||
list._changes = [{operation: 'REMOVE_ALL'}];
|
||||
} else if (command[0] === 6) {
|
||||
list._changes = [{operation: 'REMOVE_ALL'}];
|
||||
_.each(command[2], function (resID) {
|
||||
linkRecord(list, resID);
|
||||
});
|
||||
}
|
||||
});
|
||||
defs.push(self._readUngroupedList(list));
|
||||
var def = self._readUngroupedList(list).then(function () {
|
||||
var x2ManysDef = self._fetchX2ManysBatched(list);
|
||||
var referencesDef = self._fetchReferencesBatched(list);
|
||||
return $.when(x2ManysDef, referencesDef);
|
||||
});
|
||||
defs.push(def);
|
||||
} else {
|
||||
var newValue = self._parseServerValue(field, val);
|
||||
if (newValue !== oldValue) {
|
||||
@@ -1455,6 +1477,25 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
|
||||
// inner function that adds a record (based on its res_id) to a list
|
||||
// dataPoint (used for onchanges that return commands 4 (LINK TO) or
|
||||
// commands 6 (REPLACE WITH))
|
||||
function linkRecord (list, resID) {
|
||||
rec = self.localData[list._cache[resID]];
|
||||
if (rec) {
|
||||
// modifications done on a record are discarded if the onchange
|
||||
// uses a LINK TO or a REPLACE WITH
|
||||
self.discardChanges(rec.id);
|
||||
}
|
||||
// the dataPoint id will be set when the record will be fetched (for
|
||||
// now, this dataPoint may not exist yet)
|
||||
list._changes.push({
|
||||
operation: 'ADD',
|
||||
id: rec ? rec.id : null,
|
||||
resID: resID,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* When an operation is applied to a x2many field, the field widgets
|
||||
@@ -1951,7 +1992,9 @@ var BasicModel = AbstractModel.extend({
|
||||
_.each(list.data, function (dataPoint) {
|
||||
var record = self.localData[dataPoint];
|
||||
var value = record.data[fieldName];
|
||||
if (value) {
|
||||
// if the reference field has already been fetched, the value is a
|
||||
// datapoint ID, and in this case there's nothing to do
|
||||
if (value && !self.localData[value]) {
|
||||
var model = value.split(',')[0];
|
||||
var resID = value.split(',')[1];
|
||||
if (!(model in toFetch)) {
|
||||
|
||||
@@ -73,7 +73,6 @@ var KanbanModel = BasicModel.extend({
|
||||
domain: parent.domain.concat([[groupBy,"=",result[0]]]),
|
||||
fields: parent.fields,
|
||||
fieldsInfo: parent.fieldsInfo,
|
||||
groupedBy: parent.groupedBy,
|
||||
isOpen: true,
|
||||
limit: parent.limit,
|
||||
parentID: parent.id,
|
||||
|
||||
@@ -54,8 +54,13 @@ ListRenderer.include({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
start: function () {
|
||||
if (this._isEditable()) {
|
||||
this.$el.css({height: '100%'});
|
||||
// deliberately use the 'editable' attribute instead of '_isEditable'
|
||||
// function, because the groupBy must not be taken into account to
|
||||
// enable the '_onWindowClicked' handler (otherwise, an editable grouped
|
||||
// list which is reloaded without groupBy wouldn't have this handler
|
||||
// bound, and edited rows couldn't be left by clicking outside the list)
|
||||
if (this.editable) {
|
||||
this.$el.css({height: '100%'}); // seems useless: to remove in master
|
||||
core.bus.on('click', this, this._onWindowClicked.bind(this));
|
||||
}
|
||||
return this._super();
|
||||
|
||||
@@ -1154,6 +1154,60 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: discard newly added element with empty required field', function (assert) {
|
||||
// This test simulates discarding a record that has been created with
|
||||
// one of its required field that is empty. When we discard the changes
|
||||
// on this empty field, it should not assume that this record should be
|
||||
// abandonned, since it has been added (even though it is a new record).
|
||||
assert.expect(6);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="trululu" required="1"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
return $.when({p: [[0, 0, {display_name: 'new record', trululu: false}]]});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual($('tr.o_data_row').length, 1,
|
||||
"should have created the new record in the o2m");
|
||||
|
||||
var requiredElement = $('td.o_data_cell.o_required_modifier');
|
||||
assert.strictEqual(requiredElement.length, 1,
|
||||
"should have a required field on this record");
|
||||
assert.strictEqual(requiredElement.text(), "",
|
||||
"should have empty string in the required field on this record");
|
||||
|
||||
requiredElement.click();
|
||||
// discard by clicking on body
|
||||
$('body').click();
|
||||
|
||||
assert.strictEqual($('tr.o_data_row').length, 1,
|
||||
"should still have the record in the o2m");
|
||||
|
||||
// update selector of required field element
|
||||
requiredElement = $('td.o_data_cell.o_required_modifier');
|
||||
assert.strictEqual(requiredElement.length, 1,
|
||||
"should still have the required field on this record");
|
||||
assert.strictEqual(requiredElement.text(), "",
|
||||
"should still have empty string in the required field on this record");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: default_get with x2many create', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
@@ -2852,6 +2906,163 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange returning a command 6 for an x2many', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
foo: function (obj) {
|
||||
obj.turtles = [[6, false, [1, 2, 3]]];
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"there should be one record in the relation");
|
||||
|
||||
// change the value of foo to trigger the onchange
|
||||
form.$('.o_field_widget[name=foo]').val('some value').trigger('input');
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 3,
|
||||
"there should be three records in the relation");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('x2many fields inside x2manys are fetched after an onchange', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.data.turtle.records[0].partner_ids = [1];
|
||||
this.data.partner.onchanges = {
|
||||
foo: function (obj) {
|
||||
obj.turtles = [[5], [4, 1], [4, 2], [4, 3]];
|
||||
},
|
||||
};
|
||||
|
||||
var checkRPC = false;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form>' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="partner_ids" widget="many2many_tags"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (checkRPC && args.method === 'read' && args.model === 'partner') {
|
||||
assert.deepEqual(args.args[1], ['display_name'],
|
||||
"should only read the display_name for the m2m tags");
|
||||
assert.deepEqual(args.args[0], [1],
|
||||
"should only read the display_name of the unknown record");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"there should be one record in the relation");
|
||||
assert.strictEqual(form.$('.o_data_row .o_field_widget[name=partner_ids]').text().replace(/\s/g, ''),
|
||||
'secondrecordaaa', "many2many_tags should be correctly displayed");
|
||||
|
||||
// change the value of foo to trigger the onchange
|
||||
checkRPC = true; // enable flag to check read RPC for the m2m field
|
||||
form.$('.o_field_widget[name=foo]').val('some value').trigger('input');
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 3,
|
||||
"there should be three records in the relation");
|
||||
assert.strictEqual(form.$('.o_data_row:first .o_field_widget[name=partner_ids]').text().trim(),
|
||||
'first record', "many2many_tags should be correctly displayed");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('reference fields inside x2manys are fetched after an onchange', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.turtle.records[1].turtle_ref = 'product,41';
|
||||
this.data.partner.onchanges = {
|
||||
foo: function (obj) {
|
||||
obj.turtles = [[5], [4, 1], [4, 2], [4, 3]];
|
||||
},
|
||||
};
|
||||
|
||||
var checkRPC = false;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form>' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="turtle_ref" class="ref_field"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (checkRPC && args.method === 'name_get') {
|
||||
assert.deepEqual(args.args[0], [37],
|
||||
"should only fetch the name_get of the unknown record");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"there should be one record in the relation");
|
||||
assert.strictEqual(form.$('.ref_field').text().trim(), 'xpad',
|
||||
"reference field should be correctly displayed");
|
||||
|
||||
// change the value of foo to trigger the onchange
|
||||
checkRPC = true; // enable flag to check read RPC for reference field
|
||||
form.$('.o_field_widget[name=foo]').val('some value').trigger('input');
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 3,
|
||||
"there should be three records in the relation");
|
||||
assert.strictEqual(form.$('.ref_field').text().trim(), 'xpadxphone',
|
||||
"reference fields should be correctly displayed");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(20);
|
||||
|
||||
@@ -1947,6 +1947,62 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('archive new kanban column', function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
mockRPC: function (route) {
|
||||
assert.step(route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
var rpcs = ['/web/dataset/call_kw/partner/read_group', '/web/dataset/search_read', '/web/dataset/search_read'];
|
||||
assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
|
||||
"there should be 2 columns");
|
||||
|
||||
// create a column
|
||||
kanban.$('.o_column_quick_create').click();
|
||||
kanban.$('.o_column_quick_create input').val('new colum');
|
||||
kanban.$('.o_column_quick_create button.o_kanban_add').click();
|
||||
rpcs.push('/web/dataset/call_kw/product/name_create');
|
||||
assert.verifySteps(rpcs);
|
||||
|
||||
// add a record inside
|
||||
kanban.$('.o_kanban_group:eq(2) .o_kanban_quick_add i').click();
|
||||
var $quickCreate = kanban.$('.o_kanban_group:eq(2) .o_kanban_quick_create');
|
||||
$quickCreate.find('input').val('new record');
|
||||
$quickCreate.find('button.o_kanban_add').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_group').length, 3,
|
||||
"there should be 3 columns");
|
||||
rpcs.push('/web/dataset/call_kw/partner/name_create');
|
||||
rpcs.push('/web/dataset/call_kw/partner/read');
|
||||
assert.verifySteps(rpcs);
|
||||
|
||||
var $newColumn = kanban.$('.o_kanban_group:eq(2)');
|
||||
assert.strictEqual($newColumn.find('.o_kanban_record').length, 1,
|
||||
"there should be 1 record in the new column");
|
||||
$newColumn.find('.o_column_archive').click();
|
||||
assert.strictEqual($newColumn.find('.o_kanban_record').length, 0,
|
||||
"there should not be partners anymore");
|
||||
rpcs.push('/web/dataset/call_kw/partner/write');
|
||||
rpcs.push('/web/dataset/search_read');
|
||||
assert.verifySteps(rpcs, "a search_read should be done after archiving the records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('load more records in column', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
|
||||
@@ -2751,10 +2751,13 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped list are not editable', function (assert) {
|
||||
QUnit.test('grouped list are not editable (ungrouped first)', function (assert) {
|
||||
// Editable grouped list views are not supported, so the purpose of this
|
||||
// test is to check that when a list view is grouped, its editable
|
||||
// attribute is ignored
|
||||
// In this test, the view isn't grouped at the beginning, so it is first
|
||||
// editable, and then it is reloaded with a groupBy and is no longer
|
||||
// editable
|
||||
assert.expect(5);
|
||||
|
||||
var list = createView({
|
||||
@@ -2790,6 +2793,59 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped list are not editable (grouped first)', function (assert) {
|
||||
// Editable grouped list views are not supported, so the purpose of this
|
||||
// test is to check that when a list view is grouped, its editable
|
||||
// attribute is ignored
|
||||
// In this test, the view is grouped at the beginning, so it isn't
|
||||
// editable, and then it is reloaded with no groupBy and becomes editable
|
||||
assert.expect(6);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
var resID = event.data.res_id || false;
|
||||
assert.step('switch view ' + event.data.view_type + ' ' + resID);
|
||||
},
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// the view being grouped, it is not editable, so clicking on a record
|
||||
// should open the form view
|
||||
list.$('.o_group_header:first').click(); // open first group
|
||||
list.$('.o_data_cell:first').click();
|
||||
|
||||
// for the same reason, clicking on 'Create' should open the form view
|
||||
list.$buttons.find('.o_list_button_add').click();
|
||||
|
||||
assert.verifySteps(['switch view form 1', 'switch view form false'],
|
||||
"two switch view to form should have been requested");
|
||||
|
||||
// reload without groupBy
|
||||
list.reload({groupBy: []});
|
||||
|
||||
// as the view is no longer grouped, it is editable, so clicking on a
|
||||
// row should switch it in edition
|
||||
list.$('.o_data_cell:first').click();
|
||||
|
||||
assert.verifySteps(['switch view form 1', 'switch view form false'],
|
||||
"no more switch view should have been requested");
|
||||
assert.strictEqual(list.$('.o_selected_row').length, 1,
|
||||
"a row should be in edition");
|
||||
|
||||
// clicking on the body should leave the edition
|
||||
$('body').click();
|
||||
assert.strictEqual(list.$('.o_selected_row').length, 0,
|
||||
"the row should no longer be in edition");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('field values are escaped', function (assert) {
|
||||
assert.expect(1);
|
||||
var value = '<script>throw Error();</script>';
|
||||
|
||||
@@ -58,6 +58,10 @@ function checkXML(xml) {
|
||||
* @returns {string} formatted xml
|
||||
*/
|
||||
function formatXML(xml) {
|
||||
// do nothing if an inline script is present to avoid breaking it
|
||||
if (/<script(?: [^>]*)?>[^<][\s\S]*<\/script>/i.test(xml)) {
|
||||
return xml;
|
||||
}
|
||||
return window.vkbeautify.xml(xml, 4);
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web_editor.widget', function (require) {
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var utils = require('web.utils');
|
||||
var weContext = require("web_editor.context");
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -1208,6 +1209,7 @@ var LinkDialog = Dialog.extend({
|
||||
if (!$e.length) {
|
||||
$e = this.$('input.url-source:first');
|
||||
}
|
||||
$e.closest('.form-group').removeClass('has-error');
|
||||
var val = $e.val();
|
||||
var label = this.$('#o_link_dialog_label_input').val() || val;
|
||||
|
||||
@@ -1228,8 +1230,7 @@ var LinkDialog = Dialog.extend({
|
||||
var size = this.$("select.link-style").val() || '';
|
||||
var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
|
||||
var isNewWindow = this.$('input.window-new').prop('checked');
|
||||
|
||||
if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
|
||||
if ($e.hasClass('email-address') && (_.str.startsWith(val, 'mailto:') || (val.indexOf("@") !== -1 && !_.str.startsWith(val, 'http') && !_.str.startsWith(val, 'www')))) {
|
||||
self.get_data_buy_mail(def, $e, isNewWindow, label, classes, test);
|
||||
} else {
|
||||
self.get_data_buy_url(def, $e, isNewWindow, label, classes, test);
|
||||
@@ -1238,7 +1239,13 @@ var LinkDialog = Dialog.extend({
|
||||
},
|
||||
get_data_buy_mail: function (def, $e, isNewWindow, label, classes, test) {
|
||||
var val = $e.val();
|
||||
def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
|
||||
if (utils.is_email(val, true)) {
|
||||
def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
|
||||
} else {
|
||||
$e.closest('.form-group').addClass('has-error');
|
||||
$e.focus();
|
||||
def.reject();
|
||||
}
|
||||
},
|
||||
get_data_buy_url: function (def, $e, isNewWindow, label, classes, test) {
|
||||
def.resolve($e.val(), isNewWindow, label, classes);
|
||||
@@ -1322,5 +1329,9 @@ return {
|
||||
getCssSelectors: getCssSelectors,
|
||||
fontIcons: fontIcons,
|
||||
computeFonts: computeFonts,
|
||||
click_event: click_event,
|
||||
fontIconsDialog: fontIconsDialog,
|
||||
ImageDialog: ImageDialog,
|
||||
VideoDialog: VideoDialog,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -520,7 +520,7 @@ class Website(models.Model):
|
||||
if not force:
|
||||
domain += [('website_indexed', '=', True)]
|
||||
#is_visible
|
||||
domain += [('website_published', '=', True), '|', ('date_publish', '!=', False), ('date_publish', '>', fields.Datetime.now())]
|
||||
domain += [('website_published', '=', True), '|', ('date_publish', '=', False), ('date_publish', '<=', fields.Datetime.now())]
|
||||
|
||||
if query_string:
|
||||
domain += [('url', 'like', query_string)]
|
||||
@@ -875,12 +875,12 @@ class Menu(models.Model):
|
||||
replace_id(mid, new_menu.id)
|
||||
for menu in data['data']:
|
||||
menu_id = self.browse(menu['id'])
|
||||
if menu_id.page_id:
|
||||
menu_id.page_id.write({'url': menu['url']})
|
||||
# if the url match a website.page, set the m2o relation
|
||||
page = self.env['website.page'].search([('url', '=', menu['url'])], limit=1)
|
||||
if page:
|
||||
menu['page_id'] = page.id
|
||||
elif menu_id.page_id:
|
||||
menu_id.page_id.write({'url': menu['url']})
|
||||
menu_id.write(menu)
|
||||
|
||||
return True
|
||||
|
||||
@@ -117,13 +117,16 @@
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-set='head_website']" position="replace">
|
||||
<t t-set="head_website">
|
||||
<meta name="generator" content="Odoo"/>
|
||||
|
||||
<meta t-if="main_object and 'website_indexed' in main_object
|
||||
and not main_object.website_indexed" name="robots" content="noindex"/>
|
||||
<meta name="description" t-att-content="main_object and 'website_meta_description' in main_object
|
||||
<t t-set="meta_description" t-value="main_object and 'website_meta_description' in main_object
|
||||
and main_object.website_meta_description or website_meta_description"/>
|
||||
<meta name="keywords" t-att-content="main_object and 'website_meta_keywords' in main_object
|
||||
<t t-set="meta_keywords" t-value="main_object and 'website_meta_keywords' in main_object
|
||||
and main_object.website_meta_keywords or website_meta_keywords"/>
|
||||
<meta name="generator" content="Odoo"/>
|
||||
<meta t-if="meta_description" name="description" t-att-content="meta_description"/>
|
||||
<meta t-if="meta_keywords" name="keywords" t-att-content="meta_keywords"/>
|
||||
|
||||
<!-- OpenGraph tags for Facebook sharing -->
|
||||
<meta property="og:title" t-att-content="additional_title" />
|
||||
|
||||
@@ -23,8 +23,8 @@ class Event(models.Model):
|
||||
website_menu = fields.Boolean(
|
||||
'Dedicated Menu', compute='_compute_website_menu', inverse='_set_website_menu',
|
||||
help="Creates menus Introduction, Location and Register on the page "
|
||||
" of the event on the website.", store=True, copy=False)
|
||||
menu_id = fields.Many2one('website.menu', 'Event Menu')
|
||||
" of the event on the website.", store=True)
|
||||
menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)
|
||||
|
||||
def _compute_is_participating(self):
|
||||
# we don't allow public user to see participating label
|
||||
|
||||
@@ -225,9 +225,9 @@ class WebsiteForum(http.Controller):
|
||||
@http.route('/forum/get_url_title', type='json', auth="user", methods=['POST'], website=True)
|
||||
def get_url_title(self, **kwargs):
|
||||
try:
|
||||
req = requests.get(kwargs.get('url'), stream=True)
|
||||
req = requests.get(kwargs.get('url'))
|
||||
req.raise_for_status()
|
||||
arch = lxml.html.parse(req.raw)
|
||||
arch = lxml.html.fromstring(req.content)
|
||||
return arch.find(".//title").text
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
@@ -70,9 +70,16 @@ class SaleOrder(models.Model):
|
||||
require_payment = fields.Selection([
|
||||
(0, 'Online Signature'),
|
||||
(1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode',
|
||||
compute='_compute_require_payment', store=True,
|
||||
help="Choose how you want to confirm an order to launch the delivery process. You can either "
|
||||
"request a digital signature or an upfront payment. With a digital signature, you can "
|
||||
"request the payment when issuing the invoice.")
|
||||
@api.one
|
||||
@api.depends('template_id')
|
||||
def _compute_require_payment(self):
|
||||
# Do not overwrite if the field is already set
|
||||
if self.template_id and (self.require_payment is None or self.require_payment is False):
|
||||
self.require_payment = self.template_id.require_payment
|
||||
|
||||
@api.multi
|
||||
def copy(self, default=None):
|
||||
|
||||
@@ -600,14 +600,11 @@
|
||||
<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 None" t-att-value="variant_id.id" t-att-data-lst_price="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 ''" 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]"/>
|
||||
<span class="badge" t-if="variant_id.price_extra">
|
||||
<t t-esc="variant_id.price_extra > 0 and '+' or ''"/><span t-field="variant_id.price_extra" style="white-space: nowrap;" t-options='{
|
||||
"widget": "monetary",
|
||||
"from_currency": product.currency_id,
|
||||
"display_currency": website.currency_id
|
||||
}'/>
|
||||
<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"}'/>
|
||||
</span>
|
||||
</label>
|
||||
</t>
|
||||
|
||||
@@ -35,6 +35,16 @@ def test_parser():
|
||||
types.Literal('null'),
|
||||
])])
|
||||
])
|
||||
assert types.parse('Function<Array<Object[]>>') == types.Alt([
|
||||
types.Type('Function', [types.Alt([
|
||||
types.Type('Array', [types.Alt([
|
||||
types.Type('Array', [
|
||||
types.Type('Object', [])
|
||||
])
|
||||
])])
|
||||
])])
|
||||
])
|
||||
|
||||
|
||||
def test_tokens():
|
||||
toks = list(types.tokenize('A'))
|
||||
@@ -64,6 +74,19 @@ def test_tokens():
|
||||
(types.OP, '>')
|
||||
]
|
||||
|
||||
toks = list(types.tokenize('Function<Array<Object[]>>'))
|
||||
assert toks == [
|
||||
(types.NAME, 'Function'),
|
||||
(types.OP, '<'),
|
||||
(types.NAME, 'Array'),
|
||||
(types.OP, '<'),
|
||||
(types.NAME, 'Object'),
|
||||
(types.OP, '['),
|
||||
(types.OP, ']'),
|
||||
(types.OP, '>'),
|
||||
(types.OP, '>')
|
||||
]
|
||||
|
||||
def test_peekable():
|
||||
p = types.Peekable(range(5))
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ def tokenize(typespec):
|
||||
elif toktype == token.OP:
|
||||
if string in '|<>[].,':
|
||||
yield (OP, string)
|
||||
elif string == '>>':
|
||||
yield (OP, '>')
|
||||
yield (OP, '>')
|
||||
elif string == '*': # maybe?
|
||||
yield (NAME, 'any')
|
||||
elif string in '()':
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
France, 2018-02-16
|
||||
|
||||
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,
|
||||
|
||||
Olivier Sarrat olivier.sarrat@laposte.net https://github.com/osarrat
|
||||
@@ -0,0 +1,11 @@
|
||||
Nepal, 2018-02-09
|
||||
|
||||
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,
|
||||
|
||||
Subodh Dahal contact.subodhdahal@gmail.com https://github.com/SubodhDahal
|
||||
@@ -0,0 +1,10 @@
|
||||
Norway, 2018-02-08
|
||||
|
||||
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,
|
||||
Marius Stedjan marius@stedjan.com https://github.com/willmakenoise
|
||||
@@ -54,7 +54,7 @@ Running the server
|
||||
Maximum allowed virtual memory per worker. If the limit is exceeded,
|
||||
the worker is killed and recycled at the end of the current request.
|
||||
|
||||
Defaults to 640MB.
|
||||
Defaults to 2048MB.
|
||||
|
||||
.. option:: --limit-memory-hard <limit>
|
||||
|
||||
@@ -62,7 +62,7 @@ Running the server
|
||||
immediately killed without waiting for the end of the current request
|
||||
processing.
|
||||
|
||||
Defaults to 768MB.
|
||||
Defaults to 2560MB.
|
||||
|
||||
.. option:: --limit-time-cpu <limit>
|
||||
|
||||
|
||||
+14
-5
@@ -214,6 +214,13 @@ Instead you must have a proxy redirecting requests whose URL starts with
|
||||
``/longpolling/`` to the longpolling port. Other request should be proxied to
|
||||
the :option:`normal HTTP port <odoo-bin --http-port>`
|
||||
|
||||
To achieve such a thing, you'll need to deploy a reverse proxy in front of Odoo,
|
||||
like nginx or apache. When doing so, you'll need to forward some more http Headers
|
||||
to Odoo, and activate the proxy_mode in Odoo configuration to have Odoo read those
|
||||
headers.
|
||||
|
||||
|
||||
|
||||
Configuration sample
|
||||
--------------------
|
||||
|
||||
@@ -316,16 +323,18 @@ in ``/etc/nginx/sites-enabled/odoo.conf`` set:
|
||||
# log
|
||||
access_log /var/log/nginx/odoo.access.log;
|
||||
error_log /var/log/nginx/odoo.error.log;
|
||||
|
||||
|
||||
# Redirect longpoll requests to odoo longpolling port
|
||||
location /longpolling {
|
||||
proxy_pass http://odoochat;
|
||||
}
|
||||
|
||||
# Redirect requests to odoo backend server
|
||||
location / {
|
||||
proxy_redirect off;
|
||||
proxy_pass http://odoo;
|
||||
}
|
||||
location /longpolling {
|
||||
proxy_pass http://odoochat;
|
||||
}
|
||||
|
||||
|
||||
# common gzip
|
||||
gzip_types text/css text/less text/plain text/xml application/xml application/json application/javascript;
|
||||
gzip on;
|
||||
|
||||
@@ -539,7 +539,7 @@ class Partner(models.Model):
|
||||
self.ensure_one()
|
||||
if self.company_name:
|
||||
# Create parent company
|
||||
values = dict(name=self.company_name, is_company=True)
|
||||
values = dict(name=self.company_name, is_company=True, vat=self.vat)
|
||||
values.update(self._update_fields_values(self._address_fields()))
|
||||
new_company = self.create(values)
|
||||
# Set new company as my parent
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
<group col="4">
|
||||
<field name="type"/>
|
||||
<field name="name"/>
|
||||
<field name="module"/>
|
||||
<field name="module" can_create="false"/>
|
||||
<field name="model"/>
|
||||
<newline/>
|
||||
<field name="date_update" />
|
||||
@@ -495,7 +495,7 @@
|
||||
<form string="ManyToMany Relations">
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="module"/>
|
||||
<field name="module" can_create="false"/>
|
||||
<field name="model"/>
|
||||
<newline/>
|
||||
<field name="date_update" />
|
||||
|
||||
@@ -601,7 +601,54 @@ class test_m2m(CreatorCase):
|
||||
['', u'export.many2many.other:13'],
|
||||
])
|
||||
|
||||
# essentially same as o2m, so boring
|
||||
def test_multiple_records_subfield(self):
|
||||
r = self.make(self.commands)
|
||||
xid = self.env['ir.model.data'].create({
|
||||
'name': 'whopwhopwhop',
|
||||
'module': '__t__',
|
||||
'model': r._name,
|
||||
'res_id': r.id,
|
||||
}).complete_name
|
||||
sids = [
|
||||
self.env['ir.model.data'].create({
|
||||
'name': sub.str,
|
||||
'module': '__t__',
|
||||
'model': sub._name,
|
||||
'res_id': sub.id,
|
||||
}).complete_name
|
||||
for sub in r.value
|
||||
]
|
||||
r.invalidate_cache()
|
||||
|
||||
self.assertEqual(
|
||||
r._export_rows([['value', 'id']]),
|
||||
[['__t__.record000,__t__.record001,__t__.record010,__t__.record011,__t__.record100']]
|
||||
)
|
||||
self.assertEqual(
|
||||
r.with_context(import_compat=True)._export_rows([['value', 'id']]),
|
||||
[['__t__.record000,__t__.record001,__t__.record010,__t__.record011,__t__.record100']]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
r.with_context(import_compat=False)._export_rows([['id'], ['value', 'id'], ['value', 'value']]),
|
||||
[
|
||||
[xid, u'__t__.record000', u'4'],
|
||||
[u'', u'__t__.record001', u'42'],
|
||||
[u'', u'__t__.record010', u'36'],
|
||||
[u'', u'__t__.record011', u'4'],
|
||||
[u'', u'__t__.record100', u'13']
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
r.with_context(import_compat=False)._export_rows([['id'], ['value', 'value'], ['value', 'id']]),
|
||||
[
|
||||
[xid, u'4', u'__t__.record000'],
|
||||
[u'', u'42', u'__t__.record001'],
|
||||
[u'', u'36', u'__t__.record010'],
|
||||
[u'', u'4', u'__t__.record011'],
|
||||
[u'', u'13', u'__t__.record100']
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class test_function(CreatorCase):
|
||||
|
||||
+53
-37
@@ -650,8 +650,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
:param fields: list of lists of fields to traverse
|
||||
:return: list of lists of corresponding values
|
||||
"""
|
||||
import_compatible = self.env.context.get('import_compat', True)
|
||||
lines = []
|
||||
for record in self:
|
||||
|
||||
def splittor(rs):
|
||||
""" Splits the self recordset in batches of 1000 (to avoid
|
||||
entire-recordset-prefetch-effects) & removes the previous batch
|
||||
from the cache after it's been iterated in full
|
||||
"""
|
||||
for idx in range(0, len(rs), 1000):
|
||||
sub = rs[idx: idx+1000]
|
||||
for rec in sub:
|
||||
yield rec
|
||||
rs.invalidate_cache(ids=sub.ids)
|
||||
|
||||
# memory stable but ends up prefetching 275 fields (???)
|
||||
for idx, record in enumerate(splittor(self)):
|
||||
# main line of record, initially empty
|
||||
current = [''] * len(fields)
|
||||
lines.append(current)
|
||||
@@ -683,8 +697,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
else:
|
||||
primary_done.append(name)
|
||||
|
||||
# This is a special case, its strange behavior is intended!
|
||||
if field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
|
||||
# in import_compat mode, m2m should always be exported as
|
||||
# a comma-separated list of xids in a single cell
|
||||
if import_compatible and field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
|
||||
xml_ids = [r.__export_xml_id() for r in value]
|
||||
current[i] = ','.join(xml_ids) or False
|
||||
continue
|
||||
@@ -2840,47 +2855,48 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
raise UserError(_('Unable to delete this document because it is used as a default property'))
|
||||
|
||||
# Delete the records' properties.
|
||||
self.env['ir.property'].search([('res_id', 'in', refs)]).unlink()
|
||||
with self.env.norecompute():
|
||||
self.env['ir.property'].search([('res_id', 'in', refs)]).unlink()
|
||||
|
||||
self.check_access_rule('unlink')
|
||||
self.check_access_rule('unlink')
|
||||
|
||||
cr = self._cr
|
||||
Data = self.env['ir.model.data'].sudo().with_context({})
|
||||
Defaults = self.env['ir.default'].sudo()
|
||||
Attachment = self.env['ir.attachment']
|
||||
cr = self._cr
|
||||
Data = self.env['ir.model.data'].sudo().with_context({})
|
||||
Defaults = self.env['ir.default'].sudo()
|
||||
Attachment = self.env['ir.attachment']
|
||||
|
||||
for sub_ids in cr.split_for_in_conditions(self.ids):
|
||||
query = "DELETE FROM %s WHERE id IN %%s" % self._table
|
||||
cr.execute(query, (sub_ids,))
|
||||
for sub_ids in cr.split_for_in_conditions(self.ids):
|
||||
query = "DELETE FROM %s WHERE id IN %%s" % self._table
|
||||
cr.execute(query, (sub_ids,))
|
||||
|
||||
# Removing the ir_model_data reference if the record being deleted
|
||||
# is a record created by xml/csv file, as these are not connected
|
||||
# with real database foreign keys, and would be dangling references.
|
||||
#
|
||||
# Note: the following steps are performed as superuser to avoid
|
||||
# access rights restrictions, and with no context to avoid possible
|
||||
# side-effects during admin calls.
|
||||
data = Data.search([('model', '=', self._name), ('res_id', 'in', sub_ids)])
|
||||
if data:
|
||||
data.unlink()
|
||||
# Removing the ir_model_data reference if the record being deleted
|
||||
# is a record created by xml/csv file, as these are not connected
|
||||
# with real database foreign keys, and would be dangling references.
|
||||
#
|
||||
# Note: the following steps are performed as superuser to avoid
|
||||
# access rights restrictions, and with no context to avoid possible
|
||||
# side-effects during admin calls.
|
||||
data = Data.search([('model', '=', self._name), ('res_id', 'in', sub_ids)])
|
||||
if data:
|
||||
data.unlink()
|
||||
|
||||
# For the same reason, remove the defaults having some of the
|
||||
# records as value
|
||||
Defaults.discard_records(self.browse(sub_ids))
|
||||
# For the same reason, remove the defaults having some of the
|
||||
# records as value
|
||||
Defaults.discard_records(self.browse(sub_ids))
|
||||
|
||||
# For the same reason, remove the relevant records in ir_attachment
|
||||
# (the search is performed with sql as the search method of
|
||||
# ir_attachment is overridden to hide attachments of deleted
|
||||
# records)
|
||||
query = 'SELECT id FROM ir_attachment WHERE res_model=%s AND res_id IN %s'
|
||||
cr.execute(query, (self._name, sub_ids))
|
||||
attachments = Attachment.browse([row[0] for row in cr.fetchall()])
|
||||
if attachments:
|
||||
attachments.unlink()
|
||||
# For the same reason, remove the relevant records in ir_attachment
|
||||
# (the search is performed with sql as the search method of
|
||||
# ir_attachment is overridden to hide attachments of deleted
|
||||
# records)
|
||||
query = 'SELECT id FROM ir_attachment WHERE res_model=%s AND res_id IN %s'
|
||||
cr.execute(query, (self._name, sub_ids))
|
||||
attachments = Attachment.browse([row[0] for row in cr.fetchall()])
|
||||
if attachments:
|
||||
attachments.unlink()
|
||||
|
||||
# invalidate the *whole* cache, since the orm does not handle all
|
||||
# changes made in the database, like cascading delete!
|
||||
self.invalidate_cache()
|
||||
# invalidate the *whole* cache, since the orm does not handle all
|
||||
# changes made in the database, like cascading delete!
|
||||
self.invalidate_cache()
|
||||
|
||||
# recompute new-style fields
|
||||
if self.env.recompute and self._context.get('recompute', True):
|
||||
|
||||
@@ -41,7 +41,7 @@ RPC_FAULT_CODE_ACCESS_ERROR = 4
|
||||
|
||||
def xmlrpc_handle_exception_int(e):
|
||||
if isinstance(e, odoo.exceptions.UserError):
|
||||
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, odoo.tools.ustr(e.value))
|
||||
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, odoo.tools.ustr(e.name))
|
||||
elif isinstance(e, odoo.exceptions.RedirectWarning):
|
||||
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
|
||||
elif isinstance(e, odoo.exceptions.MissingError):
|
||||
|
||||
Reference in New Issue
Block a user