[MERGE] forward port branch saas-17 up to b8dd34fcbb
This commit is contained in:
@@ -266,7 +266,7 @@ class AccountAccount(models.Model):
|
||||
elif self.internal_type == 'receivable':
|
||||
action_context = {'show_mode_selector': False, 'mode': 'customers'}
|
||||
else:
|
||||
action_context = {'show_mode_selector': False, 'account_ids': [self.id,]}
|
||||
action_context = {'show_mode_selector': False, 'mode': 'accounts', 'account_ids': [self.id,]}
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'manual_reconciliation_view',
|
||||
|
||||
@@ -1073,7 +1073,7 @@ var ManualModel = StatementModel.extend({
|
||||
return self._rpc({
|
||||
model: 'account.move.line',
|
||||
method: 'get_data_for_manual_reconciliation',
|
||||
args: ['account', self.account_ids],
|
||||
args: ['account', model.account_ids || self.account_ids],
|
||||
context: context,
|
||||
})
|
||||
.then(function (result) {
|
||||
|
||||
@@ -445,7 +445,7 @@ class Import(models.TransientModel):
|
||||
:rtype: (None, None) | (list(str), dict(int: list(str)))
|
||||
"""
|
||||
if not options.get('headers'):
|
||||
return None, None
|
||||
return [], {}
|
||||
|
||||
headers = next(rows)
|
||||
return headers, {
|
||||
|
||||
@@ -150,7 +150,7 @@ class TestMatchHeadersMultiple(TransactionCase):
|
||||
|
||||
def test_noheaders(self):
|
||||
self.assertEqual(
|
||||
self.env['base_import.import']._match_headers([], [], {}), (None, None)
|
||||
self.env['base_import.import']._match_headers([], [], {}), ([], {})
|
||||
)
|
||||
|
||||
def test_nomatch(self):
|
||||
|
||||
@@ -978,7 +978,7 @@ class Lead(models.Model):
|
||||
result['activity']['today'] += 1
|
||||
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
|
||||
result['activity']['next_7_days'] += 1
|
||||
if date_deadline < date.today():
|
||||
if date_deadline < date.today() and not opp.date_closed:
|
||||
result['activity']['overdue'] += 1
|
||||
# Won in Opportunities
|
||||
if opp.date_closed and opp.stage_id.probability == 100:
|
||||
|
||||
@@ -68,7 +68,7 @@ class HrAttendance(models.Model):
|
||||
('check_in', '<=', attendance.check_in),
|
||||
('id', '!=', attendance.id),
|
||||
], order='check_in desc', limit=1)
|
||||
if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out >= attendance.check_in:
|
||||
if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out > attendance.check_in:
|
||||
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % {
|
||||
'empl_name': attendance.employee_id.name,
|
||||
'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(attendance.check_in))),
|
||||
|
||||
@@ -300,6 +300,13 @@ class HrExpense(models.Model):
|
||||
})
|
||||
return account_move
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for expense in self:
|
||||
if expense.state in ['done']:
|
||||
raise UserError(_('You cannot delete a posted expense.'))
|
||||
super(HrExpense, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def action_get_attachment_view(self):
|
||||
self.ensure_one()
|
||||
@@ -450,8 +457,8 @@ class HrExpenseSheet(models.Model):
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for expense in self:
|
||||
if expense.state == "post":
|
||||
raise UserError(_("You cannot delete a posted expense."))
|
||||
if expense.state in ['post', 'done']:
|
||||
raise UserError(_('You cannot delete a posted or paid expense.'))
|
||||
super(HrExpenseSheet, self).unlink()
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -29,26 +29,6 @@
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">['|',('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
<record id="hr_expense_report_prevent_unlink" model="ir.rule">
|
||||
<field name="name">Expense Report Prevent Delete Employee</field>
|
||||
<field name="model_id" ref="model_hr_expense_sheet"/>
|
||||
<field name="domain_force">[('state', '=', 'submit')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
<record id="hr_expense_report_auth_unlink" model="ir.rule">
|
||||
<field name="name">Expense Report Authorize Delete Officer</field>
|
||||
<field name="model_id" ref="model_hr_expense_sheet"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('hr_expense.group_hr_expense_user'))]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<record id="group_hr_timesheet_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="category_id" ref="base.module_category_hr_timesheet"/>
|
||||
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_timesheet_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="base.module_category_hr_timesheet"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_hr_timesheet_user'))]"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_hr_timesheet_user')), (4, ref('hr.group_hr_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -37,10 +37,9 @@
|
||||
<field name="name">hr.department.kanban.inherit</field>
|
||||
<field name="model">hr.department</field>
|
||||
<field name="inherit_id" ref="hr.hr_department_view_kanban"/>
|
||||
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_hr_timesheet_user'))]"/>
|
||||
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_timesheet_manager'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-left">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from openerp.modules.registry import RegistryManager
|
||||
|
||||
def migrate(cr, version):
|
||||
registry = RegistryManager.get(cr.dbname)
|
||||
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
|
||||
migrate_tags_on_taxes(cr, registry)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from openerp.modules.registry import RegistryManager
|
||||
|
||||
def migrate(cr, version):
|
||||
registry = RegistryManager.get(cr.dbname)
|
||||
from openerp.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
|
||||
migrate_set_tags_and_taxes_updatable(cr, registry, 'l10n_fr')
|
||||
|
||||
@@ -76,7 +76,10 @@ class MailController(http.Controller):
|
||||
record_action = record_sudo.get_access_action(access_uid=uid)
|
||||
else:
|
||||
record_action = record_sudo.get_access_action()
|
||||
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
record_action.pop('target_type', None)
|
||||
# the record has an URL redirection: use it directly
|
||||
if record_action['type'] == 'ir.actions.act_url':
|
||||
return werkzeug.utils.redirect(record_action['url'])
|
||||
|
||||
@@ -28,7 +28,7 @@ BasicModel.include({
|
||||
var localID = (record._changes && fieldName in record._changes) ?
|
||||
record._changes[fieldName] :
|
||||
record.data[fieldName];
|
||||
var list = this.localData[localID];
|
||||
var list = this._applyX2ManyOperations(this.localData[localID]);
|
||||
var invalidPartnerIds = [];
|
||||
_.each(list.data, function (id) {
|
||||
var record = self.localData[id];
|
||||
|
||||
@@ -1150,5 +1150,65 @@ QUnit.test('fieldmany2many tags email', function (assert) {
|
||||
$('.modal-footer .btn-primary').click();
|
||||
});
|
||||
|
||||
QUnit.test('fieldmany2many tags email (edition)', function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
this.data.partner.records[0].timmy = [12];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
res_id: 1,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="timmy" widget="many2many_tags_email"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method ==='read' && args.model === 'partner_type') {
|
||||
assert.step(args.args[0]);
|
||||
assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
archs: {
|
||||
'partner_type,false,form': '<form string="Types"><field name="display_name"/><field name="email"/></form>',
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps([[12]]);
|
||||
assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 1,
|
||||
"should contain one tag");
|
||||
|
||||
// add an other existing tag
|
||||
var $input = form.$('.o_field_many2manytags input');
|
||||
$input.click(); // opens the dropdown
|
||||
$input.autocomplete('widget').find('li:first').click(); // add 'silver'
|
||||
|
||||
assert.strictEqual($('.modal-body.o_act_window').length, 1,
|
||||
"there should be one modal opened to edit the empty email");
|
||||
assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver",
|
||||
"the opened modal should be a form view dialog with the partner_type 14");
|
||||
assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
|
||||
"there should be an email field in the modal");
|
||||
|
||||
// set the email and save the modal (will rerender the form view)
|
||||
$('.modal-body.o_act_window input[name="email"]').val('coucou@petite.perruche').trigger('input');
|
||||
$('.modal-footer .btn-primary').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 2,
|
||||
"should contain the second tag");
|
||||
// should have read [14] three times: when opening the dropdown, when opening the modal, and
|
||||
// after the save
|
||||
assert.verifySteps([[12], [14], [14], [14]]);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -338,6 +338,7 @@ class MrpProduction(models.Model):
|
||||
source_location = self.location_src_id
|
||||
original_quantity = self.product_qty - self.qty_produced
|
||||
data = {
|
||||
'sequence': bom_line.sequence,
|
||||
'name': self.name,
|
||||
'date': self.date_planned_start,
|
||||
'date_expected': self.date_planned_start,
|
||||
|
||||
@@ -7,14 +7,9 @@
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<div class="col-xs-12">
|
||||
<h2>Production Order # : <span t-field="o.name"/></h2>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<span class="text-right">
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', o.name, 600, 100)" style="width:350px;height:60px"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-xs-3">
|
||||
@@ -76,15 +71,6 @@
|
||||
<tr>
|
||||
<th><strong>Product</strong></th>
|
||||
<th class="text-right"><strong>Quantity</strong></th>
|
||||
<th class="text-center" groups="stock.group_stock_multi_locations">
|
||||
<strong>Source Location</strong>
|
||||
</th>
|
||||
<th class="text-center" groups="stock.group_stock_multi_locations">
|
||||
<strong>Destination Location</strong>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<strong>Barcode</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -101,17 +87,6 @@
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
<span t-field="line.product_uom.name" groups="product.group_uom"/>
|
||||
</td>
|
||||
<td class="text-center" groups="stock.group_stock_multi_locations">
|
||||
<span t-field="line.location_id.name"/>
|
||||
</td>
|
||||
<td class="text-center" groups="stock.group_stock_multi_locations">
|
||||
<span t-field="line.location_dest_id.name"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="line.product_id.barcode">
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', line.product_id.barcode, 600, 100)" style="width:300px;height:50px"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
|
||||
@@ -114,8 +114,8 @@
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}"/>
|
||||
<field name="product_uom_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}" string="To Consume"/>
|
||||
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}" string="Reserved"/>
|
||||
<field name="quantity_done" attrs="{'readonly': ['|', ('is_done', '=', True), ('has_tracking', 'in', ['lot','serial'])]}" string="Consumed"/>
|
||||
<button name="split_move_lot" string="Register lots" type="object" icon="fa-list"
|
||||
attrs="{'invisible': [('has_tracking', 'not in', ['lot','serial'])]}"/>
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<field name="active_move_line_ids" attrs="{'invisible': [('active_move_line_ids', '=', [])]}">
|
||||
<tree editable="bottom" create="0" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="product_qty" readonly="1"/>
|
||||
<field name="product_uom_qty" readonly="1" string="To Consume"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
@@ -165,9 +165,10 @@
|
||||
<group>
|
||||
<label for="date_planned_start" string="Planned Date"/>
|
||||
<div class="o_row">
|
||||
from <field name="date_planned_start"/>
|
||||
<div attrs="{'invisible': [('date_planned_finished', '=', False)]}">
|
||||
to
|
||||
<field name="date_planned_start" class="mr8"/>
|
||||
<div attrs="{'invisible': [('date_planned_start', '=', False)]}" class="o_row">
|
||||
<strong attrs="{'invisible': [('date_planned_finished', '=', False)]}" class="mr8">to</strong>
|
||||
<strong class="oe_edit_only mr8" attrs="{'invisible': [('date_planned_finished', '!=', False)]}">to</strong>
|
||||
<field name="date_planned_finished"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,7 +176,7 @@
|
||||
<div class="o_row">
|
||||
<field name="date_start" readonly="1"/>
|
||||
<div attrs="{'invisible': [('date_finished', '=', False)]}">
|
||||
to
|
||||
<strong class="mr8">to</strong>
|
||||
<field name="date_finished" readonly="1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
<field name="consume_line_ids" attrs="{'invisible': [('consume_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_lot_id': lot_id}">
|
||||
<tree editable="top" delete="0" create="0">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="product_qty" readonly="1"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="lot_id" context="{'default_product_id': product_id}" domain="[('product_id', '=', product_id)]"/>
|
||||
<field name="product_uom_qty" string="To Consume"/>
|
||||
<field name="product_qty" readonly="1" string="Reserved"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<record id="seq_mrp_repair" model="ir.sequence">
|
||||
<field name="name">Repair Order</field>
|
||||
<field name="code">mrp.repair</field>
|
||||
<field name="prefix">RMA</field>
|
||||
<field name="prefix">RMA/</field>
|
||||
<field name="padding">5</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
@@ -419,7 +419,7 @@ class Repair(models.Model):
|
||||
moves = self.env['stock.move']
|
||||
for operation in repair.operations:
|
||||
move = Move.create({
|
||||
'name': operation.name,
|
||||
'name': repair.name,
|
||||
'product_id': operation.product_id.id,
|
||||
'product_uom_qty': operation.product_uom_qty,
|
||||
'product_uom': operation.product_uom.id,
|
||||
@@ -436,6 +436,7 @@ class Repair(models.Model):
|
||||
'location_id': operation.location_id.id, #TODO: owner stuff
|
||||
'location_dest_id': operation.location_dest_id.id,})],
|
||||
'repair_id': repair.id,
|
||||
'origin': repair.name,
|
||||
})
|
||||
moves |= move
|
||||
operation.write({'move_id': move.id, 'state': 'done'})
|
||||
@@ -457,6 +458,7 @@ class Repair(models.Model):
|
||||
'location_id': repair.location_id.id, #TODO: owner stuff
|
||||
'location_dest_id': repair.location_dest_id.id,})],
|
||||
'repair_id': repair.id,
|
||||
'origin': repair.name,
|
||||
})
|
||||
consumed_lines = moves.mapped('move_line_ids')
|
||||
produced_lines = move.move_line_ids
|
||||
|
||||
@@ -477,6 +477,13 @@ var PosDB = core.Class.extend({
|
||||
}
|
||||
return orders;
|
||||
},
|
||||
set_cashier: function(cashier) {
|
||||
// Always update if the user is the same as before
|
||||
this.save('cashier', cashier);
|
||||
},
|
||||
get_cashier: function() {
|
||||
return this.load('cashier');
|
||||
}
|
||||
});
|
||||
|
||||
return PosDB;
|
||||
|
||||
@@ -245,6 +245,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
}
|
||||
|
||||
self.db.set_uuid(self.config.uuid);
|
||||
self.set_cachier(self.get_cashier());
|
||||
|
||||
var orders = self.db.get_orders();
|
||||
for (var i = 0; i < orders.length; i++) {
|
||||
@@ -622,11 +623,12 @@ exports.PosModel = Backbone.Model.extend({
|
||||
|
||||
// returns the user who is currently the cashier for this point of sale
|
||||
get_cashier: function(){
|
||||
return this.get('cashier') || this.user;
|
||||
return this.db.get_cashier() || this.get('cashier') || this.user;
|
||||
},
|
||||
// changes the current cashier
|
||||
set_cashier: function(user){
|
||||
this.set('cashier', user);
|
||||
this.db.set_cashier(this.cashier);
|
||||
},
|
||||
//creates a new empty order and sets it as the current order
|
||||
add_new_order: function(){
|
||||
|
||||
@@ -26,8 +26,8 @@ class AccountAnalyticLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
# prevent to update invoiced timesheets if product_id is not of type order
|
||||
if self.so_line.product_id.invoice_policy != "order" and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
|
||||
# prevent to update invoiced timesheets if one line is of type delivery
|
||||
if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
|
||||
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
|
||||
raise UserError(_('You can not modify already invoiced timesheets.'))
|
||||
|
||||
@@ -123,7 +123,9 @@ class AccountAnalyticLine(models.Model):
|
||||
revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
|
||||
billable_type = 'billable_time'
|
||||
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task':
|
||||
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
|
||||
quantity_hour = unit_amount
|
||||
if so_line.product_uom.category_id == timesheet_uom.category_id:
|
||||
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
|
||||
# compute the total revenue the SO since we are in fixed price
|
||||
total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
|
||||
# compute the total revenue already existing (without the current timesheet line)
|
||||
|
||||
@@ -187,15 +187,20 @@ class TestSaleTimesheet(TestSale):
|
||||
self.assertEquals(timesheet2.timesheet_invoice_id, invoice)
|
||||
self.assertEquals(timesheet3.timesheet_invoice_id, invoice)
|
||||
|
||||
# check that analytic line for product 'ordered' can be altered
|
||||
# check that analytic line for product 'delivery' cannot be altered
|
||||
with self.assertRaises(UserError):
|
||||
timesheet1.write(dict(unit_amount=10))
|
||||
self.assertNotEquals(timesheet1.unit_amount, 10)
|
||||
|
||||
# check that analytic line for product 'delivered' cannot be altered
|
||||
# check that analytic line for product 'ordered' can be altered
|
||||
timesheet3.write(dict(unit_amount=10))
|
||||
self.assertEquals(timesheet3.unit_amount, 10)
|
||||
|
||||
# check that if at least 1 analytic line is for product 'delivery', it cannot be altered
|
||||
with self.assertRaises(UserError):
|
||||
(timesheet1 + timesheet3).write(dict(unit_amount=15))
|
||||
self.assertNotEquals(timesheet1.unit_amount, 15)
|
||||
self.assertNotEquals(timesheet3.unit_amount, 15)
|
||||
|
||||
def test_revenue_multi_currency(self):
|
||||
""" Create a SO with 2 lines : one for a delivered service, one for a ordered service. Confirm
|
||||
|
||||
@@ -353,6 +353,13 @@ class Product(models.Model):
|
||||
def action_view_routes(self):
|
||||
return self.mapped('product_tmpl_id').action_view_routes()
|
||||
|
||||
@api.multi
|
||||
def action_view_stock_move_lines(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock.stock_move_line_action').read()[0]
|
||||
action['domain'] = [('product_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
res = super(Product, self).write(values)
|
||||
@@ -524,11 +531,9 @@ class ProductTemplate(models.Model):
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def action_view_stock_moves(self):
|
||||
products = self.mapped('product_variant_ids')
|
||||
action = self.env.ref('stock.act_product_stock_move_open').read()[0]
|
||||
if products:
|
||||
action['context'] = {'default_product_id': products.ids[0]}
|
||||
def action_view_stock_move_lines(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock.stock_move_line_action').read()[0]
|
||||
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
|
||||
return action
|
||||
|
||||
|
||||
@@ -211,9 +211,9 @@
|
||||
<span class="o_stat_text">Forecasted</span>
|
||||
</div>
|
||||
</button>
|
||||
<button string="Traceability"
|
||||
type="action"
|
||||
name= "%(act_product_stock_move_open)d"
|
||||
<button string="Inventory Moves"
|
||||
type="object"
|
||||
name= "action_view_stock_move_lines"
|
||||
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
||||
class="oe_stat_button" icon="fa-arrows-v"
|
||||
groups="stock.group_stock_user"/>
|
||||
@@ -275,8 +275,8 @@
|
||||
<span class="o_stat_text">Forecasted</span>
|
||||
</div>
|
||||
</button>
|
||||
<button string="Traceability" type="object"
|
||||
name= "action_view_stock_moves"
|
||||
<button string="Inventory Moves" type="object"
|
||||
name= "action_view_stock_move_lines"
|
||||
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
||||
class="oe_stat_button" icon="fa-arrows-v"
|
||||
groups="stock.group_stock_user"/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<field name="name">stock.move.line.tree</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-muted="state == 'cancel'" decoration-danger="(state not in ('cancel','done'))" string="Move Lines" create="0">
|
||||
<tree decoration-muted="state == 'cancel'" decoration-danger="(state not in ('cancel','done'))" string="Move Lines" create="0" default_order="date">
|
||||
<field name="date"/>
|
||||
<field name="picking_id" string="Reference"/>
|
||||
<field name="create_date" invisible="1" groups="base.group_no_one"/>
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="move_lines" attrs="{'readonly': [('state', 'in', ('cancel', 'done'))]}" context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability" decoration-muted="scrapped == True or state == 'cancel'" string="Stock Moves" editable="bottom">
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="date_expected" invisible="1"/>
|
||||
<field name="state" invisible="1" readonly="0"/>
|
||||
<field name="picking_type_id" invisible="1"/>
|
||||
|
||||
@@ -72,7 +72,10 @@ class StockMoveLine(models.Model):
|
||||
}
|
||||
move_id.write(move_vals)
|
||||
if move_id.product_id.valuation == 'real_time':
|
||||
move_id.with_context(force_valuation_amount=qty_difference*move_id.price_unit)._account_entry_move()
|
||||
if move_id.product_id.cost_method == 'standard':
|
||||
move_id.with_context(force_valuation_amount=qty_difference * move_id.product_id.standard_price)._account_entry_move()
|
||||
else:
|
||||
move_id.with_context(force_valuation_amount=qty_difference*move_id.price_unit)._account_entry_move()
|
||||
return super(StockMoveLine, self).write(vals)
|
||||
|
||||
|
||||
|
||||
@@ -356,8 +356,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
|
||||
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
|
||||
if (!m.isValid()) { return $.when(null); }
|
||||
var d = m.toDate();
|
||||
var date_string = field_utils.format[this.attrs.type](d, {type: t.attrs.type});
|
||||
var date_string = field_utils.format[t](m, {type: t});
|
||||
var label = _.str.sprintf(_.str.escapeHTML(
|
||||
_t("Search %(field)s at: %(value)s")), {
|
||||
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
||||
@@ -367,7 +366,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
|
||||
facet: {
|
||||
category: this.attrs.string,
|
||||
field: this,
|
||||
values: [{label: date_string, value: d}]
|
||||
values: [{label: date_string, value: m.toDate()}]
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
@@ -184,13 +184,12 @@ var Dialog = Widget.extend({
|
||||
this.$modal.remove();
|
||||
}
|
||||
|
||||
setTimeout(function () { // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal
|
||||
var modals = $('body > .modal').filter(':visible');
|
||||
if (modals.length) {
|
||||
modals.last().focus();
|
||||
$('body').addClass('modal-open');
|
||||
}
|
||||
}, 0);
|
||||
var modals = $('body > .modal').filter(':visible');
|
||||
if (modals.length) {
|
||||
modals.last().focus();
|
||||
// Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal
|
||||
$('body').addClass('modal-open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -308,11 +308,8 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
}).always(function () {
|
||||
self.avoid_recursion = false;
|
||||
});
|
||||
} else {
|
||||
// normal use case, just use the cookie
|
||||
self.session_id = utils.get_cookie("session_id");
|
||||
return $.when();
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Executes an RPC call, registering the provided callbacks.
|
||||
|
||||
@@ -305,18 +305,6 @@ return core.Class.extend({
|
||||
viewType = viewType === 'tree' ? 'list' : viewType;
|
||||
innerFieldsView.type = viewType;
|
||||
attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView));
|
||||
|
||||
// default_order is like:
|
||||
// 'name,id desc'
|
||||
// but we need it like:
|
||||
// [{name: 'id', asc: false}, {name: 'name', asc: true}]
|
||||
var defaultOrder = innerFieldsView.arch.attrs.default_order;
|
||||
if (defaultOrder) {
|
||||
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
|
||||
order = order.trim().split(' ');
|
||||
return {name: order[0], asc: order[1] !== 'desc'};
|
||||
});
|
||||
}
|
||||
});
|
||||
delete field.views;
|
||||
}
|
||||
@@ -346,6 +334,23 @@ return core.Class.extend({
|
||||
}
|
||||
}
|
||||
attrs.mode = mode;
|
||||
if (mode in attrs.views) {
|
||||
var view = attrs.views[mode];
|
||||
// process the default_order, which is like 'name,id desc'
|
||||
// but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
|
||||
var defaultOrder = view.arch.attrs.default_order;
|
||||
if (defaultOrder) {
|
||||
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
|
||||
order = order.trim().split(' ');
|
||||
return {name: order[0], asc: order[1] !== 'desc'};
|
||||
});
|
||||
}
|
||||
// detect editables list has they behave differently with respect
|
||||
// to the sorting (changes are not sorted directly)
|
||||
if (mode === 'list' && view.arch.attrs.editable) {
|
||||
attrs.keepChangesUnsorted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attrs.Widget.prototype.fieldsToFetch) {
|
||||
attrs.viewType = 'default';
|
||||
|
||||
@@ -337,7 +337,6 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
|
||||
if (element.type === 'record') {
|
||||
|
||||
var data = _.extend({}, element.data, element._changes);
|
||||
var relDataPoint;
|
||||
for (var fieldName in data) {
|
||||
@@ -412,16 +411,46 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
|
||||
// here, type === 'list'
|
||||
// apply potential changes (only for x2many lists)
|
||||
element = this._applyX2ManyOperations(element);
|
||||
var listData = _.map(element.data, function (elemID) {
|
||||
return self.get(elemID, options);
|
||||
});
|
||||
// apply potential changes (only for x2many lists):
|
||||
// for list having the flag _keepChangesUnsorted set to true (typically,
|
||||
// editable lists), we sort before apply changes by default (to keep the
|
||||
// changes unsorted, i.e. the edited line at the same place). However, if
|
||||
// the user forces a sort, all changes done so far are sorted (this is the
|
||||
// purpose of the _keepChangesUnsortedCount key), and changes done later
|
||||
// on won't be sorted.
|
||||
var count;
|
||||
if (element._keepChangesUnsorted) {
|
||||
// only apply a subset (by default 0) of changes before sorting
|
||||
count = element._keepChangesUnsortedCount || 0;
|
||||
element = this._applyX2ManyOperations(element, {to: count, position: 'bottom'});
|
||||
} else {
|
||||
// apply all changes before sorting
|
||||
element = this._applyX2ManyOperations(element, {position: 'bottom'});
|
||||
}
|
||||
this._sortList(element);
|
||||
if (element._keepChangesUnsorted) {
|
||||
// apply the remaining changes after the sort
|
||||
element = this._applyX2ManyOperations(element, {from: count, position: 'bottom'});
|
||||
}
|
||||
if (element._changes) {
|
||||
_.each(element._changes, function (change) {
|
||||
if (change.operation === 'ADD' && change.isNew) {
|
||||
element.data = _.without(element.data, change.id);
|
||||
if (change.position === 'top') {
|
||||
element.data.unshift(change.id);
|
||||
} else {
|
||||
element.data.push(change.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var list = {
|
||||
aggregateValues: _.extend({}, element.aggregateValues),
|
||||
context: _.extend({}, element.context),
|
||||
count: element.count,
|
||||
data: listData,
|
||||
data: _.map(element.data, function (elemID) {
|
||||
return self.get(elemID, options);
|
||||
}),
|
||||
domain: element.domain.slice(0),
|
||||
fields: element.fields,
|
||||
getContext: element.getContext,
|
||||
@@ -435,7 +464,7 @@ var BasicModel = AbstractModel.extend({
|
||||
offset: element.offset,
|
||||
orderedBy: element.orderedBy,
|
||||
res_id: element.res_id,
|
||||
res_ids: element.res_ids,
|
||||
res_ids: element.res_ids.slice(0),
|
||||
type: 'list',
|
||||
value: element.value,
|
||||
viewType: element.viewType,
|
||||
@@ -443,7 +472,6 @@ var BasicModel = AbstractModel.extend({
|
||||
if (element.fieldsInfo) {
|
||||
list.fieldsInfo = element.fieldsInfo;
|
||||
}
|
||||
this._sortList(list);
|
||||
return list;
|
||||
},
|
||||
/**
|
||||
@@ -685,6 +713,10 @@ var BasicModel = AbstractModel.extend({
|
||||
if (!options.keepChanges) {
|
||||
this.discardChanges(id, {rollback: false});
|
||||
}
|
||||
} else if (element._changes) {
|
||||
_.each(element._changes, function (change) {
|
||||
delete change.isNew;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.context !== undefined) {
|
||||
@@ -877,8 +909,15 @@ var BasicModel = AbstractModel.extend({
|
||||
var list = this.localData[list_id];
|
||||
if (list.type === 'record') {
|
||||
return;
|
||||
} else if (list._changes) {
|
||||
_.each(list._changes, function (change) {
|
||||
delete change.isNew;
|
||||
});
|
||||
}
|
||||
list.offset = 0;
|
||||
// the user manually selected a sort order, so we sort the list with all
|
||||
// the changes he made so far, even if it is editable, however, changes
|
||||
// he'll made after won't be sorted if the list is editable
|
||||
list._keepChangesUnsortedCount = list._changes ? list._changes.length : 0;
|
||||
if (list.orderedBy.length === 0) {
|
||||
list.orderedBy.push({name: fieldName, asc: true});
|
||||
} else if (list.orderedBy[0].name === fieldName){
|
||||
@@ -973,7 +1012,7 @@ var BasicModel = AbstractModel.extend({
|
||||
};
|
||||
return this._makeDefaultRecord(list.model, params).then(function (id) {
|
||||
var position = options && options.position || 'top';
|
||||
list._changes.push({operation: 'ADD', id: id, position: position});
|
||||
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
|
||||
var record = self.localData[id];
|
||||
list._cache[record.res_id] = id;
|
||||
return id;
|
||||
@@ -1368,11 +1407,17 @@ var BasicModel = AbstractModel.extend({
|
||||
// no 'ADD' operation for that dataPoint, as it would mean
|
||||
// that the record wasn't in the relation yet
|
||||
var idsToRemove = command.ids;
|
||||
list._changes = _.reject(list._changes, function (change) {
|
||||
list._changes = _.reject(list._changes, function (change, index) {
|
||||
var idInCommands = _.contains(command.ids, change.id);
|
||||
if (idInCommands && change.operation === 'ADD') {
|
||||
idsToRemove = _.without(idsToRemove, change.id);
|
||||
}
|
||||
// decrement _keepChangesUnsortedCount if we filter out an
|
||||
// operation whose index is smaller than the count (as there
|
||||
// will be one operation less to apply before sorting)
|
||||
if (idInCommands && index < list._keepChangesUnsortedCount) {
|
||||
list._keepChangesUnsortedCount--;
|
||||
}
|
||||
return idInCommands;
|
||||
});
|
||||
_.each(idsToRemove, function (id) {
|
||||
@@ -1425,19 +1470,28 @@ var BasicModel = AbstractModel.extend({
|
||||
*
|
||||
* @private
|
||||
* @param {Object} dataPoint of type list
|
||||
* @param {Object} [options] mostly contains the range of operations to apply
|
||||
* @param {Object} [options.from=0] the index of the first operation to apply
|
||||
* @param {Object} [options.to=length] the index of the last operation to apply
|
||||
* @param {Object} [options.position] if set, each new operation will be set
|
||||
* accordingly at the top or the bottom of the list
|
||||
* @returns {Object} element of type list in which the commands have been
|
||||
* applied
|
||||
*/
|
||||
_applyX2ManyOperations: function (list) {
|
||||
_applyX2ManyOperations: function (list, options) {
|
||||
if (!list.static) {
|
||||
// this function only applies on x2many lists
|
||||
return list;
|
||||
}
|
||||
var self = this;
|
||||
list = _.extend({}, list);
|
||||
list.data = list.data.slice(0);
|
||||
list.res_ids = list.res_ids.slice(0);
|
||||
_.each(list._changes, function (change) {
|
||||
var changes = list._changes || [];
|
||||
if (options) {
|
||||
var to = options.to === 0 ? 0 : (options.to || changes.length);
|
||||
changes = changes.slice(options.from || 0, to);
|
||||
}
|
||||
_.each(changes, function (change) {
|
||||
var relRecord;
|
||||
if (change.id) {
|
||||
relRecord = self.localData[change.id];
|
||||
@@ -1446,7 +1500,7 @@ var BasicModel = AbstractModel.extend({
|
||||
case 'ADD':
|
||||
list.count++;
|
||||
var resID = relRecord ? relRecord.res_id : change.resID;
|
||||
if (change.position === 'top') {
|
||||
if (change.position === 'top' && (options ? options.position !== 'bottom' : true)) {
|
||||
list.res_ids.unshift(resID);
|
||||
} else {
|
||||
list.res_ids.push(resID);
|
||||
@@ -2150,6 +2204,7 @@ var BasicModel = AbstractModel.extend({
|
||||
rawContext: rawContext,
|
||||
relationField: field.relation_field,
|
||||
viewType: view ? view.type : fieldInfo.viewType,
|
||||
_keepChangesUnsorted: fieldInfo.keepChangesUnsorted,
|
||||
});
|
||||
record.data[fieldName] = list.id;
|
||||
if (!fieldInfo.__no_fetch) {
|
||||
@@ -2836,6 +2891,7 @@ var BasicModel = AbstractModel.extend({
|
||||
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
|
||||
openGroupByDefault: params.openGroupByDefault,
|
||||
orderedBy: params.orderedBy || [],
|
||||
_keepChangesUnsorted: params._keepChangesUnsorted,
|
||||
parentID: params.parentID,
|
||||
rawContext: params.rawContext,
|
||||
ref: params.ref || res_id,
|
||||
@@ -3370,7 +3426,9 @@ var BasicModel = AbstractModel.extend({
|
||||
change.id = dataPoint.id;
|
||||
}
|
||||
});
|
||||
list.data.push(dataPoint.id);
|
||||
if (_.contains(list.res_ids, id)) {
|
||||
list.data.push(dataPoint.id);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
});
|
||||
@@ -3461,20 +3519,32 @@ var BasicModel = AbstractModel.extend({
|
||||
},
|
||||
/**
|
||||
* Do a in-memory sort of a list resource data points. This method assumes
|
||||
* that the list data has already been fetched, and that the changes have
|
||||
* been applied. Its intended use is for static datasets, such as a one2many
|
||||
* in a form view.
|
||||
* that the list data has already been fetched, and that the changes that
|
||||
* need to be sorted have already been applied. Its intended use is for
|
||||
* static datasets, such as a one2many in a form view.
|
||||
*
|
||||
* @param {Object} list list dataPoint on which changes have been applied
|
||||
* @param {Object} list list dataPoint on which (some) changes might have
|
||||
* been applied; it is a copy of an internal dataPoint, not the result of
|
||||
* get
|
||||
*/
|
||||
_sortList: function (list) {
|
||||
if (!list.static) {
|
||||
// only sort x2many lists
|
||||
return;
|
||||
}
|
||||
|
||||
if (list.orderedBy.length) {
|
||||
var self = this;
|
||||
|
||||
// sort records according to ordered_by[0]
|
||||
var order = list.orderedBy[0];
|
||||
var data = list.data;
|
||||
data.sort(function (r1, r2) {
|
||||
var data1 = r1.data;
|
||||
var data2 = r2.data;
|
||||
var res_ids = list.res_ids;
|
||||
data.sort(function (record1ID, record2ID) {
|
||||
var r1 = self.localData[record1ID];
|
||||
var r2 = self.localData[record2ID];
|
||||
var data1 = _.extend({}, r1.data, r1._changes);
|
||||
var data2 = _.extend({}, r2.data, r2._changes);
|
||||
if (data1[order.name] < data2[order.name]) {
|
||||
return order.asc ? -1 : 1;
|
||||
}
|
||||
@@ -3483,6 +3553,15 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// sort res_ids accordingly (only the current range of ids, the one
|
||||
// mapping the data, needs to be sorted)
|
||||
var preRangeIDs = res_ids.slice(0, list.offset); // resIDs before the range
|
||||
var postRangeIDs = res_ids.slice(list.offset + list.limit); // resIDs after the range
|
||||
var rangeIDs = _.map(data, function (dataPointID) {
|
||||
return self.localData[dataPointID].res_id;
|
||||
});
|
||||
list.res_ids = preRangeIDs.concat(rangeIDs).concat(postRangeIDs);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -252,6 +252,24 @@ return AbstractRenderer.extend({
|
||||
this.color_map[key] = index;
|
||||
return index;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getLocalState: function () {
|
||||
var $fcScroller = this.$calendar.find('.fc-scroller');
|
||||
return {
|
||||
scrollPosition: $fcScroller.scrollTop(),
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setLocalState: function (localState) {
|
||||
if (localState.scrollPosition) {
|
||||
var $fcScroller = this.$calendar.find('.fc-scroller');
|
||||
$fcScroller.scrollTop(localState.scrollPosition);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -376,6 +394,7 @@ return AbstractRenderer.extend({
|
||||
var $calendar = this.$calendar;
|
||||
var $fc_view = $calendar.find('.fc-view');
|
||||
var scrollPosition = $fc_view.scrollLeft();
|
||||
var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();
|
||||
|
||||
$fc_view.scrollLeft(0);
|
||||
$calendar.fullCalendar('unselect');
|
||||
@@ -420,7 +439,11 @@ return AbstractRenderer.extend({
|
||||
|
||||
this._renderFilters();
|
||||
this.$calendar.appendTo('body');
|
||||
this.$calendar.fullCalendar('render');
|
||||
if (scrollTop) {
|
||||
this.$calendar.fullCalendar('reinitView');
|
||||
} else {
|
||||
this.$calendar.fullCalendar('render');
|
||||
}
|
||||
this._renderEvents();
|
||||
this.$calendar.prependTo(this.$('.o_calendar_view'));
|
||||
|
||||
|
||||
@@ -1474,6 +1474,7 @@ QUnit.module('relational_fields', {
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
"should have one modal in body");
|
||||
form.destroy();
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -5537,6 +5538,118 @@ QUnit.module('relational_fields', {
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('editing tabbed one2many (editable=bottom)', function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
this.data.partner.records[0].turtles = [];
|
||||
for (var i = 0; i < 42; i++) {
|
||||
var id = 100 + i;
|
||||
this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
|
||||
this.data.partner.records[0].turtles.push(id);
|
||||
}
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method);
|
||||
if (args.method === 'write') {
|
||||
assert.deepEqual(args.args[1].turtles[42], [0, false, {turtle_foo: 'rainbow dash'}]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 41,
|
||||
"should have 41 data rows on the current page");
|
||||
assert.ok(form.$('tr.o_data_row').last().hasClass('o_selected_row'),
|
||||
"last row should be selected");
|
||||
|
||||
form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 40,
|
||||
"should have 40 data rows on the current page");
|
||||
|
||||
assert.verifySteps(['read', 'read', 'default_get', 'write', 'read', 'read']);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editing tabbed one2many (editable=top)', function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
this.data.partner.records[0].turtles = [];
|
||||
this.data.turtle.fields.turtle_foo.default = "default foo";
|
||||
for (var i = 0; i < 42; i++) {
|
||||
var id = 100 + i;
|
||||
this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
|
||||
this.data.partner.records[0].turtles.push(id);
|
||||
}
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method);
|
||||
if (args.method === 'write') {
|
||||
assert.deepEqual(args.args[1].turtles[0], [0, false, {turtle_foo: 'rainbow dash'}])
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_pager_next').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 2,
|
||||
"should have 2 data rows on the current page");
|
||||
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 3,
|
||||
"should have 3 data rows on the current page");
|
||||
assert.ok(form.$('tr.o_data_row').first().hasClass('o_selected_row'),
|
||||
"first row should be selected");
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row input').val(), 'default foo',
|
||||
"selected input should have correct string");
|
||||
|
||||
form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 40,
|
||||
"should have 40 data rows on the current page");
|
||||
|
||||
assert.verifySteps(['read', 'read', 'read', 'default_get', 'write', 'read', 'read']);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMany2Many');
|
||||
|
||||
|
||||
@@ -2086,4 +2086,38 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('only x2many lists (static) should be sorted client-side', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.params.modelName = 'partner_type';
|
||||
this.params.res_id = undefined;
|
||||
this.params.orderedBy = [{name: 'display_name', asc: true}];
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
// simulate randomn sort form the server
|
||||
return $.when({
|
||||
length: 3,
|
||||
records: [
|
||||
{id: 12, display_name: "gold", date: "2017-01-25"},
|
||||
{id: 15, display_name: "bronze"},
|
||||
{id: 14, display_name: "silver"},
|
||||
],
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
model.load(this.params).then(function (resultID) {
|
||||
var list = model.get(resultID);
|
||||
assert.deepEqual(_.map(list.data, 'res_id'), [12, 15, 14],
|
||||
"should have kept the order from the server");
|
||||
});
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
});});
|
||||
|
||||
@@ -773,6 +773,153 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('use default_order on editable tree: sort on save', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
this.data.foo.records[0].o2m = [1, 3];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="o2m">' +
|
||||
'<tree editable="bottom" default_order="display_name">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
|
||||
"Value 1 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
|
||||
"Value 3 should be second");
|
||||
|
||||
var $o2m = form.$('.o_field_widget[name=o2m]');
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
|
||||
"Value 1 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
|
||||
"Value 3 should be second");
|
||||
assert.ok(form.$('tbody tr:eq(2) td input').val(),
|
||||
"Value 2 should be third (shouldn't be sorted)");
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
|
||||
"Value 1 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
|
||||
"Value 2 should be second (should be sorted after saving)");
|
||||
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 3)').length,
|
||||
"Value 3 should be third");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('use default_order on editable tree: sort on demand', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
this.data.foo.records[0].o2m = [1, 3];
|
||||
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
|
||||
this.data.bar.records[0].name = "Value 1";
|
||||
this.data.bar.records[2].name = "Value 3";
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="o2m">' +
|
||||
'<tree editable="bottom" default_order="name">' +
|
||||
'<field name="name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
|
||||
"Value 1 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
|
||||
"Value 3 should be second");
|
||||
|
||||
var $o2m = form.$('.o_field_widget[name=o2m]');
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
|
||||
"Value 1 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
|
||||
"Value 3 should be second");
|
||||
assert.ok(form.$('tbody tr:eq(2) td input').val(),
|
||||
"Value 2 should be third (shouldn't be sorted)");
|
||||
|
||||
$o2m.find('.o_column_sortable').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 3)').length,
|
||||
"Value 3 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
|
||||
"Value 2 should be second (should be sorted after saving)");
|
||||
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 1)').length,
|
||||
"Value 1 should be third");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('use default_order on editable tree: sort on demand in page', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
|
||||
|
||||
var ids = [];
|
||||
for (var i=0; i<45; i++) {
|
||||
var id = 4 + i;
|
||||
ids.push(id);
|
||||
this.data.bar.records.push({
|
||||
id: id,
|
||||
name: "Value " + id,
|
||||
});
|
||||
}
|
||||
this.data.foo.records[0].o2m = ids;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="o2m">' +
|
||||
'<tree editable="bottom" default_order="name">' +
|
||||
'<field name="name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// Change page
|
||||
form.$('.o_pager_next').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 44)').length,
|
||||
"record 44 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 48)').length,
|
||||
"record 48 should be last");
|
||||
|
||||
form.$('.o_column_sortable').click();
|
||||
assert.ok(form.$('tbody tr:first td:contains(Value 48)').length,
|
||||
"record 48 should be first");
|
||||
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 44)').length,
|
||||
"record 44 should be first");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('can display button in edit mode', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ class BlogPost(models.Model):
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.url,
|
||||
'target': 'self',
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
|
||||
@@ -783,6 +783,7 @@ class Post(models.Model):
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id),
|
||||
'target': 'self',
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ class SaleOrder(models.Model):
|
||||
if self.can_directly_mark_as_paid:
|
||||
self.action_confirm()
|
||||
if self.env['ir.config_parameter'].sudo().get_param('website_sale.automatic_invoice', default=False):
|
||||
self.payment_tx_id._generate_and_pay_invoice(self.payment_tx_id, self.payment_acquirer_id.provider)
|
||||
self.payment_tx_id._generate_and_pay_invoice()
|
||||
self.payment_tx_id.state = 'done'
|
||||
else:
|
||||
raise ValidationError(_("The quote should be sent and the payment acquirer type should be manual or wire transfer"))
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<t t-if="not bins">
|
||||
<div class="text-center text-muted">
|
||||
<div class="text-center text-muted oe_product">
|
||||
<h3 class="css_editable_display">No product defined.</h3>
|
||||
<p groups="sales_team.group_sale_manager">Click <i>'New'</i> in the top-right corner to create your first product.</p>
|
||||
</div>
|
||||
@@ -420,8 +420,8 @@
|
||||
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
|
||||
<div class="carousel-outer">
|
||||
<div class="carousel-inner">
|
||||
<div class="item active" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + product.product_variant_id['__last_update']}"/>
|
||||
<div t-if="variant_img" class="item" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + product.product_variant_id['__last_update']}"/>
|
||||
<div class="item active" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
|
||||
<div t-if="variant_img" class="item" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<div class="item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
|
||||
</t>
|
||||
@@ -441,10 +441,10 @@
|
||||
|
||||
<ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
|
||||
<li data-target="#o-carousel-product" data-slide-to="0" class="active">
|
||||
<img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.product/{{product.product_variant_ids[0].id}}/image/90x90" t-att-alt="product.name"/>
|
||||
<img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="1" class="">
|
||||
<img class="img img-responsive" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
<img class="img img-responsive" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use strict';
|
||||
$(document).ready(function () {
|
||||
|
||||
// When choosing an delivery carrier, update the quotation and the acquirers
|
||||
var $pay_button = $('.oe_sale_acquirer_button button');
|
||||
$pay_button.prop('disabled', false);
|
||||
|
||||
// When choosing an delivery carrier, update the quotation and the acquirers. Disable the 'Pay
|
||||
// Now' button to avoid being redirected to payment acquier if the delivery carrier update is
|
||||
// not over.
|
||||
var $carrier = $("#delivery_carrier");
|
||||
$carrier.find("input[name='delivery_type']").click(function (ev) {
|
||||
$pay_button.prop('disabled', true);
|
||||
var carrier_id = $(ev.currentTarget).val();
|
||||
window.location.href = '/shop/payment?carrier_id=' + carrier_id;
|
||||
});
|
||||
|
||||
@@ -446,6 +446,7 @@ class Slide(models.Model):
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '%s' % self.website_url,
|
||||
'target': 'self',
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
return super(Slide, self).get_access_action(access_uid)
|
||||
|
||||
@@ -213,6 +213,10 @@ 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 --xmlrpc-port>`
|
||||
|
||||
.. warning:: The livechat worker requires the ``psycogreen`` Python module,
|
||||
which is not always included with all installation packages.
|
||||
It can be manually installed with ``pip install psycogreen``.
|
||||
|
||||
Configuration sample
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ CREATE TABLE ir_module_module (
|
||||
website character varying(256),
|
||||
summary character varying(256),
|
||||
name character varying(128) NOT NULL,
|
||||
author character varying(128),
|
||||
author character varying,
|
||||
icon varchar,
|
||||
state character varying(16),
|
||||
latest_version character varying(64),
|
||||
|
||||
@@ -1538,7 +1538,9 @@ class IrModelData(models.Model):
|
||||
if model == 'ir.model.fields':
|
||||
# Don't remove the LOG_ACCESS_COLUMNS unless _log_access
|
||||
# has been turned off on the model.
|
||||
field = self.env[model].browse(res_id)
|
||||
field = self.env[model].browse(res_id).with_context(
|
||||
prefetch_fields=False,
|
||||
)
|
||||
if not field.exists():
|
||||
_logger.info('Deleting orphan external_ids %s', external_ids)
|
||||
external_ids.unlink()
|
||||
|
||||
+3
-2
@@ -306,7 +306,7 @@ class WebRequest(object):
|
||||
odoo.tools.debugger.post_mortem(
|
||||
odoo.tools.config, sys.exc_info())
|
||||
# otherwise "no active exception to reraise"
|
||||
raise exception
|
||||
raise pycompat.reraise(type(exception), exception, sys.exc_info()[2])
|
||||
|
||||
def _call_function(self, *args, **kwargs):
|
||||
request = self
|
||||
@@ -1420,7 +1420,8 @@ class Root(object):
|
||||
# (the one using the cookie). That is a special feature of the Session Javascript class.
|
||||
# - It could allow session fixation attacks.
|
||||
if not explicit_session and hasattr(response, 'set_cookie'):
|
||||
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
|
||||
response.set_cookie(
|
||||
'session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60, httponly=True)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
+5
-4
@@ -371,11 +371,12 @@ try:
|
||||
|
||||
# TODO when xlsxwriter bump to 0.9.8, add worksheet_class=None parameter instead of kw
|
||||
def add_worksheet(self, name=None, **kw):
|
||||
# invalid Excel character: []:*?/\
|
||||
name = re.sub(r'[\[\]:*?/\\]', '', name)
|
||||
if name:
|
||||
# invalid Excel character: []:*?/\
|
||||
name = re.sub(r'[\[\]:*?/\\]', '', name)
|
||||
|
||||
# maximum size is 31 characters
|
||||
name = name[:31]
|
||||
# maximum size is 31 characters
|
||||
name = name[:31]
|
||||
return super(PatchedXlsxWorkbook, self).add_worksheet(name, **kw)
|
||||
|
||||
xlsxwriter.Workbook = PatchedXlsxWorkbook
|
||||
|
||||
+7
-3
@@ -23,14 +23,18 @@ _CONFDELTYPES = {
|
||||
|
||||
def existing_tables(cr, tablenames):
|
||||
""" Return the names of existing tables among ``tablenames``. """
|
||||
query = """ SELECT table_name FROM information_schema.tables
|
||||
WHERE table_name IN %s AND table_schema != 'information_schema' """
|
||||
query = """
|
||||
SELECT relname
|
||||
FROM pg_class
|
||||
WHERE relname IN %s
|
||||
AND relkind IN ('r', 'v', 'm')
|
||||
"""
|
||||
cr.execute(query, [tuple(tablenames)])
|
||||
return [row[0] for row in cr.fetchall()]
|
||||
|
||||
def table_exists(cr, tablename):
|
||||
""" Return whether the given table exists. """
|
||||
query = "SELECT 1 FROM information_schema.tables WHERE table_name=%s AND table_schema != 'information_schema'"
|
||||
query = "SELECT 1 FROM pg_class WHERE relkind IN ('r', 'v', 'm') AND relname=%s"
|
||||
cr.execute(query, (tablename,))
|
||||
return cr.rowcount
|
||||
|
||||
|
||||
Reference in New Issue
Block a user