[MERGE] forward port branch saas-11.3 up to ff80efd71e
This commit is contained in:
@@ -185,14 +185,15 @@ class account_abstract_payment(models.AbstractModel):
|
||||
:param currency: If not specified, search a default currency on wizard/journal.
|
||||
:return: The total amount to pay the invoices.
|
||||
'''
|
||||
# Get the payment currency
|
||||
if not currency:
|
||||
currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id
|
||||
|
||||
# Get the payment invoices
|
||||
if not invoices:
|
||||
invoices = self.invoice_ids
|
||||
|
||||
# Get the payment currency
|
||||
if not currency:
|
||||
currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id or invoices and invoices[0].currency_id
|
||||
|
||||
# Avoid currency rounding issues by summing the amounts according to the company_currency_id before
|
||||
total = 0.0
|
||||
groups = groupby(invoices, lambda i: i.currency_id)
|
||||
@@ -210,6 +211,14 @@ class account_register_payments(models.TransientModel):
|
||||
_inherit = 'account.abstract.payment'
|
||||
_description = "Register payments on multiple invoices"
|
||||
|
||||
@api.onchange('journal_id')
|
||||
def _onchange_journal(self):
|
||||
res = super(account_register_payments, self)._onchange_journal()
|
||||
active_ids = self._context.get('active_ids')
|
||||
invoices = self.env['account.invoice'].browse(active_ids)
|
||||
self.amount = abs(self._compute_payment_amount(invoices))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
rec = super(account_register_payments, self).default_get(fields)
|
||||
|
||||
@@ -351,7 +351,9 @@ class AccountAssetAsset(models.Model):
|
||||
def set_to_close(self):
|
||||
move_ids = self._get_disposal_moves()
|
||||
if move_ids:
|
||||
self._return_disposal_view(move_ids)
|
||||
return self._return_disposal_view(move_ids)
|
||||
# Fallback, as if we just clicked on the smartbutton
|
||||
return self.open_entries()
|
||||
|
||||
@api.multi
|
||||
def set_to_draft(self):
|
||||
|
||||
@@ -106,7 +106,7 @@ The purpose of this component is to listen to keypresses to detect barcodes, the
|
||||
on core.bus inside a 'barcode_event'.
|
||||
All keypress events are buffered until there is no more keypress during 50ms or a carriage return / tab is
|
||||
inputted (because most barcode scanners use this as a suffix).
|
||||
If the buffered keys looks like a barcode (match the the regexp /.{3,}[\n\r\t]*), an event is triggered :
|
||||
If the buffered keys looks like a barcode, an event is triggered :
|
||||
core.bus.trigger('barcode_scanned', barcode);
|
||||
Otherwise, the keypresses are 'resent'. However, for security reasons, a keypress event programmatically
|
||||
crafted doesn't trigger native browser behaviors. For this reason, BarcodeEvents doesn't intercept keypresses
|
||||
|
||||
@@ -248,7 +248,7 @@ var BoardRenderer = FormRenderer.extend({
|
||||
}
|
||||
var view = _.find(action.views, function (descr) {
|
||||
return descr[1] === params.viewType;
|
||||
});
|
||||
}) || [false, params.viewType];
|
||||
return self.loadViews(action.res_model, context, [view])
|
||||
.then(function (viewsInfo) {
|
||||
var viewInfo = viewsInfo[params.viewType];
|
||||
|
||||
@@ -24,7 +24,7 @@ class MailActivity(models.Model):
|
||||
'default_res_id': self.env.context.get('default_res_id'),
|
||||
'default_res_model': self.env.context.get('default_res_model'),
|
||||
'default_name': self.summary or self.res_name,
|
||||
'default_description': self.note and tools.html2plaintext(self.note) or '',
|
||||
'default_description': self.note and tools.html2plaintext(self.note).strip() or '',
|
||||
'default_activity_ids': [(6, 0, self.ids)],
|
||||
}
|
||||
return action
|
||||
|
||||
@@ -149,14 +149,22 @@ class Team(models.Model):
|
||||
tree_view_id = self.env.ref('crm.crm_case_tree_view_oppor').id
|
||||
form_view_id = self.env.ref('crm.crm_case_form_view_oppor').id
|
||||
kanb_view_id = self.env.ref('crm.crm_case_kanban_view_leads').id
|
||||
action['views'] = [
|
||||
[kanb_view_id, 'kanban'],
|
||||
[tree_view_id, 'tree'],
|
||||
[form_view_id, 'form'],
|
||||
[False, 'graph'],
|
||||
[False, 'calendar'],
|
||||
[False, 'pivot']
|
||||
]
|
||||
|
||||
mode_string = action.get('view_mode', '')
|
||||
view_mode = mode_string.split(',') if mode_string else ['kanban', 'tree', 'form', 'graph', 'calendar', 'pivot']
|
||||
dict_views = {
|
||||
'kanban': kanb_view_id,
|
||||
'tree': tree_view_id,
|
||||
'form': form_view_id,
|
||||
'graph': False,
|
||||
'calendar': False,
|
||||
'pivot': False,
|
||||
}
|
||||
|
||||
# Sort the views according to the view mode of the action
|
||||
# which can be modified via studio, or technical menu for that matter
|
||||
action['views'] = [[dict_views.get(view_type, False), view_type] for view_type in view_mode]
|
||||
|
||||
action['context'] = action_context
|
||||
return action
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class AccountAnalyticLine(models.Model):
|
||||
vals['account_id'] = project.analytic_account_id.id
|
||||
vals['company_id'] = project.analytic_account_id.company_id.id
|
||||
if not project.analytic_account_id.active:
|
||||
raise UserError(_('The project you are timesheeting on is not linked to a active analytic account. Please the project configuration.'))
|
||||
raise UserError(_('The project you are timesheeting on is not linked to an active analytic account. Set one on the project configuration.'))
|
||||
# employee implies user
|
||||
if vals.get('employee_id') and not vals.get('user_id'):
|
||||
employee = self.env['hr.employee'].browse(vals['employee_id'])
|
||||
|
||||
@@ -48,7 +48,7 @@ class Project(models.Model):
|
||||
allow_timesheets = values['allow_timesheets'] if 'allow_timesheets' in values else self.default_get(['allow_timesheets'])['allow_timesheets']
|
||||
if allow_timesheets and not values.get('analytic_account_id'):
|
||||
analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': values.get('name', _('Unkwon Analytic Account')),
|
||||
'name': values.get('name', _('Unknown Analytic Account')),
|
||||
'company_id': values.get('company_id', self.env.user.company_id.id),
|
||||
'partner_id': values.get('partner_id'),
|
||||
'active': True,
|
||||
@@ -96,7 +96,7 @@ class Task(models.Model):
|
||||
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", related='project_id.analytic_account_id', readonly=True)
|
||||
allow_timesheets = fields.Boolean("Allow timesheets", compute='_compute_allow_timesheets', help="Timesheets can be logged on this task.")
|
||||
remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', inverse='_inverse_remaining_hours', help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
|
||||
remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
|
||||
effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.")
|
||||
total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.")
|
||||
progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.")
|
||||
@@ -130,11 +130,6 @@ class Task(models.Model):
|
||||
for task in self:
|
||||
task.remaining_hours = task.planned_hours - task.effective_hours - task.subtask_effective_hours
|
||||
|
||||
@api.onchange('remaining_hours')
|
||||
def _inverse_remaining_hours(self):
|
||||
for task in self:
|
||||
task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours
|
||||
|
||||
@api.depends('effective_hours', 'subtask_effective_hours')
|
||||
def _compute_total_hours_spent(self):
|
||||
for task in self:
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event
|
||||
from . import mass_mailing
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class MassMailingCampaign(models.Model):
|
||||
_inherit = "mail.mass_mailing"
|
||||
|
||||
@api.onchange('mailing_model_id', 'contact_list_ids')
|
||||
def _onchange_model_and_list(self):
|
||||
result = super(MassMailingCampaign, self)._onchange_model_and_list()
|
||||
if self.mailing_model_name == 'event.registration' and self.mailing_domain == '[]':
|
||||
self.mailing_domain = self.env.context.get('default_mailing_domain', '[]')
|
||||
return result
|
||||
@@ -73,6 +73,11 @@
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group name="description_internal">
|
||||
<group string="Description for Internal" colspan="4">
|
||||
<field name="description" nolabel="1" placeholder="This note is only for internal purposes."/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Sales" attrs="{'invisible':[('sale_ok','=',False)]}" name="sales">
|
||||
<div name="pricelist_item" groups="product.group_product_pricelist">
|
||||
@@ -109,11 +114,6 @@
|
||||
<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'}"/>
|
||||
</group>
|
||||
</page>
|
||||
<group name="description_internal">
|
||||
<group string="Description for Internal">
|
||||
<field name="description" nolabel="1" placeholder="This note is only for internal purposes."/>
|
||||
</group>
|
||||
</group>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -119,7 +119,7 @@ class PurchaseRequisition(models.Model):
|
||||
def action_in_progress(self):
|
||||
self.ensure_one()
|
||||
if not all(obj.line_ids for obj in self):
|
||||
raise UserError(_('You cannot confirm agreement because there is no product line.'))
|
||||
raise UserError(_("You cannot confirm agreement '%s' because there is no product line.") % self.name)
|
||||
if self.type_id.quantity_copy == 'none' and self.vendor_id:
|
||||
for requisition_line in self.line_ids:
|
||||
if requisition_line.price_unit <= 0.0:
|
||||
|
||||
@@ -78,6 +78,10 @@ class StockMove(models.Model):
|
||||
vals['purchase_line_id'] = self.purchase_line_id.id
|
||||
return vals
|
||||
|
||||
def _clean_merged(self):
|
||||
super(StockMove, self)._clean_merged()
|
||||
self.write({'created_purchase_line_id': False})
|
||||
|
||||
def _action_done(self):
|
||||
res = super(StockMove, self)._action_done()
|
||||
self.mapped('purchase_line_id').sudo()._update_received_qty()
|
||||
|
||||
@@ -173,14 +173,14 @@ class TestSaleStock(TestSale):
|
||||
|
||||
# Check invoice
|
||||
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" after picking return' % self.so.invoice_status)
|
||||
self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, 'Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered)
|
||||
self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, msg='Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered)
|
||||
# let's do an invoice with refunds
|
||||
adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({
|
||||
'advance_payment_method': 'all',
|
||||
})
|
||||
adv_wiz.with_context(open_invoices=True).create_invoices()
|
||||
self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft')
|
||||
self.assertAlmostEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, 'Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity)
|
||||
self.assertAlmostEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, msg='Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity)
|
||||
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status)
|
||||
|
||||
def test_03_sale_stock_delivery_partial(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo.tools.misc import split_every
|
||||
@@ -318,9 +318,18 @@ class ProcurementGroup(models.Model):
|
||||
orderpoints_noprefetch = orderpoints_noprefetch[1000:]
|
||||
|
||||
# Calculate groups that can be executed together
|
||||
location_data = defaultdict(lambda: dict(products=self.env['product.product'], orderpoints=self.env['stock.warehouse.orderpoint'], groups=list()))
|
||||
location_data = OrderedDict()
|
||||
|
||||
def makedefault():
|
||||
return {
|
||||
'products': self.env['product.product'],
|
||||
'orderpoints': self.env['stock.warehouse.orderpoint'],
|
||||
'groups': []
|
||||
}
|
||||
|
||||
for orderpoint in orderpoints:
|
||||
key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id])
|
||||
location_data[key] = makedefault()
|
||||
location_data[key]['products'] += orderpoint.product_id
|
||||
location_data[key]['orderpoints'] += orderpoint
|
||||
location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id])
|
||||
|
||||
@@ -565,6 +565,10 @@ class StockMove(models.Model):
|
||||
move.package_level_id.id
|
||||
]
|
||||
|
||||
def _clean_merged(self):
|
||||
"""Cleanup hook used when merging moves"""
|
||||
self.write({'propagate': False})
|
||||
|
||||
def _merge_moves(self, merge_into=False):
|
||||
""" This method will, for each move in `self`, go up in their linked picking and try to
|
||||
find in their existing moves a candidate into which we can merge the move.
|
||||
@@ -602,7 +606,7 @@ class StockMove(models.Model):
|
||||
|
||||
if moves_to_unlink:
|
||||
# We are using propagate to False in order to not cancel destination moves merged in moves[0]
|
||||
moves_to_unlink.write({'propagate': False})
|
||||
moves_to_unlink._clean_merged()
|
||||
moves_to_unlink._action_cancel()
|
||||
moves_to_unlink.sudo().unlink()
|
||||
return (self | self.env['stock.move'].concat(*moves_to_merge)) - moves_to_unlink
|
||||
|
||||
@@ -142,6 +142,7 @@ var KanbanColumnProgressBar = Widget.extend({
|
||||
|
||||
// Display and animate the progress bars
|
||||
var barNumber = 0;
|
||||
var barMinWidth = 6; // In %
|
||||
_.each(self.colors, function (val, key) {
|
||||
var $bar = self.$bars[val];
|
||||
var count = self.subgroupCounts && self.subgroupCounts[key] || 0;
|
||||
@@ -168,16 +169,15 @@ var KanbanColumnProgressBar = Widget.extend({
|
||||
$bar.addClass('o_bar_has_records');
|
||||
// Make sure every bar that has records has some space
|
||||
// and that everything adds up to 100%
|
||||
var minWidth = 6*barNumber;
|
||||
var maxWidth = 100 - minWidth;
|
||||
var maxWidth = 100 - barMinWidth * barNumber;
|
||||
self.$('.progress-bar.o_bar_has_records').css('max-width', maxWidth + '%');
|
||||
self.$('.progress-bar.o_bar_has_records').css('min-width', minWidth + '%');
|
||||
$bar.css('width', (count * 100 / self.groupCount) + '%');
|
||||
barNumber++;
|
||||
} else {
|
||||
$bar.css('width', '');
|
||||
}
|
||||
});
|
||||
this.$('.progress-bar.o_bar_has_records').css('min-width', barMinWidth + '%');
|
||||
|
||||
// Display and animate the counter number
|
||||
var start = this.prevTotalCounterValue;
|
||||
|
||||
@@ -541,7 +541,7 @@ var PivotModel = AbstractModel.extend({
|
||||
_loadData: function () {
|
||||
var self = this;
|
||||
var groupBys = [];
|
||||
var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys;
|
||||
var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys;
|
||||
var colGroupBys = this.data.colGroupBys;
|
||||
var measures = _.map(this.data.measures, function(measure) {
|
||||
if (self.fields[measure].type === 'many2one') {
|
||||
@@ -624,7 +624,7 @@ var PivotModel = AbstractModel.extend({
|
||||
});
|
||||
|
||||
var index = 0;
|
||||
var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys;
|
||||
var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys;
|
||||
var colGroupBys = this.data.colGroupBys;
|
||||
var datapt, row, col, attrs, cell_value;
|
||||
var main_row_header, main_col_header;
|
||||
|
||||
@@ -44,7 +44,7 @@ class WebsiteSaleDigital(CustomerPortal):
|
||||
product_id = product.id
|
||||
template = product.product_tmpl_id
|
||||
att = Attachment.search_read(
|
||||
domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)],
|
||||
domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id), ('product_downloadable', '=', True)],
|
||||
fields=['name', 'write_date'],
|
||||
order='write_date desc',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Switzerland, 2018-07-31
|
||||
|
||||
International Business Hub agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Sandro Pignataro pignatarosandro@gmail.com https://github.com/Pigna74
|
||||
|
||||
List of contributors:
|
||||
|
||||
Guido Notari guido.notari@gmail.com https://github.com/gnotari
|
||||
Matteo Bilotta byloth@gmail.com https://github.com/Byloth
|
||||
|
||||
+4
-1
@@ -71,7 +71,10 @@ def _initialize_db(id, db_name, demo, lang, user_password, login='admin', countr
|
||||
if country_code:
|
||||
countries = env['res.country'].search([('code', 'ilike', country_code)])
|
||||
if countries:
|
||||
env['res.company'].browse(1).country_id = countries[0]
|
||||
comp_local = {'country_id': countries[0].id}
|
||||
if countries[0].currency_id:
|
||||
comp_local['currency_id'] = countries[0].currency_id.id
|
||||
env['res.company'].browse(1).write(comp_local)
|
||||
|
||||
# update admin's password and lang and login
|
||||
values = {'password': user_password, 'lang': lang}
|
||||
|
||||
Reference in New Issue
Block a user