[MERGE] forward port branch 11.0 up to 02ee3fd88e
This commit is contained in:
@@ -428,12 +428,17 @@ class account_payment(models.Model):
|
||||
|
||||
@api.multi
|
||||
def button_invoices(self):
|
||||
if self.partner_type == 'supplier':
|
||||
views = [(self.env.ref('account.invoice_supplier_tree').id, 'tree'), (self.env.ref('account.invoice_supplier_form').id, 'form')]
|
||||
else:
|
||||
views = [(self.env.ref('account.invoice_tree').id, 'tree'), (self.env.ref('account.invoice_form').id, 'form')]
|
||||
return {
|
||||
'name': _('Paid Invoices'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,form',
|
||||
'res_model': 'account.invoice',
|
||||
'view_id': False,
|
||||
'views': views,
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('id', 'in', [x.id for x in self.invoice_ids])],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -61,9 +62,9 @@ class AccountAnalyticAccount(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _compute_debit_credit_balance(self):
|
||||
res_currency_obj = self.env['res.currency']
|
||||
Curr = self.env['res.currency']
|
||||
analytic_line_obj = self.env['account.analytic.line']
|
||||
domain = [('account_id', 'in', self.mapped('id'))]
|
||||
domain = [('account_id', 'in', self.ids)]
|
||||
if self._context.get('from_date', False):
|
||||
domain.append(('date', '>=', self._context['from_date']))
|
||||
if self._context.get('to_date', False):
|
||||
@@ -75,17 +76,23 @@ class AccountAnalyticAccount(models.Model):
|
||||
domain.append(('company_id', 'in', self._context['company_ids']))
|
||||
|
||||
user_currency = self.env.user.company_id.currency_id
|
||||
account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount', 'currency_id'])
|
||||
account_ids = set([line['account_id'][0] for line in account_amounts])
|
||||
data_debit = {account_id: 0.0 for account_id in account_ids}
|
||||
data_credit = {account_id: 0.0 for account_id in account_ids}
|
||||
for account_amount in account_amounts:
|
||||
currency_id = account_amount['currency_id'][0]
|
||||
amount = res_currency_obj.browse(currency_id).compute(account_amount['amount'], user_currency)
|
||||
if amount < 0.0:
|
||||
data_debit[account_amount['account_id'][0]] += amount
|
||||
else:
|
||||
data_credit[account_amount['account_id'][0]] += amount
|
||||
credit_groups = analytic_line_obj.read_group(
|
||||
domain=domain + [('amount', '>=', 0.0)],
|
||||
fields=['account_id', 'currency_id', 'amount'],
|
||||
groupby=['account_id', 'currency_id'],
|
||||
)
|
||||
data_credit = defaultdict(float)
|
||||
for l in credit_groups:
|
||||
data_credit[l['account_id'][0]] += Curr.browse(l['currency_id'][0]).compute(l['amount'], user_currency)
|
||||
|
||||
debit_groups = analytic_line_obj.read_group(
|
||||
domain=domain + [('amount', '<', 0.0)],
|
||||
fields=['account_id', 'currency_id', 'amount'],
|
||||
groupby=['account_id', 'currency_id'],
|
||||
)
|
||||
data_debit = defaultdict(float)
|
||||
for l in debit_groups:
|
||||
data_debit[l['account_id'][0]] += Curr.browse(l['currency_id'][0]).compute(l['amount'], user_currency)
|
||||
|
||||
for account in self:
|
||||
account.debit = abs(data_debit.get(account.id, 0.0))
|
||||
|
||||
@@ -655,7 +655,13 @@ class Meeting(models.Model):
|
||||
}[data['rrule_type']]
|
||||
|
||||
deadline = fields.Datetime.from_string(data['stop'])
|
||||
return deadline + relativedelta(**{delay: count * mult})
|
||||
computed_final_date = False
|
||||
while not computed_final_date and count > 0:
|
||||
try: # may crash if year > 9999 (in case of recurring events)
|
||||
computed_final_date = deadline + relativedelta(**{delay: count * mult})
|
||||
except ValueError:
|
||||
count -= data['interval']
|
||||
return computed_final_date or deadline
|
||||
return final_date
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (C) David Arnold (devCO).
|
||||
# Author David Arnold (devCO), dar@devco.co
|
||||
# Copyright (C) David Arnold (XOE Solutions).
|
||||
# Author David Arnold (XOE Solutions), dar@xoe.solutions
|
||||
# Co-Authors Juan Pablo Aries (devCO), jpa@devco.co
|
||||
# Hector Ivan Valencia Muñoz (TIX SAS)
|
||||
# Nhomar Hernandez (Vauxoo)
|
||||
@@ -13,7 +13,8 @@
|
||||
'version': '0.8',
|
||||
'category': 'Localization',
|
||||
'description': 'Colombian Accounting and Tax Preconfiguration',
|
||||
'author': 'David Arnold BA HSG (devCO)',
|
||||
'author': 'David Arnold (XOE Solutions)',
|
||||
'website': 'https://xoe.solutions',
|
||||
'depends': [
|
||||
'account',
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# List of contributors:
|
||||
# Jordi Esteve <jesteve@zikzakmedia.com>
|
||||
# Ignacio Ibeas <ignacio@acysos.com>
|
||||
# Dpto. Consultoría Grupo Opentia <consultoria@opentia.es>
|
||||
# Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
# Carlos Liébana <carlos.liebana@factorlibre.com>
|
||||
|
||||
@@ -113,6 +113,16 @@
|
||||
<field name="chart_template_id" ref="account_chart_template_common"/>
|
||||
</record>
|
||||
|
||||
<record id="fp_reagyp_a" model="account.fiscal.position.template">
|
||||
<field name="name">REAGYP - Agricultura</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
</record>
|
||||
|
||||
<record id="fp_reagyp_gp" model="account.fiscal.position.template">
|
||||
<field name="name">REAGYP - Ganadería y pesca</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
</record>
|
||||
|
||||
<!-- ************************************************************* -->
|
||||
<!-- Fiscal Position Tax Templates -->
|
||||
<!-- ************************************************************* -->
|
||||
@@ -2889,6 +2899,28 @@
|
||||
<field name="tax_dest_id" ref="account_tax_template_s_iva0_isp"/>
|
||||
</record>
|
||||
|
||||
<!-- Régimen especial de Agricultura, Ganaderia y Pesca -->
|
||||
<record id="fptt_reagyp_a_4b_1" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fp_reagyp_a"/>
|
||||
<field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
|
||||
<field name="tax_dest_id" ref="l10n_es.account_tax_template_p_iva12_agr"/>
|
||||
</record>
|
||||
<record id="fptt_reagyp_a_4b_2" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fp_reagyp_a"/>
|
||||
<field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
|
||||
<field name="tax_dest_id" ref="l10n_es.account_tax_template_p_irpf2"/>
|
||||
</record>
|
||||
<record id="fptt_reagyp_gp_4b_1" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fp_reagyp_gp"/>
|
||||
<field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
|
||||
<field name="tax_dest_id" ref="account_tax_template_p_iva105_gan"/>
|
||||
</record>
|
||||
<record id="fptt_reagyp_gp_4b_2" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fp_reagyp_gp"/>
|
||||
<field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
|
||||
<field name="tax_dest_id" ref="l10n_es.account_tax_template_p_irpf2"/>
|
||||
</record>
|
||||
|
||||
<!-- ************************************************************* -->
|
||||
<!-- Fiscal Position Account Templates -->
|
||||
<!-- ************************************************************* -->
|
||||
|
||||
@@ -1044,6 +1044,18 @@
|
||||
<field name="tax_group_id" ref="tax_group_iva_12"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_42')])]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_p_iva105_gan" model="account.tax.template">
|
||||
<field name="description">P_IVA105_GAN</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="account_id" ref="l10n_es.account_common_472"/>
|
||||
<field name="name">10,5% IVA Soportado régimen ganadero o pesca</field>
|
||||
<field name="refund_account_id" ref="l10n_es.account_common_472"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount" eval="10.5"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="include_base_amount" eval="1"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_42')])]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_s_iva0_e" model="account.tax.template">
|
||||
<field name="description">S_IVA0_E</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
@@ -1330,6 +1342,7 @@
|
||||
<field name="refund_account_id" ref="l10n_es.account_common_4751"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount" eval="-2"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_group_id" ref="tax_group_retenciones_2"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_111_08'), ref('mod_111_09')])]"/>
|
||||
|
||||
@@ -124,7 +124,8 @@ class MailThread(models.AbstractModel):
|
||||
followers = self.env['mail.followers'].sudo().search([
|
||||
('res_model', '=', self._name),
|
||||
('partner_id', operator, operand)])
|
||||
return [('id', 'in', followers.mapped('res_id'))]
|
||||
# using read() below is much faster than followers.mapped('res_id')
|
||||
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||
|
||||
@api.model
|
||||
def _search_follower_channels(self, operator, operand):
|
||||
@@ -137,7 +138,8 @@ class MailThread(models.AbstractModel):
|
||||
followers = self.env['mail.followers'].sudo().search([
|
||||
('res_model', '=', self._name),
|
||||
('channel_id', operator, operand)])
|
||||
return [('id', 'in', followers.mapped('res_id'))]
|
||||
# using read() below is much faster than followers.mapped('res_id')
|
||||
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||
|
||||
@api.multi
|
||||
@api.depends('message_follower_ids')
|
||||
@@ -147,7 +149,8 @@ class MailThread(models.AbstractModel):
|
||||
('res_id', 'in', self.ids),
|
||||
('partner_id', '=', self.env.user.partner_id.id),
|
||||
])
|
||||
following_ids = followers.mapped('res_id')
|
||||
# using read() below is much faster than followers.mapped('res_id')
|
||||
following_ids = [res['res_id'] for res in followers.read(['res_id'])]
|
||||
for record in self:
|
||||
record.message_is_follower = record.id in following_ids
|
||||
|
||||
@@ -159,9 +162,11 @@ class MailThread(models.AbstractModel):
|
||||
])
|
||||
# Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False)
|
||||
if (operator == '=' and operand) or (operator == '!=' and not operand):
|
||||
return [('id', 'in', followers.mapped('res_id'))]
|
||||
# using read() below is much faster than followers.mapped('res_id')
|
||||
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||
else:
|
||||
return [('id', 'not in', followers.mapped('res_id'))]
|
||||
# using read() below is much faster than followers.mapped('res_id')
|
||||
return [('id', 'not in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||
|
||||
@api.multi
|
||||
def _get_message_unread(self):
|
||||
|
||||
@@ -32,6 +32,7 @@ function _readActivities(self, ids) {
|
||||
model: 'mail.activity',
|
||||
method: 'read',
|
||||
args: [ids],
|
||||
context: (self.record && self.record.getContext()) || self.getSession().user_context,
|
||||
}).then(function (activities) {
|
||||
// convert create_date and date_deadline to moments
|
||||
_.each(activities, function (activity) {
|
||||
@@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
|
||||
method: 'action_feedback',
|
||||
args: [[id]],
|
||||
kwargs: {feedback: feedback},
|
||||
context: this.record.getContext(),
|
||||
});
|
||||
},
|
||||
_scheduleActivity: function (id, previous_activity_type_id, callback) {
|
||||
|
||||
@@ -474,10 +474,16 @@ class MrpProduction(models.Model):
|
||||
@api.multi
|
||||
def _generate_workorders(self, exploded_boms):
|
||||
workorders = self.env['mrp.workorder']
|
||||
original_one = False
|
||||
for bom, bom_data in exploded_boms:
|
||||
# If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
|
||||
if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
|
||||
workorders += self._workorders_create(bom, bom_data)
|
||||
temp_workorders = self._workorders_create(bom, bom_data)
|
||||
workorders += temp_workorders
|
||||
if temp_workorders: # In order to avoid two "ending work orders"
|
||||
if original_one:
|
||||
temp_workorders[-1].next_work_order_id = original_one
|
||||
original_one = temp_workorders[0]
|
||||
return workorders
|
||||
|
||||
def _workorders_create(self, bom, bom_data):
|
||||
|
||||
@@ -225,6 +225,7 @@ class MrpWorkorder(models.Model):
|
||||
'done_wo': False,
|
||||
'location_id': move.location_id.id,
|
||||
'location_dest_id': move.location_dest_id.id,
|
||||
'date': move.date,
|
||||
})
|
||||
qty_todo -= 1
|
||||
elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
|
||||
@@ -416,7 +417,12 @@ class MrpWorkorder(models.Model):
|
||||
|
||||
@api.multi
|
||||
def button_start(self):
|
||||
# TDE CLEANME
|
||||
self.ensure_one()
|
||||
# As button_start is automatically called in the new view
|
||||
if self.state in ('done', 'cancel'):
|
||||
return True
|
||||
|
||||
# Need a loss in case of the real time exceeding the expected
|
||||
timeline = self.env['mrp.workcenter.productivity']
|
||||
if self.duration < self.duration_expected:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
|
||||
|
||||
@@ -449,6 +449,7 @@ PaymentScreenWidget.include({
|
||||
order.selected_paymentline.paid = true;
|
||||
order.selected_paymentline.mercury_swipe_pending = false;
|
||||
order.selected_paymentline.mercury_amount = response.authorize;
|
||||
order.selected_paymentline.set_amount(response.authorize);
|
||||
order.selected_paymentline.mercury_card_number = decodedMagtek['number'];
|
||||
order.selected_paymentline.mercury_card_brand = response.card_type;
|
||||
order.selected_paymentline.mercury_card_owner_name = decodedMagtek['name'];
|
||||
|
||||
@@ -347,6 +347,7 @@ class PurchaseOrder(models.Model):
|
||||
|
||||
@api.multi
|
||||
def print_quotation(self):
|
||||
self.write({'state': "sent"})
|
||||
return self.env.ref('purchase.report_purchase_quotation').report_action(self)
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -142,11 +142,6 @@
|
||||
<field name="number_increment">1</field>
|
||||
</record>
|
||||
|
||||
<record id="base.main_partner" model="res.partner">
|
||||
<field name="property_stock_customer" model="stock.warehouse"
|
||||
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
|
||||
</record>
|
||||
|
||||
<!-- create xml ids for demo data that are widely used in tests or in other codes, for more convenience -->
|
||||
<function model="ir.model.data" name="_update">
|
||||
<value>stock.location</value>
|
||||
|
||||
@@ -28,12 +28,18 @@ class Company(models.Model):
|
||||
location.sudo().write({'company_id': company.id})
|
||||
company.write({'internal_transit_location_id': location.id})
|
||||
|
||||
warehouses = self.env['stock.warehouse'].search([('partner_id', '=', self.partner_id.id)])
|
||||
warehouses.mapped('partner_id').with_context(force_company=self.id).write({
|
||||
'property_stock_customer': location.id,
|
||||
'property_stock_supplier': location.id,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(Company, self).create(vals)
|
||||
|
||||
# multi-company rules prevents creating warehouse and sub-locations
|
||||
company.create_transit_location()
|
||||
# mutli-company rules prevents creating warehouse and sub-locations
|
||||
self.env['stock.warehouse'].check_access_rights('create')
|
||||
self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id, 'partner_id': company.partner_id.id})
|
||||
company.create_transit_location()
|
||||
return company
|
||||
|
||||
@@ -79,12 +79,6 @@ class StockQuant(models.Model):
|
||||
if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
|
||||
raise ValidationError(_('A serial number should only be linked to a single product.'))
|
||||
|
||||
@api.constrains('in_date', 'lot_id')
|
||||
def check_in_date(self):
|
||||
for quant in self:
|
||||
if quant.in_date and not quant.lot_id:
|
||||
raise ValidationError(_('An incoming date cannot be set to an untracked product.'))
|
||||
|
||||
@api.constrains('location_id')
|
||||
def check_location_id(self):
|
||||
for quant in self:
|
||||
@@ -109,9 +103,9 @@ class StockQuant(models.Model):
|
||||
@api.model
|
||||
def _get_removal_strategy_order(self, removal_strategy):
|
||||
if removal_strategy == 'fifo':
|
||||
return 'in_date, id'
|
||||
return 'in_date ASC NULLS FIRST, id'
|
||||
elif removal_strategy == 'lifo':
|
||||
return 'in_date desc, id desc'
|
||||
return 'in_date DESC NULLS LAST, id desc'
|
||||
raise UserError(_('Removal strategy %s not implemented.') % (removal_strategy,))
|
||||
|
||||
def _gather(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
|
||||
@@ -134,7 +128,17 @@ class StockQuant(models.Model):
|
||||
domain = expression.AND([[('owner_id', '=', owner_id and owner_id.id or False)], domain])
|
||||
domain = expression.AND([[('location_id', '=', location_id.id)], domain])
|
||||
|
||||
return self.search(domain, order=removal_strategy_order)
|
||||
# Copy code of _search for special NULLS FIRST/LAST order
|
||||
self.sudo(self._uid).check_access_rights('read')
|
||||
query = self._where_calc(domain)
|
||||
self._apply_ir_rules(query, 'read')
|
||||
from_clause, where_clause, where_clause_params = query.get_sql()
|
||||
where_str = where_clause and (" WHERE %s" % where_clause) or ''
|
||||
query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY "+ removal_strategy_order
|
||||
self._cr.execute(query_str, where_clause_params)
|
||||
res = self._cr.fetchall()
|
||||
# No uniquify list necessary as auto_join is not applied anyways...
|
||||
return self.browse([x[0] for x in res])
|
||||
|
||||
@api.model
|
||||
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
|
||||
@@ -196,17 +200,16 @@ class StockQuant(models.Model):
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
rounding = product_id.uom_id.rounding
|
||||
|
||||
if lot_id:
|
||||
incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
incoming_dates = [d for d in quants.mapped('in_date') if d]
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
|
||||
for quant in quants:
|
||||
try:
|
||||
|
||||
@@ -21,24 +21,31 @@ class MrpStockReport(models.TransientModel):
|
||||
|
||||
@api.model
|
||||
def _get_move_lines(self, move_lines):
|
||||
res = self.env['stock.move.line']
|
||||
for move_line in move_lines:
|
||||
lines_seen = move_lines
|
||||
lines_todo = list(move_lines)
|
||||
while lines_todo:
|
||||
move_line = lines_todo.pop(0)
|
||||
# if MTO
|
||||
if move_line.move_id.move_orig_ids:
|
||||
res |= move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id.id == move_line.lot_id.id)
|
||||
lines = move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id == move_line.lot_id
|
||||
) - lines_seen
|
||||
# if MTS
|
||||
elif move_line:
|
||||
lines = self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('id', 'not in', lines_seen.ids),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
else:
|
||||
if move_line:
|
||||
res |= self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('id', '!=', move_line.id),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
if res:
|
||||
res |= self._get_move_lines(res)
|
||||
return res
|
||||
continue
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
|
||||
@api.model
|
||||
def get_lines(self, line_id=None, **kw):
|
||||
|
||||
@@ -178,9 +178,10 @@ class Warehouse(models.Model):
|
||||
ResCompany = self.env['res.company']
|
||||
if company_id:
|
||||
transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).with_context(force_company=company_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
else:
|
||||
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
|
||||
def create_sequences_and_picking_types(self):
|
||||
IrSequenceSudo = self.env['ir.sequence'].sudo()
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<span t-field="move.product_id.description_picking"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-if="move.product_qty" t-esc="move.product_qty"/>
|
||||
<span t-if="move.product_qty" t-field="move.product_qty"/>
|
||||
<span t-if="not move.product_qty" t-esc="move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')"/>
|
||||
<span t-field="move.product_id.uom_id" groups="product.group_uom"/>
|
||||
</td>
|
||||
|
||||
@@ -185,6 +185,73 @@ class TestPickShip(TestStockCommon):
|
||||
# the client picking should not be assigned anymore, as we returned partially what we took
|
||||
self.assertEqual(picking_client.state, 'confirmed')
|
||||
|
||||
def test_mto_moves_return_return(self):
|
||||
picking_pick, picking_client = self.create_pick_ship()
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.productA.id,
|
||||
'name': '123456789'
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||||
|
||||
picking_pick.action_assign()
|
||||
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
picking_pick.button_validate()
|
||||
self.assertEqual(picking_pick.state, 'done')
|
||||
self.assertEqual(picking_client.state, 'assigned')
|
||||
|
||||
# return this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_pick.button_validate()
|
||||
# return this return of this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_id=return_pick.id)\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_return_pick.button_validate()
|
||||
# test computation of traceability
|
||||
vals = {
|
||||
'line_id': 1,
|
||||
'model_name': 'stock.move.line',
|
||||
'level': 11,
|
||||
'parent_quant': False,
|
||||
}
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=return_return_pick.move_line_ids[0].id,
|
||||
stream='upstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[return_return_pick.id, return_pick.id, picking_pick.id],
|
||||
"Upstream computation from return of return worked"
|
||||
)
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=picking_pick.move_line_ids[0].id,
|
||||
stream='downstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[picking_pick.id, return_pick.id, return_return_pick.id],
|
||||
"Downstream computation from original picking worked"
|
||||
)
|
||||
|
||||
def test_mto_resupply_cancel_ship(self):
|
||||
""" This test simulates a pick pack ship with a resupply route
|
||||
set. Pick and pack are validated, ship is cancelled. This test
|
||||
|
||||
@@ -629,7 +629,24 @@ class StockQuant(TransactionCase):
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0)
|
||||
self.assertEqual(quantity, 1)
|
||||
self.assertEqual(in_date, None)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_1b(self):
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 2.0)
|
||||
self.assertEqual(quantity, 3)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_2(self):
|
||||
""" Check that an incoming date is correctly set when updating the quantity of a tracked
|
||||
@@ -707,6 +724,34 @@ class StockQuant(TransactionCase):
|
||||
# Removal strategy is LIFO, so lot1 should be received as it was received later.
|
||||
self.assertEqual(quants[0][0].lot_id.id, lot1.id)
|
||||
|
||||
def test_in_date_4b(self):
|
||||
""" Check for LIFO and max with/without in_date that it handles the LIFO NULLS LAST well
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
stock_location1 = self.env.ref('stock.stock_location_components')
|
||||
stock_location2 = self.env.ref('stock.stock_location_14')
|
||||
lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
|
||||
stock_location.removal_strategy_id = lifo_strategy
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location1.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
|
||||
in_date_location2 = datetime.now()
|
||||
self.env['stock.quant']._update_available_quantity(product1, stock_location2, 1.0, in_date=in_date_location2)
|
||||
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1)
|
||||
|
||||
# Removal strategy is LIFO, so the one with date is the most recent one and should be selected
|
||||
self.assertEqual(quants[0][0].location_id.id, stock_location2.id)
|
||||
|
||||
def test_in_date_5(self):
|
||||
""" Receive the same lot at different times, once they're in the same location, the quants
|
||||
are merged and only the earliest incoming date is kept.
|
||||
|
||||
@@ -1047,8 +1047,12 @@ class Binary(http.Controller):
|
||||
elif status != 200 and download:
|
||||
return request.not_found()
|
||||
|
||||
height = int(height or 0)
|
||||
width = int(width or 0)
|
||||
if headers and dict(headers).get('Content-Type', '') == 'image/svg+xml': # we shan't resize svg images
|
||||
height = 0
|
||||
width = 0
|
||||
else:
|
||||
height = int(height or 0)
|
||||
width = int(width or 0)
|
||||
|
||||
if crop and (width or height):
|
||||
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
|
||||
|
||||
@@ -260,12 +260,20 @@ function get_file(options) {
|
||||
if (options.error) {
|
||||
var body = this.contentDocument.body;
|
||||
var nodes = body.children.length === 0 ? body.childNodes : body.children;
|
||||
options.error({
|
||||
message: nodes.length > 1 ? nodes[1].textContent : '',
|
||||
data: {
|
||||
title: nodes.length > 0 ? nodes[0].textContent : '',
|
||||
},
|
||||
});
|
||||
var errorParams = {};
|
||||
|
||||
try { // Case of a serialized Odoo Exception: It is Json Parsable
|
||||
var node = nodes[1] || nodes[0];
|
||||
errorParams = JSON.parse(node.textContent);
|
||||
} catch (e) { // Arbitrary uncaught python side exception
|
||||
errorParams = {
|
||||
message: nodes.length > 1 ? nodes[1].textContent : '',
|
||||
data: {
|
||||
title: nodes.length > 0 ? nodes[0].textContent : '',
|
||||
}
|
||||
}
|
||||
}
|
||||
options.error(errorParams);
|
||||
}
|
||||
} finally {
|
||||
complete();
|
||||
|
||||
@@ -1542,8 +1542,10 @@ var BasicModel = AbstractModel.extend({
|
||||
rec = self._makeDataPoint(params);
|
||||
list._cache[rec.res_id] = rec.id;
|
||||
}
|
||||
|
||||
rec._noAbandon = true;
|
||||
// Do not abandon the record if it has been created
|
||||
// from `default_get`. The list has a savepoint only
|
||||
// after having fully executed `default_get`.
|
||||
rec._noAbandon = !list._savePoint;
|
||||
list._changes.push({operation: 'ADD', id: rec.id});
|
||||
if (command[0] === 1) {
|
||||
list._changes.push({operation: 'UPDATE', id: rec.id});
|
||||
@@ -1962,6 +1964,9 @@ var BasicModel = AbstractModel.extend({
|
||||
var records = [];
|
||||
var ids = [];
|
||||
list = this._applyX2ManyOperations(list);
|
||||
if (_.isEmpty(list.data)) {
|
||||
return $.when();
|
||||
}
|
||||
_.each(list.data, function (localId) {
|
||||
var record = self.localData[localId];
|
||||
var data = record._changes || record.data;
|
||||
@@ -1975,13 +1980,16 @@ var BasicModel = AbstractModel.extend({
|
||||
return this._rpc({
|
||||
model: model,
|
||||
method: 'name_get',
|
||||
args: [ids],
|
||||
args: [_.uniq(ids)],
|
||||
context: list.context,
|
||||
})
|
||||
.then(function (name_gets) {
|
||||
for (var i = 0; i < name_gets.length; i++) {
|
||||
records[i].data.display_name = name_gets[i][1];
|
||||
}
|
||||
_.each(records, function (record) {
|
||||
var nameGet = _.find(name_gets, function (nameGet) {
|
||||
return nameGet[0] === record.data.id;
|
||||
});
|
||||
record.data.display_name = nameGet[1];
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -2467,7 +2475,16 @@ var BasicModel = AbstractModel.extend({
|
||||
var self = this;
|
||||
var def;
|
||||
if (list.static) {
|
||||
def = this._readUngroupedList(list);
|
||||
def = this._readUngroupedList(list).then(function () {
|
||||
if (list.parentID && self.isNew(list.parentID)) {
|
||||
// list from a default_get, so fetch display_name for many2one fields
|
||||
var many2ones = self._getMany2OneFieldNames(list);
|
||||
var defs = _.map(many2ones, function (name) {
|
||||
return self._fetchNameGets(list, name);
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
def = this._searchReadUngroupedList(list);
|
||||
}
|
||||
@@ -3035,6 +3052,23 @@ var BasicModel = AbstractModel.extend({
|
||||
var viewType = options && options.viewType || element.viewType;
|
||||
return Object.keys(fieldsInfo && fieldsInfo[viewType] || {});
|
||||
},
|
||||
/**
|
||||
* Get many2one fields names in a datapoint. This is useful in order to
|
||||
* fetch their names in the case of a default_get.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} datapoint a valid resource object
|
||||
* @returns {string[]} list of field names that are many2one
|
||||
*/
|
||||
_getMany2OneFieldNames: function (datapoint) {
|
||||
var many2ones = [];
|
||||
_.each(datapoint.fields, function (field, name) {
|
||||
if (field.type === 'many2one') {
|
||||
many2ones.push(name);
|
||||
}
|
||||
});
|
||||
return many2ones;
|
||||
},
|
||||
/**
|
||||
* Evaluate the record evaluation context. This method is supposed to be
|
||||
* called by _getEvalContext. It basically only generates a dictionary of
|
||||
|
||||
@@ -1237,8 +1237,167 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: discard newly added element with empty required field (onchange)', function (assert) {
|
||||
assert.expect(8);
|
||||
QUnit.test('list in form: name_get with unique ids (default_get)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].display_name = "MyTrululu";
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
return $.when({
|
||||
p: [
|
||||
[0, 0, { trululu: 1 }],
|
||||
[0, 0, { trululu: 1 }]
|
||||
]
|
||||
});
|
||||
}
|
||||
if (args.method === 'name_get') {
|
||||
assert.deepEqual(args.args[0], _.uniq(args.args[0]),
|
||||
"should not have duplicates in name_get rpc");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('td.o_data_cell').text(), "MyTrululuMyTrululu",
|
||||
"both records should have the correct display_name for trululu field");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: show name of many2one fields in multi-page (default_get)', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom" limit="1">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
return $.when({
|
||||
p: [
|
||||
[0, 0, { display_name: 'record1', trululu: 1 }],
|
||||
[0, 0, { display_name: 'record2', trululu: 2 }]
|
||||
]
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('td.o_data_cell').first().text(),
|
||||
"record1", "should show display_name of 1st record");
|
||||
assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
|
||||
"first record", "should show display_name of trululu of 1st record");
|
||||
|
||||
form.$('button.o_pager_next').click();
|
||||
|
||||
assert.strictEqual(form.$('td.o_data_cell').first().text(),
|
||||
"record2", "should show display_name of 2nd record");
|
||||
assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
|
||||
"second record", "should show display_name of trululu of 2nd record");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: discard newly added element with empty required field (onchange in default_get)', function (assert) {
|
||||
// variant of the test "list in form: discard newly added element with
|
||||
// empty required field (default_get)", in which the `default_get`
|
||||
// performs an `onchange` at the same time. This `onchange` may create
|
||||
// some records, which should not be abandoned on discard, similarly
|
||||
// to records created directly by `default_get`
|
||||
assert.expect(7);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
product_id: function (obj) {
|
||||
if (obj.product_id === 37) {
|
||||
obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="trululu" required="1"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
return $.when({
|
||||
product_id: 37,
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// check that there is a record in the editable list with empty string as required field
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"should have a row in the editable list");
|
||||
assert.strictEqual($('td.o_data_cell').first().text(), "entry",
|
||||
"should have the correct displayed name");
|
||||
var requiredField = $('td.o_data_cell.o_required_modifier');
|
||||
assert.strictEqual(requiredField.length, 1,
|
||||
"should have a required field on this record");
|
||||
assert.strictEqual(requiredField.text(), "",
|
||||
"should have empty string in the required field on this record");
|
||||
|
||||
// click on empty required field in editable list record
|
||||
requiredField.click();
|
||||
// click off so that the required field still stay empty
|
||||
$('body').click();
|
||||
|
||||
// record should not be dropped
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"should not have dropped record in the editable list");
|
||||
assert.strictEqual($('td.o_data_cell').first().text(), "entry",
|
||||
"should still have the correct displayed name");
|
||||
assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
|
||||
"should still have empty string in the required field");
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: discard newly added element with empty required field (onchange after default_get)', function (assert) {
|
||||
// discarding a record from an `onchange` in a `default_get` should not
|
||||
// abandon the record. However, any `onchange` after `default_get`
|
||||
// should be abandoned on discard
|
||||
assert.expect(6);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
@@ -1249,7 +1408,6 @@ QUnit.module('relational_fields', {
|
||||
obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
|
||||
}
|
||||
},
|
||||
trululu: function () {},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
@@ -1291,13 +1449,9 @@ QUnit.module('relational_fields', {
|
||||
// click off so that the required field still stay empty
|
||||
$('body').click();
|
||||
|
||||
// record should not be dropped
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"should not have droppped record in the editable list");
|
||||
assert.strictEqual($('td.o_data_cell').first().text(), "entry",
|
||||
"should still have the correct displayed name");
|
||||
assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
|
||||
"should still have empty string in the required field");
|
||||
// record should be dropped
|
||||
assert.strictEqual(form.$('.o_data_row').length, 0,
|
||||
"should have dropped record in the editable list");
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
|
||||
@@ -112,6 +112,7 @@ class WebsiteForm(http.Controller):
|
||||
'record': {}, # Values to create record
|
||||
'attachments': [], # Attached files
|
||||
'custom': '', # Custom fields values
|
||||
'meta': '', # Add metadata if enabled
|
||||
}
|
||||
|
||||
authorized_fields = model.sudo()._get_form_writable_fields()
|
||||
|
||||
@@ -77,7 +77,7 @@ tour.register('shop_buy_product', {
|
||||
content: "Pay Now",
|
||||
//Either there are multiple payment methods, and one is checked, either there is only one, and therefore there are no radio inputs
|
||||
extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked,#payment_method:not(:has("input:radio:visible"))',
|
||||
trigger: 'button[id="o_payment_form_pay"]:visible',
|
||||
trigger: 'button[id="o_payment_form_pay"]:visible:not(:disabled)',
|
||||
},
|
||||
{
|
||||
content: "finish",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class WebsiteSaleDelivery(WebsiteSale):
|
||||
@@ -46,9 +45,6 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
has_stockable_products = any(line.product_id.type in ['consu', 'product'] for line in order.order_line)
|
||||
if has_stockable_products:
|
||||
if order.carrier_id and not order.delivery_rating_success:
|
||||
values['errors'].append(
|
||||
(_("Ouch, you cannot choose this carrier!"),
|
||||
_("%s does not ship to your address, please choose another one.\n(Error: %s)" % (order.carrier_id.name, order.delivery_message))))
|
||||
order._remove_delivery_line()
|
||||
|
||||
delivery_carriers = order._get_delivery_methods()
|
||||
@@ -67,8 +63,15 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
return {'status': order.delivery_rating_success,
|
||||
'error_message': order.delivery_message,
|
||||
'carrier_id': carrier_id,
|
||||
'new_amount_delivery': float_repr(currency.round(order.delivery_price), currency.decimal_places),
|
||||
'new_amount_untaxed': order.amount_untaxed,
|
||||
'new_amount_tax': order.amount_tax,
|
||||
'new_amount_total': order.amount_total,
|
||||
'new_amount_delivery': self._format_amount(order.delivery_price, currency),
|
||||
'new_amount_untaxed': self._format_amount(order.amount_untaxed, currency),
|
||||
'new_amount_tax': self._format_amount(order.amount_tax, currency),
|
||||
'new_amount_total': self._format_amount(order.amount_total, currency),
|
||||
}
|
||||
|
||||
def _format_amount(self, amount, currency):
|
||||
fmt = "%.{0}f".format(currency.decimal_places)
|
||||
lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
|
||||
|
||||
return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
|
||||
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
|
||||
|
||||
@@ -65,12 +65,11 @@ class SaleOrder(models.Model):
|
||||
carrier = delivery
|
||||
break
|
||||
self.write({'carrier_id': carrier.id})
|
||||
self._remove_delivery_line()
|
||||
if carrier:
|
||||
self.get_delivery_price()
|
||||
if self.delivery_rating_success:
|
||||
self.set_delivery_line()
|
||||
else:
|
||||
self._remove_delivery_line()
|
||||
|
||||
return bool(carrier)
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ odoo.define('website_sale_delivery.checkout', function (require) {
|
||||
else {
|
||||
console.error(result.error_message);
|
||||
$compute_badge.text(result.error_message);
|
||||
$amount_delivery.text(result.new_amount_delivery);
|
||||
$amount_untaxed.text(result.new_amount_untaxed);
|
||||
$amount_tax.text(result.new_amount_tax);
|
||||
$amount_total.text(result.new_amount_total);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,6 +44,12 @@ odoo.define('website_sale_delivery.checkout', function (require) {
|
||||
|
||||
var $carriers = $("#delivery_carrier input[name='delivery_type']");
|
||||
$carriers.click(_onCarrierClick);
|
||||
// Workaround to:
|
||||
// - update the amount/error on the label at first rendering
|
||||
// - prevent clicking on 'Pay Now' if the shipper rating fails
|
||||
if ($carriers.length > 0) {
|
||||
$carriers.filter(':checked').click();
|
||||
}
|
||||
|
||||
/* Handle stuff */
|
||||
$(".oe_website_sale select[name='shipping_id']").on('change', function () {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<template id="cart_delivery" name="Delivery Costs" inherit_id="website_sale.total">
|
||||
<xpath expr="//div[@id='order_total_untaxed']" position="before">
|
||||
<div class="row" id="order_delivery" t-if="website_sale_order and website_sale_order.has_delivery">
|
||||
<div class="row" id="order_delivery" t-if="website_sale_order and website_sale_order.carrier_id">
|
||||
<span class="col-xs-6 text-right text-muted" title="Delivery will be updated after choosing a new delivery method"> Delivery:</span>
|
||||
<span class="col-xs-6 text-right-not-xs text-left-xs text-muted">
|
||||
<span t-field="website_sale_order.amount_delivery" style="white-space: nowrap;" t-options='{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
Spain, 2018-05-12
|
||||
|
||||
Acysos S.L. 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,
|
||||
|
||||
Ignacio Ibeas ignacio@acysos.com https://github.com/acysos
|
||||
|
||||
List of contributors:
|
||||
|
||||
Ignacio Ibeas ignacio@acysos.com https://github.com/acysos
|
||||
Alex Ezquebo alexander@acysos.com https://github.com/acysos
|
||||
@@ -0,0 +1,11 @@
|
||||
Italy, 2018-05-17
|
||||
|
||||
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,
|
||||
|
||||
Samuele Carli <carlisamuele@csspace.net> https://github.com/WohthaN
|
||||
@@ -397,6 +397,7 @@ state_it_sv,it,"Savona","SV"
|
||||
state_it_si,it,"Siena","SI"
|
||||
state_it_sr,it,"Siracusa","SR"
|
||||
state_it_so,it,"Sondrio","SO"
|
||||
state_it_su,it,"Sud Sardegna","SU"
|
||||
state_it_ta,it,"Taranto","TA"
|
||||
state_it_te,it,"Teramo","TE"
|
||||
state_it_tr,it,"Terni","TR"
|
||||
|
||||
|
@@ -1123,6 +1123,7 @@
|
||||
<field file="base/static/img/country_flags/mx.png" name="image" type="base64" />
|
||||
<field name="currency_id" ref="MXN" />
|
||||
<field eval="52" name="phone_code" />
|
||||
<field name="vat_label">RFC</field>
|
||||
</record>
|
||||
<record id="my" model="res.country">
|
||||
<field name="name">Malaysia</field>
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div class="o_kanban_image">
|
||||
<img t-att-src="kanban_image('res.users', 'image_medium', record.id.value)"/>
|
||||
<img t-att-src="kanban_image('res.users', 'image_medium', record.id.raw_value)"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<ul>
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ def redirect_with_hash(url, code=303):
|
||||
# FIXME: decide whether urls should be bytes or text, apparently
|
||||
# addons/website/controllers/main.py:91 calls this with a bytes url
|
||||
# but addons/web/controllers/main.py:481 uses text... (blows up on login)
|
||||
url = pycompat.to_text(url)
|
||||
url = pycompat.to_text(url).strip()
|
||||
if urls.url_parse(url, scheme='http').scheme not in ('http', 'https'):
|
||||
url = u'http://' + url
|
||||
url = url.replace("'", "%27").replace("<", "%3C")
|
||||
|
||||
@@ -130,7 +130,7 @@ class Registry(Mapping):
|
||||
self.loaded = False # whether all modules are loaded
|
||||
self.ready = False # whether everything is set up
|
||||
|
||||
# Inter-process signaling (used only when odoo.multi_process is True):
|
||||
# Inter-process signaling:
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
@@ -358,7 +358,7 @@ class Registry(Mapping):
|
||||
|
||||
def setup_signaling(self):
|
||||
""" Setup the inter-process signaling on this registry. """
|
||||
if not odoo.multi_process:
|
||||
if self.in_test_mode():
|
||||
return
|
||||
|
||||
with self.cursor() as cr:
|
||||
@@ -384,7 +384,7 @@ class Registry(Mapping):
|
||||
""" Check whether the registry has changed, and performs all necessary
|
||||
operations to update the registry. Return an up-to-date registry.
|
||||
"""
|
||||
if not odoo.multi_process:
|
||||
if self.in_test_mode():
|
||||
return self
|
||||
|
||||
with closing(self.cursor()) as cr:
|
||||
@@ -410,7 +410,7 @@ class Registry(Mapping):
|
||||
|
||||
def signal_changes(self):
|
||||
""" Notifies other processes if registry or cache has been invalidated. """
|
||||
if odoo.multi_process and self.registry_invalidated:
|
||||
if self.registry_invalidated and not self.in_test_mode():
|
||||
_logger.info("Registry changed, signaling through the database")
|
||||
with closing(self.cursor()) as cr:
|
||||
cr.execute("select nextval('base_registry_signaling')")
|
||||
@@ -418,7 +418,7 @@ class Registry(Mapping):
|
||||
|
||||
# no need to notify cache invalidation in case of registry invalidation,
|
||||
# because reloading the registry implies starting with an empty cache
|
||||
elif odoo.multi_process and self.cache_invalidated:
|
||||
elif self.cache_invalidated and not self.in_test_mode():
|
||||
_logger.info("At least one model cache has been invalidated, signaling through the database.")
|
||||
with closing(self.cursor()) as cr:
|
||||
cr.execute("select nextval('base_cache_signaling')")
|
||||
|
||||
@@ -92,9 +92,6 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler):
|
||||
me = threading.currentThread()
|
||||
me.name = 'odoo.service.http.request.%s' % (me.ident,)
|
||||
|
||||
# _reexec() should set LISTEN_* to avoid connection refused during reload time. It
|
||||
# should also work with systemd socket activation. This is currently untested
|
||||
# and not yet used.
|
||||
|
||||
class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.ThreadedWSGIServer):
|
||||
""" werkzeug Threaded WSGI Server patched to allow reusing a listen socket
|
||||
@@ -106,14 +103,15 @@ class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.
|
||||
handler=RequestHandler)
|
||||
|
||||
def server_bind(self):
|
||||
envfd = os.environ.get('LISTEN_FDS')
|
||||
if envfd and os.environ.get('LISTEN_PID') == str(os.getpid()):
|
||||
SD_LISTEN_FDS_START = 3
|
||||
if os.environ.get('LISTEN_FDS') == '1' and os.environ.get('LISTEN_PID') == str(os.getpid()):
|
||||
self.reload_socket = True
|
||||
self.socket = socket.fromfd(int(envfd), socket.AF_INET, socket.SOCK_STREAM)
|
||||
# should we os.close(int(envfd)) ? it seem python duplicate the fd.
|
||||
self.socket = socket.fromfd(SD_LISTEN_FDS_START, socket.AF_INET, socket.SOCK_STREAM)
|
||||
_logger.info('HTTP service (werkzeug) running through socket activation')
|
||||
else:
|
||||
self.reload_socket = False
|
||||
super(ThreadedWSGIServerReloadable, self).server_bind()
|
||||
_logger.info('HTTP service (werkzeug) running on %s:%s', self.server_name, self.server_port)
|
||||
|
||||
def server_activate(self):
|
||||
if not self.reload_socket:
|
||||
@@ -260,7 +258,6 @@ class ThreadedServer(CommonServer):
|
||||
t = threading.Thread(target=self.http_thread, name="odoo.service.httpd")
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
_logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port)
|
||||
|
||||
def start(self, stop=False):
|
||||
_logger.debug("Setting signal handlers")
|
||||
@@ -887,7 +884,8 @@ def _reexec(updated_modules=None):
|
||||
args += ["-u", ','.join(updated_modules)]
|
||||
if not args or args[0] != exe:
|
||||
args.insert(0, exe)
|
||||
os.execv(sys.executable, args)
|
||||
# We should keep the LISTEN_* environment variabled in order to support socket activation on reexec
|
||||
os.execve(sys.executable, args, os.environ)
|
||||
|
||||
def load_test_file_py(registry, test_file):
|
||||
# Locate python module based on its filename and run the tests
|
||||
|
||||
+6
-3
@@ -10,7 +10,7 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from email.header import decode_header
|
||||
from email.header import decode_header, Header
|
||||
from email.utils import getaddresses, formataddr
|
||||
from lxml import etree
|
||||
|
||||
@@ -524,11 +524,14 @@ def decode_smtp_header(smtp_header):
|
||||
text. email.header decode_header method return a decoded string and its
|
||||
charset for each decoded par of the header. This method unicodes the
|
||||
decoded header and join them in a complete string. """
|
||||
if isinstance(smtp_header, Header):
|
||||
smtp_header = ustr(smtp_header)
|
||||
if smtp_header:
|
||||
text = decode_header(smtp_header.replace('\r', ''))
|
||||
# The joining space will not be needed as of Python 3.3
|
||||
# See https://hg.python.org/cpython/rev/8c03fe231877
|
||||
return ' '.join([ustr(x[0], x[1]) for x in text])
|
||||
# See https://github.com/python/cpython/commit/07ea53cb218812404cdbde820647ce6e4b2d0f8e
|
||||
sep = ' ' if pycompat.PY2 else ''
|
||||
return sep.join([ustr(x[0], x[1]) for x in text])
|
||||
return u''
|
||||
|
||||
# was mail_thread.decode_header()
|
||||
|
||||
Reference in New Issue
Block a user