diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-09-06 17:40:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-09-06 17:40:59 +0200 |
| commit | d5382abeaad4d93b8def7e63cd47a86baaba8f0b (patch) | |
| tree | 6364ff760709b541eebdde4b04494b480dec87fb /addons | |
| parent | c75fe95dfca366bf6c3b6a51908677039488aa4f (diff) | |
| parent | b8dd34fcbb895b8d62f3e133dcf5ee6f40a1400d (diff) | |
[MERGE] forward port branch saas-17 up to b8dd34fcbb8
Diffstat (limited to 'addons')
46 files changed, 618 insertions, 145 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 16d1bf22015..b003051e499 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -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', diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 864037a65ef..b136af9a4ed 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -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) { diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index c86db44e26c..40324f194dc 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -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, { diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py index cf324b44abd..a59cfa9205c 100644 --- a/addons/base_import/tests/test_base_import.py +++ b/addons/base_import/tests/test_base_import.py @@ -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): diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index f06cea3f434..90d24275051 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -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: diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py index c373bbe23f4..76d442a4b67 100644 --- a/addons/hr_attendance/models/hr_attendance.py +++ b/addons/hr_attendance/models/hr_attendance.py @@ -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))), diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index d30810eeb1e..c7c07bcee62 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -301,6 +301,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() res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment') @@ -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 diff --git a/addons/hr_expense/security/ir_rule.xml b/addons/hr_expense/security/ir_rule.xml index 7148e828231..eba7fe52e1a 100644 --- a/addons/hr_expense/security/ir_rule.xml +++ b/addons/hr_expense/security/ir_rule.xml @@ -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> diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 2a8b9743d2f..9a75f999347 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -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> diff --git a/addons/hr_timesheet/views/hr_views.xml b/addons/hr_timesheet/views/hr_views.xml index d87abf0fb60..21c645d8159 100644 --- a/addons/hr_timesheet/views/hr_views.xml +++ b/addons/hr_timesheet/views/hr_views.xml @@ -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"> diff --git a/addons/l10n_fr/migrations/9.0.1.1/post-migrate_tags_on_taxes.py b/addons/l10n_fr/migrations/9.0.1.1/post-migrate_tags_on_taxes.py new file mode 100644 index 00000000000..6f933404f1d --- /dev/null +++ b/addons/l10n_fr/migrations/9.0.1.1/post-migrate_tags_on_taxes.py @@ -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) + diff --git a/addons/l10n_fr/migrations/9.0.1.1/pre-set_tags_and_taxes_updatable.py b/addons/l10n_fr/migrations/9.0.1.1/pre-set_tags_and_taxes_updatable.py new file mode 100644 index 00000000000..586ae984b8c --- /dev/null +++ b/addons/l10n_fr/migrations/9.0.1.1/pre-set_tags_and_taxes_updatable.py @@ -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') + diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 6146b5b37e5..ef1f09c8edc 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -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']) diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index 382efabbd35..c566d2a4706 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -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]; diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 337f3e9f9d1..77d1cca435a 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -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(); +}); + }); }); diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 69ad489da17..1125ed663af 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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, diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index 979eff2ccf9..d6869e71b33 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -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> diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index cc4774192d4..803340e4696 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -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'])]}"/> diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index 92a0affab4f..5e274b1633c 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -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> diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml index 4675092417a..ddcde4b3065 100644 --- a/addons/mrp/wizard/mrp_product_produce_views.xml +++ b/addons/mrp/wizard/mrp_product_produce_views.xml @@ -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> diff --git a/addons/mrp_repair/data/ir_sequence_data.xml b/addons/mrp_repair/data/ir_sequence_data.xml index 3a47f77e404..095afd57002 100644 --- a/addons/mrp_repair/data/ir_sequence_data.xml +++ b/addons/mrp_repair/data/ir_sequence_data.xml @@ -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> diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index 274faac3639..1457a321e56 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -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 diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index c2b3fad52f0..ccdd0fb6616 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -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; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 77f0189321b..50b9c312966 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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(){ diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 07c70419b89..0f30f231aa1 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -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) diff --git a/addons/sale_timesheet/tests/test_timesheet_revenue.py b/addons/sale_timesheet/tests/test_timesheet_revenue.py index ad9ec1cdc68..19dd8a6f3c8 100644 --- a/addons/sale_timesheet/tests/test_timesheet_revenue.py +++ b/addons/sale_timesheet/tests/test_timesheet_revenue.py @@ -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 diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 391542c829f..3f64fe60ecc 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -354,6 +354,13 @@ class Product(models.Model): 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) if 'active' in values and not values['active'] and self.mapped('orderpoint_ids').filtered(lambda r: r.active): @@ -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 diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index 01180928be2..e0567e04061 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -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"/> diff --git a/addons/stock/views/stock_move_line_views.xml b/addons/stock/views/stock_move_line_views.xml index 929e22c3c12..6f9c7bedb36 100644 --- a/addons/stock/views/stock_move_line_views.xml +++ b/addons/stock/views/stock_move_line_views.xml @@ -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"/> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 017f035be4a..82169779185 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -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"/> diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index d4564d97959..cadeaea8bb8 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -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) diff --git a/addons/web/static/src/js/chrome/search_inputs.js b/addons/web/static/src/js/chrome/search_inputs.js index 2dba4c0dd99..d734bb0ebf2 100644 --- a/addons/web/static/src/js/chrome/search_inputs.js +++ b/addons/web/static/src/js/chrome/search_inputs.js @@ -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()}] } }]); } diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index dce47fa5a10..88bfae2b545 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -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'); + } } }); diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index 065b3b360e5..fc45e669407 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -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. diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 77857ef2d31..105d7f30d69 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -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'; diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 5eca6c5374f..7952b75d7f0 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -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); } }, /** diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index 422258b4035..9ec70551413 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -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')); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index a55065a5b7c..5dc17e6fb59 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -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'); diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index 2c5f6fe8993..a4a19ab620d 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -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(); + }); + });}); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 2ac373dbcd0..ce246ef17d6 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -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); diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index cb50a8682b3..d215ba39ddd 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -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, } diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index b9d6ea0e865..e8349e0cb9e 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -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, } diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 724d24b5c03..67068bff115 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -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")) diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 9f3b79d4b54..81a7c9e0578 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -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)"> diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index ff48ebd9dfd..f6f8386a515 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -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; }); diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 3b6dfb2ef0c..fbf48b596ad 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -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) |
