[MERGE] forward port branch 11.0 up to 82c901c937

This commit is contained in:
Christophe Simonis
2019-05-02 10:59:21 +02:00
23 changed files with 271 additions and 69 deletions
+1 -1
View File
@@ -739,7 +739,7 @@ class AccountMoveLine(models.Model):
vals['debit'] = amount < 0 and abs(amount) or 0.0
vals['partner_id'] = self.env['res.partner']._find_accounting_partner(self[0].partner_id).id
company_currency = self[0].account_id.company_id.currency_id
writeoff_currency = self[0].currency_id or company_currency
writeoff_currency = self[0].account_id.currency_id or company_currency
if not self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded' and 'amount_currency' not in vals and writeoff_currency != company_currency:
vals['currency_id'] = writeoff_currency.id
sign = 1 if vals['debit'] > 0 else -1
+14
View File
@@ -724,3 +724,17 @@ class account_payment(models.Model):
})
return vals
def _get_invoice_payment_amount(self, inv):
"""
Computes the amount covered by the current payment in the given invoice.
:param inv: an invoice object
:returns: the amount covered by the payment in the invoice
"""
self.ensure_one()
return sum([
data['amount']
for data in inv._get_payments_vals()
if data['account_payment_id'] == self.id
])
@@ -46,7 +46,7 @@
<td><span t-field="inv.date_invoice"/></td>
<td><span t-field="inv.number"/></td>
<td class="text-right"><span t-field="inv.amount_total"/></td>
<td class="text-right"><span t-esc="inv.amount_total - inv.residual" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
<td class="text-right"><span t-esc="o._get_invoice_payment_amount(inv)" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
<td class="text-right"><span t-field="inv.residual"/></td>
</tr>
</tbody>
@@ -33,6 +33,7 @@
<field name="depreciation_date"/>
<filter string="Draft" name="draft" domain="[('state','=','draft')]" help="Assets in draft state"/>
<filter string="Running" name="running" domain="[('state','=','open')]" help="Assets in running state"/>
<filter string="Not archived" name="only_active" domain="[('asset_id.active','=', True)]"/>
<separator/>
<filter string="Posted" name="posted" domain="[('move_check','=',True)]" help="Posted depreciation lines" context="{'unposted_value_visible': 0}"/>
<field name="asset_id"/>
@@ -62,7 +63,7 @@
<field name="view_mode">graph,pivot</field>
<field name="search_view_id" ref="view_asset_asset_report_search"/>
<field name="domain">[('asset_category_id.type', '=', 'purchase')]</field>
<field name="context">{}</field> <!-- force empty -->
<field name="context">{'search_default_only_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No content
@@ -3,10 +3,10 @@
<record id="hr_contract_form_in_inherit" model="ir.ui.view">
<field name="name">hr.contract.form.in.inherit</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_payroll.hr_contract_form_inherit"/>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='struct_id']" position="after">
<xpath expr="//group[@name='duration_group']" position="after">
<group col="2" colspan="2" name="right_column">
<separator colspan="2" string="Allowance"/>
<field name="driver_salay"/>
+1 -1
View File
@@ -729,7 +729,7 @@ class PosOrder(models.Model):
if to_invoice:
pos_order.action_pos_order_invoice()
pos_order.invoice_id.sudo().action_invoice_open()
pos_order.invoice_id.sudo().with_context(force_company=self.env.user.company_id.id).action_invoice_open()
pos_order.account_move = pos_order.invoice_id.move_id
return order_ids
+82 -40
View File
@@ -178,6 +178,61 @@ var ScreenWidget = PosBaseWidget.extend({
}
}
},
/**
* Handles the error response from the server when we push
* an invoiceable order
* Displays appropriates warnings and errors and
* proposes subsequent actions
*
* @private
* @param {PosModel} order: the order to consider, defaults to current order
* @param {Boolean} refresh_screens: whether or not displayed screens should refresh
* @param {Object} error: the error provided by Ajax
*/
_handleFailedPushForInvoice: function (order, refresh_screen, error) {
var self = this;
order = order || this.pos.get_order();
this.invoicing = false;
order.finalized = false;
if (error.message === 'Missing Customer') {
this.gui.show_popup('confirm',{
'title': _t('Please select the Customer'),
'body': _t('You need to select the customer before you can invoice an order.'),
confirm: function(){
self.gui.show_screen('clientlist', null, refresh_screen);
},
});
} else if (error.message === 'Backend Invoice') {
this.gui.show_popup('confirm',{
'title': _t('Please print the invoice from the backend'),
'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
confirm: function () {
this.gui.show_screen('receipt', null, refresh_screen);
},
cancel: function () {
this.gui.show_screen('receipt', null, refresh_screen);
},
});
} else if (error.code < 0) { // XmlHttpRequest Errors
this.gui.show_popup('error',{
'title': _t('The order could not be sent'),
'body': _t('Check your internet connection and try again.'),
cancel: function () {
this.gui.show_screen('receipt', {button_print_invoice: true}, refresh_screen); // refresh if necessary
},
});
} else if (error.code === 200) { // OpenERP Server Errors
this.gui.show_popup('error-traceback',{
'title': error.data.message || _t("Server Error"),
'body': error.data.debug || _t('The server encountered an error while receiving your order.'),
});
} else { // ???
this.gui.show_popup('error',{
'title': _t("Unknown Error"),
'body': _t("The order could not be sent to the server due to an unknown error"),
});
}
},
});
/*--------------------------------------*\
@@ -1536,7 +1591,9 @@ var ReceiptScreenWidget = ScreenWidget.extend({
return this.pos.config.iface_print_auto && !this.pos.get_order()._printed;
},
should_close_immediately: function() {
return this.pos.config.iface_print_via_proxy && this.pos.config.iface_print_skip_screen;
var order = this.pos.get_order();
var invoiced_finalized = order.is_to_invoice() ? order.finalized : true;
return this.pos.config.iface_print_via_proxy && this.pos.config.iface_print_skip_screen && invoiced_finalized;
},
lock_screen: function(locked) {
this._locked = locked;
@@ -1640,9 +1697,32 @@ var ReceiptScreenWidget = ScreenWidget.extend({
self.print();
}
});
var button_print_invoice = this.$('.button.print_invoice');
button_print_invoice.click(function () {
var order = self.pos.get_order();
var invoiced = self.pos.push_and_invoice_order(order);
self.invoicing = true;
invoiced.fail(self._handleFailedPushForInvoice.bind(self, order, true)); // refresh
invoiced.done(function(){
self.invoicing = false;
self.gui.show_screen('receipt', {button_print_invoice: false}, true); // refresh
});
});
},
render_change: function() {
var self = this;
this.$('.change-value').html(this.format_currency(this.pos.get_order().get_change()));
var order = this.pos.get_order();
var order_screen_params = order.get_screen_data('params');
var button_print_invoice = this.$('.button.print_invoice');
if (order_screen_params && order_screen_params.button_print_invoice) {
button_print_invoice.show();
} else {
button_print_invoice.hide();
}
},
render_receipt: function() {
this.$('.pos-receipt-container').html(QWeb.render('PosTicket', this.get_receipt_render_env()));
@@ -2067,45 +2147,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
var invoiced = this.pos.push_and_invoice_order(order);
this.invoicing = true;
invoiced.fail(function(error){
self.invoicing = false;
order.finalized = false;
if (error.message === 'Missing Customer') {
self.gui.show_popup('confirm',{
'title': _t('Please select the Customer'),
'body': _t('You need to select the customer before you can invoice an order.'),
confirm: function(){
self.gui.show_screen('clientlist');
},
});
} else if (error.message === 'Backend Invoice') {
self.gui.show_popup('confirm',{
'title': _t('Please print the invoice from the backend'),
'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
confirm: function () {
this.gui.show_screen('receipt');
},
cancel: function () {
this.gui.show_screen('receipt');
},
});
} else if (error.code < 0) { // XmlHttpRequest Errors
self.gui.show_popup('error',{
'title': _t('The order could not be sent'),
'body': _t('Check your internet connection and try again.'),
});
} else if (error.code === 200) { // OpenERP Server Errors
self.gui.show_popup('error-traceback',{
'title': error.data.message || _t("Server Error"),
'body': error.data.debug || _t('The server encountered an error while receiving your order.'),
});
} else { // ???
self.gui.show_popup('error',{
'title': _t("Unknown Error"),
'body': _t("The order could not be sent to the server due to an unknown error"),
});
}
});
invoiced.fail(this._handleFailedPushForInvoice.bind(this, order, false));
invoiced.done(function(){
self.invoicing = false;
@@ -712,6 +712,9 @@
</span>
</div>
<div class="centered-content touch-scrollable">
<div class="button print_invoice">
<i class='fa fa-print'></i> Print Invoice
</div>
<div class="button print">
<i class='fa fa-print'></i> Print Receipt
</div>
@@ -333,6 +333,9 @@ class ProductTemplate(models.Model):
related_vals['volume'] = vals['volume']
if vals.get('weight'):
related_vals['weight'] = vals['weight']
# Please do forward port
if vals.get('packaging_ids'):
related_vals['packaging_ids'] = vals['packaging_ids']
if related_vals:
template.write(related_vals)
return template
+13 -1
View File
@@ -112,7 +112,7 @@
colspan="4"
attrs="{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}"
groups="product.group_stock_packaging">
<field name="packaging_ids" nolabel="1" context="{'default_product_id': product_variant_id,'tree_view_ref':'product.product_packaging_tree_view', 'form_view_ref':'product.product_packaging_form_view'}"/>
<field name="packaging_ids" nolabel="1" context="{'default_product_id': product_variant_id,'tree_view_ref':'product.product_packaging_tree_view', 'form_view_ref':'product.product_packaging_form_view2'}"/>
</group>
</page>
</notebook>
@@ -507,6 +507,18 @@
</field>
</record>
<!-- Don't forward port in V >= saas-11.5 -->
<record id="product_packaging_form_view2" model="ir.ui.view">
<field name="name">product.packaging.form.view2</field>
<field name="model">product.packaging</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="product.product_packaging_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="replace"/>
<xpath expr="//field[@name='id']" position="replace"/>
</field>
</record>
<record model="ir.actions.act_window" id="action_packaging_view">
<field name="name">Product Packagings</field>
<field name="res_model">product.packaging</field>
@@ -112,16 +112,6 @@
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="portal.message_thread">
<t t-set="object" t-value="project"/>
</t>
</div>
</div>
</t>
</template>
+9
View File
@@ -1059,3 +1059,12 @@ class Picking(models.Model):
action['domain'] = [('id', 'in', packages.ids)]
action['context'] = {'picking_id': self.id}
return action
def action_picking_move_tree(self):
action = self.env.ref('stock.stock_move_action').read()[0]
action['views'] = [
(self.env.ref('stock.view_picking_move_tree').id, 'tree'),
]
action['context'] = self.env.context
action['domain'] = [('picking_id', 'in', self.ids)]
return action
+32
View File
@@ -66,6 +66,38 @@
</field>
</record>
<record id="view_picking_move_tree" model="ir.ui.view">
<field name="name">stock.picking.move.tree</field>
<field name="model">stock.move</field>
<field eval="50" name="priority"/>
<field name="arch" type="xml">
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability and show_reserved_availability" decoration-muted="scrapped == True or state == 'cancel' or (state == 'done' and is_locked == True)" string="Stock Moves" editable="bottom">
<field name="name" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="state" invisible="1" readonly="0"/>
<field name="picking_type_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="scrapped" invisible="1"/>
<field name="picking_code" invisible="1"/>
<field name="product_type" invisible="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="show_reserved_availability" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
<field name="has_move_lines" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="product_id" required="1" attrs="{'readonly': ['|', '&amp;', ('state', '!=', 'draft'), ('additional', '=', False), ('has_move_lines', '=', True)]}"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
<field name="reserved_availability" string="Reserved"/>
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
</tree>
</field>
</record>
<record id="view_move_kandan" model="ir.ui.view">
<field name="name">stock.move.kanban</field>
<field name="model">stock.move</field>
@@ -233,6 +233,19 @@
class="oe_stat_button" icon="fa-cubes"
attrs="{'invisible': [('has_packages', '=', False)]}"/>
<button name="%(action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability Report" type="action" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('has_tracking', '=', False)]}" groups="stock.group_production_lot"/>
<!-- Use the following button to avoid onchange on one2many -->
<button name="action_picking_move_tree"
class="oe_stat_button"
icon="fa-arrows-v"
type="object"
help="List view of operations"
groups="base.group_no_one"
attrs="{'invisible': ['|', '&amp;', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}"
context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Operations</span>
</div>
</button>
</div>
<h1 class="hidden-xs">
<field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/>
+1 -1
View File
@@ -558,7 +558,7 @@ class StockMove(models.Model):
debit_value = self.company_id.currency_id.round(valuation_amount)
# check that all data is correct
if self.company_id.currency_id.is_zero(debit_value):
if self.company_id.currency_id.is_zero(debit_value) and not self.env['ir.config_parameter'].sudo().get_param('stock_account.allow_zero_cost'):
raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.display_name,))
credit_value = debit_value
@@ -204,6 +204,10 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
});
}
} else {
// ignore Chrome video internal error: https://crbug.com/809574
if (!error && message === 'ResizeObserver loop limit exceeded') {
return;
}
var traceback = error ? error.stack : '';
crash_manager.show_error({
type: _t("Odoo Client Error"),
@@ -60,10 +60,11 @@ return AbstractModel.extend({
end.add(-1, 'days');
}
var isDateEvent = this.fields[this.mapping.date_start].type === 'date';
// An "allDay" event without the "all_day" option is not considered
// as a 24h day. It's just a part of the day (by default: 7h-19h).
if (event.allDay) {
if (!this.mapping.all_day) {
if (!this.mapping.all_day && !isDateEvent) {
if (event.r_start) {
start.hours(event.r_start.hours())
.minutes(event.r_start.minutes())
@@ -2236,6 +2236,67 @@ QUnit.module('Views', {
calendar.destroy();
});
QUnit.test('timzeone does not affect calendar with date field', function (assert) {
assert.expect(8);
var calendar = createView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar date_start="start_date" mode="month">'+
'<field name="name"/>'+
'<field name="start_date"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
mockRPC: function (route, args) {
if (args.method === "create") {
assert.strictEqual(args.args[0].start_date, "2016-12-20 00:00:00");
}
if (args.method === "write") {
assert.step(args.args[1].start_date);
}
return this._super(route, args);
},
session: {
getTZOffset: function () {
return 120; // 2 hours timezone
},
},
});
// Create event (on 20 december)
var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)');
testUtils.triggerMouseEvent($cell, "mousedown");
testUtils.triggerMouseEvent($cell, "mouseup");
var $input = $('.modal-body input:first');
$input.val("An event").trigger('input');
$('.modal button.btn:contains(Create)').trigger('click');
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "12/20/2016")
// Move event to another day (on 27 november)
testUtils.dragAndDrop(
calendar.$('.fc-event').first(),
calendar.$('.fc-day-top').first()
);
assert.verifySteps(["2016-11-27 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "11/27/2016")
// Move event to last day (on 7 january)
testUtils.dragAndDrop(
calendar.$('.fc-event').first(),
calendar.$('.fc-day-top').last()
);
assert.verifySteps(["2016-11-27 00:00:00", "2017-01-07 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017")
calendar.destroy();
});
QUnit.test('form_view_id attribute works (for creating events)', function (assert) {
assert.expect(1);
+2 -6
View File
@@ -22,12 +22,8 @@ class WebsiteForum(http.Controller):
_user_per_page = 30
def _get_notifications(self):
badge_subtype = request.env.ref('gamification.mt_badge_granted')
if badge_subtype:
msg = request.env['mail.message'].search([('subtype_id', '=', badge_subtype.id), ('needaction', '=', True)])
else:
msg = list()
return msg
"""Deprecated"""
return list()
def _prepare_forum_values(self, forum=None, **kwargs):
values = {
@@ -45,7 +45,7 @@ class Job(models.Model):
default_description = self.env["ir.model.data"].xmlid_to_object("website_hr_recruitment.default_website_description")
return (default_description.render() if default_description else "")
website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description)
website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description, prefetch=False)
@api.multi
def _compute_website_url(self):
@@ -370,6 +370,10 @@ class FloatTimeConverter(models.AbstractModel):
def value_to_html(self, value, options):
sign = math.copysign(1.0, value)
hours, minutes = divmod(abs(value) * 60, 60)
minutes = round(minutes)
if minutes == 60:
minutes = 0
hours += 1
return '%02d:%02d' % (sign * hours, minutes)
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from openerp.tests.common import TransactionCase
from odoo.tests.common import TransactionCase
class One2manyCase(TransactionCase):
@@ -51,6 +51,8 @@ class One2manyCase(TransactionCase):
self.multi.lines = self.multi.lines[:-1]
self.assertEqual(len(self.multi.lines), 9)
self.assertIn("hello", self.multi.lines.mapped('name'))
if not self.multi.id:
return
# Invalidate the cache and check again; this crashes if the value
# of self.multi.lines in cache contains new records
self.multi.invalidate_cache()
@@ -89,11 +91,26 @@ class One2manyCase(TransactionCase):
self.multi.lines = [(0, 0, {"name": str(name)})]
self.operations()
def test_rpcstyle_one_by_one_on_new(self):
self.multi = self.env["test_new_api.multi"].new({
"name": "What is up?"
})
for name in range(10):
self.multi.lines = [(0, 0, {"name": str(name)})]
self.operations()
def test_rpcstyle_single(self):
"""Check lines created with RPC style and added in one step"""
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations()
def test_rpcstyle_single_on_new(self):
self.multi = self.env["test_new_api.multi"].new({
"name": "What is up?"
})
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations()
def test_many2one_integer(self):
"""Test several models one2many with same inverse Integer field"""
# utility function to convert records to tuples with id,name
+2 -2
View File
@@ -2115,8 +2115,8 @@ class _RelationalMulti(_Relational):
elif isinstance(value, (list, tuple)):
# value is a list/tuple of commands, dicts or record ids
comodel = record.env[self.comodel_name]
# determine the value ids; by convention empty on new records
ids = OrderedSet(record[self.name].ids if record.id else ())
# determine the value ids
ids = OrderedSet(record[self.name]._ids)
# modify ids with the commands
for command in value:
if isinstance(command, (tuple, list)):