[MERGE] forward port branch saas-11.3 up to 387d5cff90
closes odoo/odoo#30566
This commit is contained in:
@@ -419,9 +419,6 @@ class AccountMove(models.Model):
|
||||
journal_id=journal_id,
|
||||
auto=auto)
|
||||
reversed_moves |= reversed_move
|
||||
#unreconcile all lines reversed
|
||||
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
|
||||
aml.remove_move_reconcile()
|
||||
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
|
||||
for account in set([x.account_id for x in aml]):
|
||||
to_rec = aml.filtered(lambda y: y.account_id == account)
|
||||
@@ -438,6 +435,18 @@ class AccountMove(models.Model):
|
||||
def open_reconcile_view(self):
|
||||
return self.line_ids.open_reconcile_view()
|
||||
|
||||
# FIXME: Clarify me and change me in master
|
||||
@api.multi
|
||||
def action_duplicate(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_move_journal_line').read()[0]
|
||||
action['target'] = 'inline'
|
||||
action['context'] = dict(self.env.context)
|
||||
action['context']['view_no_maturity'] = False
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
action['res_id'] = self.copy().id
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _run_reverses_entries(self):
|
||||
''' This method is called from a cron job. '''
|
||||
@@ -627,7 +636,7 @@ class AccountMoveLine(models.Model):
|
||||
index=True, store=True, copy=False) # related is required
|
||||
blocked = fields.Boolean(string='No Follow-up', default=False,
|
||||
help="You can check this box to mark this journal item as a litigation with the associated partner")
|
||||
date_maturity = fields.Date(string='Due date', index=True, required=True,
|
||||
date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False,
|
||||
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
|
||||
date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required
|
||||
analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines")
|
||||
|
||||
@@ -204,7 +204,7 @@ class ResPartner(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _credit_debit_get(self):
|
||||
tables, where_clause, where_params = self.env['account.move.line']._query_get()
|
||||
tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get()
|
||||
where_params = [tuple(self.ids)] + where_params
|
||||
if where_clause:
|
||||
where_clause = 'AND ' + where_clause
|
||||
|
||||
@@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({
|
||||
|
||||
field_registry.add('payment', ShowPaymentLineWidget);
|
||||
|
||||
return {
|
||||
ShowPaymentLineWidget: ShowPaymentLineWidget
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -1556,10 +1556,11 @@
|
||||
<field name="name">account.move.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Account Entry">
|
||||
<form string="Account Entry" duplicate="false">
|
||||
<header>
|
||||
<button name="action_post" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/>
|
||||
<button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/>
|
||||
<button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
||||
@@ -15,13 +15,14 @@ class ResPartner(models.Model):
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
U.partner_id as id,
|
||||
CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
|
||||
CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
|
||||
ELSE 'online'
|
||||
END as status
|
||||
FROM bus_presence B
|
||||
JOIN res_users U ON B.user_id = U.id
|
||||
WHERE U.partner_id IN %s AND U.active = 't'
|
||||
GROUP BY U.partner_id
|
||||
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
|
||||
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
|
||||
for partner in self:
|
||||
|
||||
@@ -70,7 +70,7 @@ class DeliveryCarrier(models.Model):
|
||||
def install_more_provider(self):
|
||||
return {
|
||||
'name': 'New Providers',
|
||||
'view_mode': 'kanban',
|
||||
'view_mode': 'kanban,form',
|
||||
'res_model': 'ir.module.module',
|
||||
'domain': [['name', 'ilike', 'delivery_']],
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -59,6 +59,7 @@ class FleetVehicle(models.Model):
|
||||
fuel_type = fields.Selection([
|
||||
('gasoline', 'Gasoline'),
|
||||
('diesel', 'Diesel'),
|
||||
('lpg', 'LPG'),
|
||||
('electric', 'Electric'),
|
||||
('hybrid', 'Hybrid')
|
||||
], 'Fuel Type', help='Fuel Used by the vehicle')
|
||||
|
||||
@@ -45,15 +45,21 @@ class FleetVehicle(models.Model):
|
||||
elif contract.cost_frequency == "yearly":
|
||||
car.total_cost += contract.cost_generated / 12.0
|
||||
|
||||
def _get_co2_fee(self, co2):
|
||||
return max((((co2 * 9.0) - 600.0) * 1.2488) / 12.0, 0.0)
|
||||
def _get_co2_fee(self, co2, fuel_type):
|
||||
fuel_coefficient = {'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600}
|
||||
co2_fee = 0
|
||||
if fuel_type and fuel_type != 'electric':
|
||||
if not co2:
|
||||
co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182
|
||||
co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0
|
||||
return max(co2_fee, 26.47)
|
||||
|
||||
@api.depends('co2')
|
||||
@api.depends('co2', 'fuel_type')
|
||||
def _compute_co2_fee(self):
|
||||
for car in self:
|
||||
car.co2_fee = self._get_co2_fee(car.co2)
|
||||
car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)
|
||||
|
||||
@api.depends('fuel_type', 'car_value', 'acquisition_date')
|
||||
@api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
|
||||
def _compute_car_atn(self):
|
||||
for car in self:
|
||||
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
|
||||
@@ -111,15 +117,18 @@ class FleetVehicle(models.Model):
|
||||
atn = 0.0
|
||||
else:
|
||||
if fuel_type in ['diesel', 'hybrid']:
|
||||
reference = 87.0
|
||||
reference = 88.0
|
||||
else:
|
||||
reference = 105.0
|
||||
reference = 107.0
|
||||
|
||||
if not co2:
|
||||
co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205
|
||||
|
||||
if co2 <= reference:
|
||||
atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff
|
||||
else:
|
||||
atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff
|
||||
return max(1280, atn) / 12.0
|
||||
return max(1310, atn) / 12.0
|
||||
|
||||
@api.onchange('model_id')
|
||||
def _onchange_model_id(self):
|
||||
@@ -140,7 +149,7 @@ class FleetVehicleModel(models.Model):
|
||||
default_recurring_cost_amount_depreciated = fields.Float(string="Cost (Depreciated)",
|
||||
help="Default recurring cost amount that should be applied to a new car from this model")
|
||||
default_co2 = fields.Float(string="CO2 emissions")
|
||||
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
|
||||
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('lpg', 'LPG'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
|
||||
default_car_value = fields.Float(string="Catalog Value (VAT Incl.)")
|
||||
can_be_requested = fields.Boolean(string="Can be requested", help="Can be requested on a contract as a new car")
|
||||
default_atn = fields.Float(compute='_compute_atn', string="ATN")
|
||||
@@ -158,7 +167,7 @@ class FleetVehicleModel(models.Model):
|
||||
for model in self:
|
||||
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
|
||||
|
||||
@api.depends('default_co2')
|
||||
@api.depends('default_co2', 'default_fuel_type')
|
||||
def _compute_co2_fee(self):
|
||||
for model in self:
|
||||
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2)
|
||||
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2, model.default_fuel_type)
|
||||
|
||||
@@ -266,6 +266,7 @@ var Chatter = Widget.extend({
|
||||
recordName: this.recordName,
|
||||
defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(),
|
||||
defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(),
|
||||
attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [],
|
||||
});
|
||||
this._composer.on('input_focused', this, function () {
|
||||
this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []);
|
||||
|
||||
@@ -56,7 +56,7 @@ var BasicComposer = Widget.extend({
|
||||
// Attachments
|
||||
this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
|
||||
this.fileuploadID = _.uniqueId('o_chat_fileupload');
|
||||
this.set('attachment_ids', []);
|
||||
this.set('attachment_ids', options.attachmentIds || []);
|
||||
|
||||
// Mention
|
||||
this._mentionManager = new MentionManager(this);
|
||||
@@ -118,6 +118,7 @@ var BasicComposer = Widget.extend({
|
||||
});
|
||||
|
||||
// Attachments
|
||||
this._renderAttachments();
|
||||
$(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
|
||||
this.on('change:attachment_ids', this, this._renderAttachments);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings')
|
||||
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False)
|
||||
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value')
|
||||
|
||||
@api.multi
|
||||
@api.depends('rating_ids', 'rating_ids.rating')
|
||||
@@ -17,3 +17,7 @@ class MailMessage(models.Model):
|
||||
mapping = dict((r.message_id.id, r.rating) for r in ratings)
|
||||
for message in self:
|
||||
message.rating_value = mapping.get(message.id, 0.0)
|
||||
|
||||
@api.multi
|
||||
def _search_rating_value(self, operator, operand):
|
||||
return [('rating_ids.rating', operator, operand)]
|
||||
|
||||
@@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
result = super(AccountAnalyticLine, self).create(values)
|
||||
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line
|
||||
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line
|
||||
result.sudo()._sale_determine_order_line()
|
||||
return result
|
||||
|
||||
@@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model):
|
||||
result = super(AccountAnalyticLine, self).write(values)
|
||||
if 'so_line' not in values: # allow to force a False value for so_line
|
||||
# only take the AAL from expense or vendor bill, meaning having a negative amount
|
||||
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line()
|
||||
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line()
|
||||
return result
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Vendor Bill / Expense : determine the Sale Order to reinvoice
|
||||
|
||||
+22
-13
@@ -47,8 +47,8 @@ class SaleOrder(models.Model):
|
||||
amount_untaxed += line.price_subtotal
|
||||
amount_tax += line.price_tax
|
||||
order.update({
|
||||
'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
|
||||
'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
|
||||
'amount_untaxed': amount_untaxed,
|
||||
'amount_tax': amount_tax,
|
||||
'amount_total': amount_untaxed + amount_tax,
|
||||
})
|
||||
|
||||
@@ -66,22 +66,31 @@ class SaleOrder(models.Model):
|
||||
for possible refunds created directly from existing invoices. This is necessary since such a
|
||||
refund is not directly linked to the SO.
|
||||
"""
|
||||
# Ignore the status of the deposit product
|
||||
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
|
||||
line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)]
|
||||
for order in self:
|
||||
invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
|
||||
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
|
||||
# 'account.invoice.refund')
|
||||
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
|
||||
refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
|
||||
refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id), ('type', 'in', ('out_invoice', 'out_refund'))])
|
||||
invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')])
|
||||
# Search for refunds as well
|
||||
refund_ids = self.env['account.invoice'].browse()
|
||||
if invoice_ids:
|
||||
for inv in invoice_ids:
|
||||
refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)])
|
||||
|
||||
# Ignore the status of the deposit product
|
||||
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
|
||||
line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
|
||||
# Search for refunds as well
|
||||
domain_inv = expression.OR([
|
||||
['&', ('origin', '=', inv.number), ('journal_id', '=', inv.journal_id.id)]
|
||||
for inv in invoice_ids if inv.number
|
||||
])
|
||||
if domain_inv:
|
||||
refund_ids = self.env['account.invoice'].search(expression.AND([
|
||||
['&', ('type', '=', 'out_refund'), ('origin', '!=', False)],
|
||||
domain_inv
|
||||
]))
|
||||
else:
|
||||
refund_ids = self.env['account.invoice'].browse()
|
||||
|
||||
line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id]
|
||||
|
||||
if order.state not in ('sale', 'done'):
|
||||
invoice_status = 'no'
|
||||
@@ -662,7 +671,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
# create an analytic account if at least an expense product
|
||||
for order in self:
|
||||
if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
|
||||
if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
|
||||
if not order.analytic_account_id:
|
||||
order._create_analytic_account()
|
||||
|
||||
@@ -1026,7 +1035,7 @@ class SaleOrderLine(models.Model):
|
||||
msg = _("Extra line with %s ") % (line.product_id.display_name,)
|
||||
line.order_id.message_post(body=msg)
|
||||
# create an analytic account if at least an expense product
|
||||
if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id:
|
||||
if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id:
|
||||
self.order_id._create_analytic_account()
|
||||
return line
|
||||
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
<field name="qty_delivered_manual" invisible="1"/>
|
||||
<field name="qty_delivered_method" invisible="1"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
<field name="price_subtotal" invisible="1"/>
|
||||
<label for="product_uom_qty" string="Ordered Quantity"/>
|
||||
<div>
|
||||
@@ -322,6 +323,7 @@
|
||||
name="product_uom_qty" class="oe_inline"/>
|
||||
<field
|
||||
name="product_uom"
|
||||
force_save="1"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline oe_no_button"
|
||||
attrs="{
|
||||
@@ -442,6 +444,7 @@
|
||||
<field name="qty_to_invoice" invisible="1"/>
|
||||
<field
|
||||
name="product_uom"
|
||||
force_save="1"
|
||||
attrs="{
|
||||
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
|
||||
'required': [('display_type', '=', False)],
|
||||
@@ -473,6 +476,7 @@
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
<field name="customer_lead" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
@@ -480,6 +484,7 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
<field name="price_subtotal"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="display_type"/>
|
||||
|
||||
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.multi
|
||||
def _sale_determine_order(self):
|
||||
mapping = super(AccountAnalyticLine, self)._sale_determine_order()
|
||||
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'):
|
||||
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']):
|
||||
if analytic_line.move_id.expense_id.sale_order_id:
|
||||
mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id
|
||||
return mapping
|
||||
|
||||
@@ -387,7 +387,7 @@ class StockMove(models.Model):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
qty_done = 0.0
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
|
||||
@@ -396,7 +396,6 @@ class StockMove(models.Model):
|
||||
else:
|
||||
# Get the standard price
|
||||
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
qty = forced_qty or qty_done
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
|
||||
|
||||
|
||||
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
|
||||
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
|
||||
self.assertAlmostEqual(self.product1.stock_value, 0.0)
|
||||
|
||||
def test_average_perpetual_6(self):
|
||||
self.product1.product_tmpl_id.cost_method = 'average'
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'Receive 1 unit at 10',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 10,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move1.move_line_ids.qty_done = 1.0
|
||||
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'Receive 1 units at 5',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 5,
|
||||
})
|
||||
move2._action_confirm()
|
||||
move2._action_assign()
|
||||
move2.move_line_ids.qty_done = 1.0
|
||||
|
||||
# Receive both at the same time
|
||||
(move1 | move2)._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 7.5)
|
||||
|
||||
def test_average_negative_1(self):
|
||||
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
|
||||
"""
|
||||
|
||||
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
|
||||
return False
|
||||
accounts = self.product_id.product_tmpl_id.get_product_accounts()
|
||||
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
|
||||
# If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
|
||||
if self.move_id._is_dropshipped():
|
||||
debit_account_id = accounts.get('expense') and accounts['expense'].id or False
|
||||
already_out_account_id = accounts['stock_output'].id
|
||||
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
|
||||
|
||||
|
||||
@@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({
|
||||
*/
|
||||
_bindAutoComplete: function () {
|
||||
var self = this;
|
||||
// avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
|
||||
if (this.$input.attr('placeholder')) {
|
||||
this.$input.attr('placeholder', function (index, val) {
|
||||
return val.split('').join('\ufeff');
|
||||
});
|
||||
}
|
||||
this.$input.autocomplete({
|
||||
source: function (req, resp) {
|
||||
_.each(self._autocompleteSources, function (source) {
|
||||
|
||||
@@ -228,17 +228,16 @@ var FormController = BasicController.extend({
|
||||
// are displayed with an alert
|
||||
var fields = self.renderer.state.fields;
|
||||
var data = self.renderer.state.data;
|
||||
var alertFields = [];
|
||||
var alertFields = {};
|
||||
for (var k = 0; k < changedFields.length; k++) {
|
||||
var field = fields[changedFields[k]];
|
||||
var fieldData = data[changedFields[k]];
|
||||
if (field.translate && fieldData) {
|
||||
alertFields.push(field);
|
||||
alertFields[changedFields[k]] = field;
|
||||
}
|
||||
}
|
||||
if (alertFields.length) {
|
||||
self.renderer.alertFields = alertFields;
|
||||
self.renderer.displayTranslationAlert();
|
||||
if (!_.isEmpty(alertFields)) {
|
||||
self.renderer.updateAlertFields(alertFields);
|
||||
}
|
||||
}
|
||||
return changedFields;
|
||||
|
||||
@@ -13,6 +13,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
className: "o_form_view",
|
||||
events: _.extend({}, BasicRenderer.prototype.events, {
|
||||
'click .o_notification_box .oe_field_translate': '_onTranslate',
|
||||
'click .o_notification_box .close': '_onTranslateNotificationClose',
|
||||
'click .oe_title, .o_inner_group': '_onClick',
|
||||
}),
|
||||
custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
|
||||
@@ -30,6 +31,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this.idsForLabels = {};
|
||||
this.lastActivatedFieldIndex = -1;
|
||||
this.alertFields = {};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -103,23 +105,32 @@ var FormRenderer = BasicRenderer.extend({
|
||||
});
|
||||
return fieldNames;
|
||||
},
|
||||
/*
|
||||
* Updates translation alert fields for the current state and display updated fields
|
||||
*
|
||||
* @param {Object} alertFields
|
||||
*/
|
||||
updateAlertFields: function (alertFields) {
|
||||
this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields);
|
||||
this.displayTranslationAlert();
|
||||
},
|
||||
/**
|
||||
* Show a warning message if the user modified a translated field. For each
|
||||
* field, the notification provides a link to edit the field's translations.
|
||||
*
|
||||
* @param {Object[]} alertFields field list
|
||||
*/
|
||||
displayTranslationAlert: function () {
|
||||
this.$('.o_notification_box').remove();
|
||||
var $notification = $(qweb.render('notification-box', {type: 'info'}))
|
||||
.append(qweb.render('translation-alert', {
|
||||
fields: this.alertFields,
|
||||
lang: _t.database.parameters.name
|
||||
}));
|
||||
if (this.$('.o_form_statusbar').length) {
|
||||
this.$('.o_form_statusbar').after($notification);
|
||||
} else {
|
||||
this.$el.prepend($notification);
|
||||
if (this.alertFields[this.state.res_id]) {
|
||||
var $notification = $(qweb.render('notification-box', {type: 'info'}))
|
||||
.append(qweb.render('translation-alert', {
|
||||
fields: this.alertFields[this.state.res_id],
|
||||
lang: _t.database.parameters.name
|
||||
}));
|
||||
if (this.$('.o_form_statusbar').length) {
|
||||
this.$('.o_form_statusbar').after($notification);
|
||||
} else {
|
||||
this.$el.prepend($notification);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -905,6 +916,9 @@ var FormRenderer = BasicRenderer.extend({
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self._updateView($form.contents());
|
||||
if (self.state.res_id in self.alertFields) {
|
||||
self.displayTranslationAlert();
|
||||
}
|
||||
}, function () {
|
||||
$form.remove();
|
||||
}).then(function(){
|
||||
@@ -1015,6 +1029,15 @@ var FormRenderer = BasicRenderer.extend({
|
||||
event.preventDefault();
|
||||
this.trigger_up('translate', {fieldName: event.target.name, id: this.state.id});
|
||||
},
|
||||
/**
|
||||
* remove alert fields of record from alertFields object
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onTranslateNotificationClose: function(ev) {
|
||||
delete this.alertFields[this.state.res_id];
|
||||
},
|
||||
});
|
||||
|
||||
return FormRenderer;
|
||||
|
||||
@@ -1525,7 +1525,7 @@
|
||||
<div role="alertdialog">
|
||||
Please update translations of :
|
||||
<t t-foreach="fields" t-as="field">
|
||||
<strong><a class="oe_field_translate" t-att-name="field.name" href="#"><t t-esc="field.string"/><t t-if="!field_last">, </t></a></strong>
|
||||
<strong><a class="oe_field_translate" t-att-name="field" href="#"><t t-esc="field_value.string"/><t t-if="field_index < _.size(fields)-1">, </t></a></strong>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -16,6 +16,7 @@ var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var createView = testUtils.createView;
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
var createAsyncView = testUtils.createAsyncView;
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
|
||||
@@ -5647,9 +5648,10 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('display translation alert', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.fields.foo.translate = true;
|
||||
this.data.partner.fields.display_name.translate = true;
|
||||
|
||||
var multi_lang = _t.database.multi_lang;
|
||||
_t.database.multi_lang = true;
|
||||
@@ -5662,6 +5664,7 @@ QUnit.module('Views', {
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
@@ -5671,14 +5674,154 @@ QUnit.module('Views', {
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('input[name="foo"]').val("test").trigger("input");
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 1,
|
||||
"should have single translation alert");
|
||||
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert");
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('input[name="display_name"]').val("test2").trigger("input");
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 2,
|
||||
"should have two translate fields in translation alert");
|
||||
|
||||
form.destroy();
|
||||
|
||||
_t.database.multi_lang = multi_lang;
|
||||
});
|
||||
|
||||
QUnit.test('translation alerts are preserved on pager change', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.fields.foo.translate = true;
|
||||
|
||||
var multi_lang = _t.database.multi_lang;
|
||||
_t.database.multi_lang = true;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
viewOptions: {
|
||||
ids: [1, 2],
|
||||
index: 0,
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('input[name="foo"]').val("test").trigger("input");
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
|
||||
"should have a translation alert");
|
||||
|
||||
// click on the pager to switch to the next record
|
||||
form.pager.$('.o_pager_next').click();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
|
||||
"should not have a translation alert");
|
||||
|
||||
// click on the pager to switch back to the previous record
|
||||
form.pager.$('.o_pager_previous').click();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
|
||||
"should have a translation alert");
|
||||
|
||||
// remove translation alert by click X and check alert even after form reload
|
||||
form.$('.o_form_view > .alert > .close').click();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
|
||||
"should not have a translation alert");
|
||||
form.reload();
|
||||
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
|
||||
"should not have a translation alert after reload");
|
||||
|
||||
form.destroy();
|
||||
_t.database.multi_lang = multi_lang;
|
||||
});
|
||||
|
||||
QUnit.test('translation alerts preseved on reverse breadcrumb', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data['ir.translation'] = {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
source: {string: "Source", type: "char"},
|
||||
value: {string: "Value", type: "char"},
|
||||
},
|
||||
records: [],
|
||||
};
|
||||
|
||||
this.data.partner.fields.foo.translate = true;
|
||||
|
||||
var multi_lang = _t.database.multi_lang;
|
||||
_t.database.multi_lang = true;
|
||||
|
||||
var archs = {
|
||||
'partner,false,form': '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
'partner,false,search': '<search></search>',
|
||||
'ir.translation,false,list': '<tree>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="source"/>' +
|
||||
'<field name="value"/>' +
|
||||
'</tree>',
|
||||
'ir.translation,false,search': '<search></search>',
|
||||
};
|
||||
|
||||
var actions = [{
|
||||
id: 1,
|
||||
name: 'Partner',
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'form']],
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Translate',
|
||||
res_model: 'ir.translation',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
target: 'current',
|
||||
flags: {'search_view': true, 'action_buttons': true},
|
||||
}];
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: actions,
|
||||
archs: archs,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
actionManager.doAction(1);
|
||||
|
||||
actionManager.controlPanel.$el.find('.o_form_button_edit').click();
|
||||
actionManager.$('input[name="foo"]').val("test").trigger("input");
|
||||
actionManager.controlPanel.$el.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
|
||||
"should have a translation alert");
|
||||
|
||||
var currentController = actionManager.getCurrentController().widget;
|
||||
actionManager.doAction(2, {
|
||||
on_reverse_breadcrumb: function () {
|
||||
if (!_.isEmpty(currentController.renderer.alertFields)) {
|
||||
currentController.renderer.displayTranslationAlert();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
$('.o_control_panel .breadcrumb a:first').click();
|
||||
assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
|
||||
"should have a translation alert");
|
||||
|
||||
actionManager.destroy();
|
||||
_t.database.multi_lang = multi_lang;
|
||||
});
|
||||
|
||||
QUnit.test('translate event correctly handled with multiple controllers', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ class Web_Editor(http.Controller):
|
||||
|
||||
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
|
||||
"""
|
||||
return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url)))
|
||||
return bool(etree.XML(view.arch).xpath("//link[@href='{}']".format(url)))
|
||||
|
||||
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
|
||||
|
||||
|
||||
@@ -9,9 +9,12 @@ declaration.
|
||||
Signed,
|
||||
|
||||
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
|
||||
Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
|
||||
|
||||
|
||||
List of contributors:
|
||||
|
||||
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
|
||||
Roberto Fichera robyf@tekno-soft.it https://github.com/robyf70
|
||||
Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
|
||||
Filippo Iovine - filippo.jovine@gmail.com - https://github.com/FilippoIOVINE
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
china, 2019-01-16
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
liweijie 674416404@qq.com https://github.com/liweijie0812
|
||||
@@ -0,0 +1,11 @@
|
||||
Yemen, 2019-01-22
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Waleed Mohsen Mohsen.Waleed@gmail.com https://github.com/MohsenWaleed
|
||||
@@ -0,0 +1,11 @@
|
||||
Mexico, 18-01-2018
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
José Francisco Martínez Salgado admin@jofranmtz.com https://github.com/xyvs
|
||||
@@ -180,8 +180,6 @@ todo_include_todos = False
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
|
||||
'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
|
||||
'django': ('https://django.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
github_user = 'odoo'
|
||||
|
||||
@@ -403,6 +403,11 @@ class IrModelFields(models.Model):
|
||||
raise UserError(_("The Selection Options expression is not a valid Pythonic expression. "
|
||||
"Please provide an expression in the [('key','Label'), ...] format."))
|
||||
|
||||
@api.constrains('domain')
|
||||
def _check_domain(self):
|
||||
for field in self:
|
||||
safe_eval(field.domain or '[]')
|
||||
|
||||
@api.constrains('name', 'state')
|
||||
def _check_name(self):
|
||||
for field in self:
|
||||
|
||||
@@ -394,7 +394,10 @@ actual arch.
|
||||
|
||||
def _compute_defaults(self, values):
|
||||
if 'inherit_id' in values:
|
||||
values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
|
||||
# Do not automatically change the mode if the view already has an inherit_id,
|
||||
# and the user change it to another.
|
||||
if not values['inherit_id'] or all(not view.inherit_id for view in self):
|
||||
values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
|
||||
return values
|
||||
|
||||
@api.model_create_multi
|
||||
|
||||
@@ -1573,6 +1573,12 @@ class ViewModeField(ViewCase):
|
||||
})
|
||||
self.assertEqual(view2.mode, 'extension')
|
||||
|
||||
view2.write({'inherit_id': None})
|
||||
self.assertEqual(view2.mode, 'primary')
|
||||
|
||||
view2.write({'inherit_id': view.id})
|
||||
self.assertEqual(view2.mode, 'extension')
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def testModeExplicit(self):
|
||||
view = self.View.create({
|
||||
@@ -1585,6 +1591,7 @@ class ViewModeField(ViewCase):
|
||||
'arch': '<qweb/>'
|
||||
})
|
||||
self.assertEqual(view.mode, 'primary')
|
||||
self.assertEqual(view2.mode, 'primary')
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.View.create({
|
||||
@@ -1636,6 +1643,27 @@ class ViewModeField(ViewCase):
|
||||
|
||||
view.write({'mode': 'primary'})
|
||||
|
||||
def testChangeInheritOfPrimary(self):
|
||||
"""
|
||||
A primary view with an inherit_id must remain primary when changing the inherit_id
|
||||
"""
|
||||
base1 = self.View.create({
|
||||
'inherit_id': None,
|
||||
'arch': '<qweb/>',
|
||||
})
|
||||
base2 = self.View.create({
|
||||
'inherit_id': None,
|
||||
'arch': '<qweb/>',
|
||||
})
|
||||
view = self.View.create({
|
||||
'mode': 'primary',
|
||||
'inherit_id': base1.id,
|
||||
'arch': '<qweb/>',
|
||||
})
|
||||
self.assertEqual(view.mode, 'primary')
|
||||
view.write({'inherit_id': base2.id})
|
||||
self.assertEqual(view.mode, 'primary')
|
||||
|
||||
|
||||
class TestDefaultView(ViewCase):
|
||||
def testDefaultViewBase(self):
|
||||
|
||||
Reference in New Issue
Block a user