mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-25 07:27:39 -05:00
[PATCH] Upstream patch - 17042022
This commit is contained in:
1 parent
a18957625a
commit
2b8cb28de2
25 files changed
+1015
-132
No files matched your search
@@ -2134,7 +2134,7 @@ class AccountMove(models.Model):
|
||||
values['total_amount_currency'] += sign * line.amount_currency
|
||||
values['total_residual_currency'] += sign * line.amount_residual_currency
|
||||
|
||||
elif not line.tax_exigible and not line.reconciled:
|
||||
elif not line.tax_exigible:
|
||||
|
||||
values['to_process_lines'] += line
|
||||
currencies.add(line.currency_id or line.company_currency_id)
|
||||
@@ -3653,6 +3653,16 @@ class AccountMoveLine(models.Model):
|
||||
vals = {'price_unit': 0.0}
|
||||
return vals
|
||||
|
||||
def _get_invoiced_qty_per_product(self):
|
||||
qties = defaultdict(float)
|
||||
for aml in self:
|
||||
qty = aml.product_uom_id._compute_quantity(aml.quantity, aml.product_id.uom_id)
|
||||
if aml.move_id.move_type == 'out_invoice':
|
||||
qties[aml.product_id] += qty
|
||||
elif aml.move_id.move_type == 'out_refund':
|
||||
qties[aml.product_id] -= qty
|
||||
return qties
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# ONCHANGE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -4600,7 +4610,7 @@ class AccountMoveLine(models.Model):
|
||||
# to compute the residual amount for each of them.
|
||||
# ==========================================================================
|
||||
|
||||
for line in move_values['to_process_lines']:
|
||||
for line in move_values['to_process_lines'].filtered(lambda x: not x.reconciled):
|
||||
|
||||
vals = {
|
||||
'currency_id': line.currency_id.id,
|
||||
|
||||
@@ -38,7 +38,7 @@ class ResPartner(models.Model):
|
||||
|
||||
@api.onchange('vat', 'country_id')
|
||||
def _l10n_it_onchange_vat(self):
|
||||
if not self.l10n_it_codice_fiscale and (self.country_id.code == "IT" or (self.vat and self.vat.startswith("IT"))):
|
||||
if not self.l10n_it_codice_fiscale and self.vat and (self.country_id.code == "IT" or self.vat.startswith("IT")):
|
||||
self.l10n_it_codice_fiscale = self._l10n_it_normalize_codice_fiscale(self.vat)
|
||||
elif self.country_id.code not in [False, "IT"]:
|
||||
self.l10n_it_codice_fiscale = ""
|
||||
|
||||
@@ -14,7 +14,7 @@ class AccountJournal(models.Model):
|
||||
return
|
||||
|
||||
company = self.env['res.company'].browse(vals['company_id']) if vals.get('company_id') else self.env.company
|
||||
if company.country_id.code == "NL" and not vals.get('type_control_ids')[0][2]:
|
||||
if company.country_id.code == "NL" and not vals.get('type_control_ids', [(6, 0, [])])[0][2]:
|
||||
type_control_ids = self.env.ref('account.data_account_type_direct_costs').ids
|
||||
vals['type_control_ids'] = [(6, 0, type_control_ids)]
|
||||
|
||||
|
||||
@@ -138,11 +138,12 @@ class BaseModel(models.AbstractModel):
|
||||
if catchall:
|
||||
result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
|
||||
|
||||
# compute name of reply-to - TDE tocheck: quotes and stuff like that
|
||||
company_name = company.name if company else self.env.company.name
|
||||
for res_id in result_email:
|
||||
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
|
||||
result[res_id] = tools.formataddr((name, result_email[res_id]))
|
||||
result[res_id] = self._notify_get_reply_to_formatted_email(
|
||||
result_email[res_id],
|
||||
doc_names.get(res_id) or '',
|
||||
company or self.env.company
|
||||
)
|
||||
|
||||
left_ids = set(_res_ids) - set(result_email)
|
||||
if left_ids:
|
||||
@@ -150,6 +151,35 @@ class BaseModel(models.AbstractModel):
|
||||
|
||||
return result
|
||||
|
||||
def _notify_get_reply_to_formatted_email(self, record_email, record_name, company):
|
||||
""" Compute formatted email for reply_to and try to avoid refold issue
|
||||
with python that splits the reply-to over multiple lines. It is due to
|
||||
a bad management of quotes (missing quotes after refold). This appears
|
||||
therefore only when having quotes (aka not simple names, and not when
|
||||
being unicode encoded).
|
||||
|
||||
To avoid that issue when formataddr would return more than 78 chars we
|
||||
return a simplified name/email to try to stay under 78 chars. If not
|
||||
possible we return only the email and skip the formataddr which causes
|
||||
the issue in python. We do not use hacks like crop the name part as
|
||||
encoding and quoting would be error prone.
|
||||
"""
|
||||
# address itself is too long for 78 chars limit: return only email
|
||||
if len(record_email) >= 78:
|
||||
return record_email
|
||||
|
||||
company_name = company.name if company else self.env.company.name
|
||||
|
||||
# try company_name + record_name, or record_name alone (or company_name alone)
|
||||
name = f"{company_name} {record_name}" if record_name else company_name
|
||||
|
||||
formatted_email = tools.formataddr((name, record_email))
|
||||
if len(formatted_email) > 78:
|
||||
formatted_email = tools.formataddr((record_name or company_name, record_email))
|
||||
if len(formatted_email) > 78:
|
||||
formatted_email = record_email
|
||||
return formatted_email
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ALIAS MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -709,7 +709,8 @@ class MrpWorkorder(models.Model):
|
||||
duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number)
|
||||
if duration_expected_working < 0:
|
||||
duration_expected_working = 0
|
||||
return alternative_workcenter.time_start + alternative_workcenter.time_stop + cycle_number * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
|
||||
alternative_wc_cycle_nb = float_round(qty_production / alternative_workcenter.capacity, precision_digits=0, rounding_method='UP')
|
||||
return alternative_workcenter.time_start + alternative_workcenter.time_stop + alternative_wc_cycle_nb * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
|
||||
time_cycle = self.operation_id.time_cycle
|
||||
return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency
|
||||
|
||||
|
||||
@@ -44,13 +44,17 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
orderpoint.show_bom = orderpoint.route_id.id in manufacture_route
|
||||
|
||||
def _quantity_in_progress(self):
|
||||
bom_kit_orderpoints = {
|
||||
orderpoint: bom
|
||||
for orderpoint in self
|
||||
for bom in self.env['mrp.bom']._bom_find(product=orderpoint.product_id, bom_type='phantom')
|
||||
if bom
|
||||
}
|
||||
res = super(StockWarehouseOrderpoint, self.filtered(lambda p: p not in bom_kit_orderpoints))._quantity_in_progress()
|
||||
bom_manufacture = self.env['mrp.bom']
|
||||
bom_kit_orderpoints = {}
|
||||
for orderpoint in self:
|
||||
bom = self.env['mrp.bom']._bom_find(product=orderpoint.product_id)
|
||||
if bom.type == 'phantom':
|
||||
bom_kit_orderpoints[orderpoint] = bom
|
||||
elif bom.type == 'normal':
|
||||
bom_manufacture |= bom
|
||||
|
||||
orderpoints_without_kit = self - self.env['stock.warehouse.orderpoint'].concat(*bom_kit_orderpoints.keys())
|
||||
res = super(StockWarehouseOrderpoint, orderpoints_without_kit)._quantity_in_progress()
|
||||
for orderpoint in bom_kit_orderpoints:
|
||||
boms, bom_sub_lines = bom_kit_orderpoints[orderpoint].explode(orderpoint.product_id, 1)
|
||||
ratios_qty_available = []
|
||||
@@ -73,6 +77,16 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
# (the quantity if we have received all in-progress components) - (the quantity using only available components)
|
||||
product_qty = min(ratios_total or [0]) - min(ratios_qty_available or [0])
|
||||
res[orderpoint.id] = orderpoint.product_id.uom_id._compute_quantity(product_qty, orderpoint.product_uom, round=False)
|
||||
|
||||
productions_group = self.env['mrp.production'].read_group(
|
||||
[('bom_id', 'in', bom_manufacture.ids), ('state', '=', 'draft'), ('orderpoint_id', 'in', orderpoints_without_kit.ids)],
|
||||
['orderpoint_id', 'product_qty', 'product_uom_id'],
|
||||
['orderpoint_id', 'product_uom_id'], lazy=False)
|
||||
for p in productions_group:
|
||||
uom = self.env['uom.uom'].browse(p['product_uom_id'][0])
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(p['orderpoint_id'][0])
|
||||
res[orderpoint.id] += uom._compute_quantity(
|
||||
p['product_qty'], orderpoint.product_uom, round=False)
|
||||
return res
|
||||
|
||||
def _set_default_route_id(self):
|
||||
|
||||
@@ -2193,3 +2193,83 @@ class TestMrpOrder(TestMrpCommon):
|
||||
production_form.product_qty = 5
|
||||
production = production_form.save()
|
||||
self.assertEqual(production.workorder_ids[0].duration_expected, 30.0, "Produce 5 with capacity 2, expected is 10mn for each run -> 30mn")
|
||||
|
||||
def test_planning_workorder(self):
|
||||
"""
|
||||
Check that the fastest work center is used when planning the workorder.
|
||||
|
||||
- create two work centers with similar production capacity
|
||||
but the work_center_2 with a longer start and stop time.
|
||||
|
||||
1:/ produce 2 units > work_center_1 faster because
|
||||
it does not need much time to start and to finish the production.
|
||||
|
||||
2/ - update the production capacity of the work_center_2 to 4
|
||||
- produce 4 units > work_center_2 faster because
|
||||
it must do a single cycle while the work_center_1 have to do two cycles.
|
||||
"""
|
||||
workcenter_1 = self.env['mrp.workcenter'].create({
|
||||
'name': 'wc1',
|
||||
'capacity': 2,
|
||||
'time_start': 1,
|
||||
'time_stop': 1,
|
||||
'time_efficiency': 100,
|
||||
})
|
||||
|
||||
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||
'name': 'wc2',
|
||||
'capacity': 2,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 100,
|
||||
'alternative_workcenter_ids': [workcenter_1.id]
|
||||
})
|
||||
|
||||
product_to_build = self.env['product.product'].create({
|
||||
'name': 'final product',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
product_to_use = self.env['product.product'].create({
|
||||
'name': 'component',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_to_build.id,
|
||||
'product_tmpl_id': product_to_build.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Test', 'workcenter_id': workcenter_2.id, 'time_cycle': 60, 'sequence': 1}),
|
||||
],
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_to_use.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
#MO_1
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 2
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
self.assertEqual(mo.workorder_ids[0].workcenter_id.id, workcenter_1.id, 'workcenter_1 is faster than workcenter_2 to manufacture 2 units')
|
||||
# Unplan the mo to prevent the first workcenter from being busy
|
||||
mo.button_unplan()
|
||||
|
||||
# Update the production capcity
|
||||
workcenter_2.capacity = 4
|
||||
|
||||
#MO_2
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 4
|
||||
mo_2 = mo_form.save()
|
||||
mo_2.action_confirm()
|
||||
mo_2.button_plan()
|
||||
self.assertEqual(mo_2.workorder_ids[0].workcenter_id.id, workcenter_2.id, 'workcenter_2 is faster than workcenter_1 to manufacture 4 units')
|
||||
@@ -596,3 +596,83 @@ class TestProcurement(TestMrpCommon):
|
||||
mo.button_mark_done()
|
||||
|
||||
self.assertEqual(pick_output.move_ids_without_package.reserved_availability, 10, "Completed products should have been auto-reserved in picking")
|
||||
|
||||
def test_rr_with_dependance_between_bom(self):
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
route_mto = self.warehouse.mto_pull_id.route_id
|
||||
route_mto.active = True
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id
|
||||
product_1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id, route_mto.id])]
|
||||
})
|
||||
product_3 = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'route_ids': [(6, 0, [route_manufacture.id])]
|
||||
})
|
||||
product_4 = self.env['product.product'].create({
|
||||
'name': 'Product C',
|
||||
'type': 'consu',
|
||||
})
|
||||
|
||||
op1 = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Product A',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_1.id,
|
||||
'product_min_qty': 1,
|
||||
'product_max_qty': 20,
|
||||
})
|
||||
|
||||
op2 = self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'Product B',
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
'product_id': product_3.id,
|
||||
'product_min_qty': 5,
|
||||
'product_max_qty': 50,
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_1.id,
|
||||
'product_tmpl_id': product_1.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_2.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_2.id,
|
||||
'product_tmpl_id': product_2.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_3.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product_3.id,
|
||||
'product_tmpl_id': product_3.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {'product_id': product_4.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
(op1 | op2)._procure_orderpoint_confirm()
|
||||
mo1 = self.env['mrp.production'].search([('product_id', '=', product_1.id)])
|
||||
mo3 = self.env['mrp.production'].search([('product_id', '=', product_3.id)])
|
||||
|
||||
self.assertEqual(len(mo1), 1)
|
||||
self.assertEqual(len(mo3), 1)
|
||||
self.assertEqual(mo1.product_qty, 20)
|
||||
self.assertEqual(mo3.product_qty, 50)
|
||||
@@ -4,3 +4,4 @@ from . import mrp_workcenter
|
||||
from . import mrp_production
|
||||
from . import product
|
||||
from . import stock_move
|
||||
from . import account_move
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
def _get_invoiced_qty_per_product(self):
|
||||
# Replace the kit-type products with their components
|
||||
qties = defaultdict(float)
|
||||
res = super()._get_invoiced_qty_per_product()
|
||||
for product, qty in res.items():
|
||||
bom_kit = self.env['mrp.bom']._bom_find(product=product, company_id=self.company_id[:1].id, bom_type='phantom')
|
||||
if bom_kit:
|
||||
invoiced_qty = product.uom_id._compute_quantity(qty, bom_kit.product_uom_id, round=False)
|
||||
factor = invoiced_qty / bom_kit.product_qty
|
||||
dummy, bom_sub_lines = bom_kit.explode(product, factor)
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
qties[bom_line.product_id] += bom_line.product_uom_id._compute_quantity(bom_line_data['qty'], bom_line.product_id.uom_id)
|
||||
else:
|
||||
qties[product] += qty
|
||||
return qties
|
||||
@@ -17,8 +17,18 @@ class AuthorizeController(http.Controller):
|
||||
@http.route([
|
||||
'/payment/authorize/return/',
|
||||
'/payment/authorize/cancel/',
|
||||
], type='http', auth='public', csrf=False)
|
||||
], type='http', auth='public', csrf=False, save_session=False)
|
||||
def authorize_form_feedback(self, **post):
|
||||
""" Process the data returned by Authorize after redirection.
|
||||
|
||||
The route is flagged with `save_session=False` to prevent Flectra from assigning a new session
|
||||
to the user if they are redirected to this route with a POST request. Indeed, as the session
|
||||
cookie is created without a `SameSite` attribute, some browsers that don't implement the
|
||||
recommended default `SameSite=Lax` behavior will not include the cookie in the redirection
|
||||
request from the payment provider to Flectra. As the redirection to the '/payment/status' page
|
||||
will satisfy any specification of the `SameSite` attribute, the session of the user will be
|
||||
retrieved and with it the transaction which will be immediately post-processed.
|
||||
"""
|
||||
_logger.info('Authorize: entering form_feedback with post data %s', pprint.pformat(post))
|
||||
if post:
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'authorize')
|
||||
@@ -77,7 +87,7 @@ class AuthorizeController(http.Controller):
|
||||
#You cannot issue refunds against unsettled, voided, declined or errored transactions.</quote>
|
||||
return res
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
|
||||
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public', save_session=False)
|
||||
def authorize_s2s_create(self, **post):
|
||||
acquirer_id = int(post.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
flectra.define('pos_adyen.PaymentScreen', function(require) {
|
||||
"use strict";
|
||||
|
||||
const PaymentScreen = require('point_of_sale.PaymentScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { onMounted } = owl.hooks;
|
||||
|
||||
const PosAdyenPaymentScreen = PaymentScreen => class extends PaymentScreen {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
onMounted(() => {
|
||||
const pendingPaymentLine = this.currentOrder.paymentlines.find(
|
||||
paymentLine => paymentLine.payment_method.use_payment_terminal === 'adyen' &&
|
||||
(!paymentLine.is_done() && paymentLine.get_payment_status() !== 'pending')
|
||||
);
|
||||
if (pendingPaymentLine) {
|
||||
const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal;
|
||||
paymentTerminal.set_most_recent_service_id(pendingPaymentLine.terminalServiceId);
|
||||
pendingPaymentLine.set_payment_status('waiting');
|
||||
paymentTerminal.start_get_status_polling().then(isPaymentSuccessful => {
|
||||
if (isPaymentSuccessful) {
|
||||
pendingPaymentLine.set_payment_status('done');
|
||||
pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals;
|
||||
} else {
|
||||
pendingPaymentLine.set_payment_status('retry');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(PaymentScreen, PosAdyenPaymentScreen);
|
||||
|
||||
return PaymentScreen;
|
||||
});
|
||||
@@ -5,4 +5,25 @@ var PaymentAdyen = require('pos_adyen.payment');
|
||||
models.register_payment_method('adyen', PaymentAdyen);
|
||||
models.register_payment_method('flectra_adyen', PaymentAdyen);
|
||||
models.load_fields('pos.payment.method', 'adyen_terminal_identifier');
|
||||
|
||||
const superPaymentline = models.Paymentline.prototype;
|
||||
models.Paymentline = models.Paymentline.extend({
|
||||
initialize: function(attr, options) {
|
||||
superPaymentline.initialize.call(this,attr,options);
|
||||
this.terminalServiceId = this.terminalServiceId || null;
|
||||
},
|
||||
export_as_JSON: function(){
|
||||
const json = superPaymentline.export_as_JSON.call(this);
|
||||
json.terminal_service_id = this.terminalServiceId;
|
||||
return json;
|
||||
},
|
||||
init_from_JSON: function(json){
|
||||
superPaymentline.init_from_JSON.apply(this,arguments);
|
||||
this.terminalServiceId = json.terminal_service_id;
|
||||
},
|
||||
setTerminalServiceId: function(id) {
|
||||
this.terminalServiceId = id;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -12,7 +12,7 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
send_payment_request: function (cid) {
|
||||
this._super.apply(this, arguments);
|
||||
this._reset_state();
|
||||
return this._adyen_pay();
|
||||
return this._adyen_pay(cid);
|
||||
},
|
||||
send_payment_cancel: function (order, cid) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -24,6 +24,10 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
set_most_recent_service_id(id) {
|
||||
this.most_recent_service_id = id;
|
||||
},
|
||||
|
||||
// private methods
|
||||
_reset_state: function () {
|
||||
this.was_cancelled = false;
|
||||
@@ -110,7 +114,7 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
return data;
|
||||
},
|
||||
|
||||
_adyen_pay: function () {
|
||||
_adyen_pay: function (cid) {
|
||||
var self = this;
|
||||
var order = this.pos.get_order();
|
||||
|
||||
@@ -125,7 +129,8 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
}
|
||||
|
||||
var data = this._adyen_pay_data();
|
||||
|
||||
var line = order.paymentlines.find(paymentLine => paymentLine.cid === cid);
|
||||
line.setTerminalServiceId(this.most_recent_service_id);
|
||||
return this._call_adyen(data).then(function (data) {
|
||||
return self._adyen_handle_response(data);
|
||||
});
|
||||
@@ -265,6 +270,8 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
self._show_error(_t('The connection to your payment terminal failed. Please check if it is still connected to the internet.'));
|
||||
self._adyen_cancel();
|
||||
resolve(false);
|
||||
} else {
|
||||
line.set_payment_status('waitingCard')
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -296,27 +303,30 @@ var PaymentAdyen = PaymentInterface.extend({
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
line.set_payment_status('waitingCard');
|
||||
|
||||
var self = this;
|
||||
var res = new Promise(function (resolve, reject) {
|
||||
// clear previous intervals just in case, otherwise
|
||||
// it'll run forever
|
||||
clearTimeout(self.polling);
|
||||
|
||||
self.polling = setInterval(function () {
|
||||
self._poll_for_response(resolve, reject);
|
||||
}, 5500);
|
||||
});
|
||||
|
||||
// make sure to stop polling when we're done
|
||||
res.finally(function () {
|
||||
self._reset_state();
|
||||
});
|
||||
|
||||
return res;
|
||||
return this.start_get_status_polling()
|
||||
}
|
||||
},
|
||||
|
||||
start_get_status_polling() {
|
||||
var self = this;
|
||||
var res = new Promise(function (resolve, reject) {
|
||||
// clear previous intervals just in case, otherwise
|
||||
// it'll run forever
|
||||
clearTimeout(self.polling);
|
||||
self._poll_for_response(resolve, reject);
|
||||
self.polling = setInterval(function () {
|
||||
self._poll_for_response(resolve, reject);
|
||||
}, 5500);
|
||||
});
|
||||
|
||||
// make sure to stop polling when we're done
|
||||
res.finally(function () {
|
||||
self._reset_state();
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
_show_error: function (msg, title) {
|
||||
if (!title) {
|
||||
title = _t('Adyen Error');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<template id="assets" inherit_id="point_of_sale.assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/pos_adyen/static/src/js/payment_adyen.js"></script>
|
||||
<script type="text/javascript" src="/pos_adyen/static/src/js/PaymentScreen.js"></script>
|
||||
<script type="text/javascript" src="/pos_adyen/static/src/js/models.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -528,7 +528,7 @@ class PurchaseOrderLine(models.Model):
|
||||
description_picking = values['product_description_variants']
|
||||
lines = self.filtered(
|
||||
lambda l: l.propagate_cancel == values['propagate_cancel']
|
||||
and ((values['orderpoint_id'] and not values['move_dest_ids']) and l.orderpoint_id == values['orderpoint_id'] or True)
|
||||
and (l.orderpoint_id == values['orderpoint_id'] if values['orderpoint_id'] and not values['move_dest_ids'] else True)
|
||||
)
|
||||
|
||||
# In case 'product_description_variants' is in the values, we also filter on the PO line
|
||||
|
||||
@@ -210,3 +210,125 @@ class TestValuationReconciliation(ValuationReconciliationTestCommon):
|
||||
|
||||
picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
|
||||
self.check_reconciliation(invoice, picking)
|
||||
|
||||
def test_reconcile_cash_basis_bill(self):
|
||||
''' Test the generation of the CABA move after bill payment
|
||||
'''
|
||||
cash_basis_base_account = self.env['account.account'].create({
|
||||
'code': 'cash_basis_base_account',
|
||||
'name': 'cash_basis_base_account',
|
||||
'user_type_id': self.env.ref('account.data_account_type_revenue').id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
self.company_data['company'].account_cash_basis_base_account_id = cash_basis_base_account
|
||||
|
||||
cash_basis_transfer_account = self.env['account.account'].create({
|
||||
'code': 'cash_basis_transfer_account',
|
||||
'name': 'cash_basis_transfer_account',
|
||||
'user_type_id': self.env.ref('account.data_account_type_revenue').id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
tax_account_1 = self.env['account.account'].create({
|
||||
'code': 'tax_account_1',
|
||||
'name': 'tax_account_1',
|
||||
'user_type_id': self.env.ref('account.data_account_type_revenue').id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
fake_country = self.env['res.country'].create({
|
||||
'name': "The Island of the Fly",
|
||||
'code': 'YY',
|
||||
})
|
||||
|
||||
tax_tags = self.env['account.account.tag'].create({
|
||||
'name': 'tax_tag_%s' % str(i),
|
||||
'applicability': 'taxes',
|
||||
'country_id': fake_country.id,
|
||||
} for i in range(8))
|
||||
|
||||
cash_basis_tax_a_third_amount = self.env['account.tax'].create({
|
||||
'name': 'tax_1',
|
||||
'amount': 33.3333,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'cash_basis_transition_account_id': cash_basis_transfer_account.id,
|
||||
'tax_exigibility': 'on_payment',
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, tax_tags[0].ids)],
|
||||
}),
|
||||
|
||||
(0, 0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': tax_account_1.id,
|
||||
'tag_ids': [(6, 0, tax_tags[1].ids)],
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [(6, 0, tax_tags[2].ids)],
|
||||
}),
|
||||
|
||||
(0, 0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': tax_account_1.id,
|
||||
'tag_ids': [(6, 0, tax_tags[3].ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
product_A = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Product A",
|
||||
"type": "product",
|
||||
"default_code": "prda",
|
||||
"categ_id": self.stock_account_product_categ.id,
|
||||
"taxes_id": [(5, 0, 0)],
|
||||
"supplier_taxes_id": [(6, 0, cash_basis_tax_a_third_amount.ids)],
|
||||
"lst_price": 100.0,
|
||||
"standard_price": 10.0,
|
||||
"property_account_income_id": self.company_data["default_account_revenue"].id,
|
||||
"property_account_expense_id": self.company_data["default_account_expense"].id,
|
||||
}
|
||||
)
|
||||
product_A.categ_id.write(
|
||||
{
|
||||
"property_account_creditor_price_difference_categ": False,
|
||||
"property_valuation": "real_time",
|
||||
"property_cost_method": "standard",
|
||||
}
|
||||
)
|
||||
|
||||
date_po_and_delivery = '2018-01-01'
|
||||
purchase_order = self._create_purchase(product_A, date_po_and_delivery, set_tax=True, price_unit=300.0)
|
||||
self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
|
||||
|
||||
bill = self._create_invoice_for_po(purchase_order, '2018-02-02')
|
||||
bill.action_post()
|
||||
|
||||
# Register a payment creating the CABA journal entry on the fly and reconcile it with the tax line.
|
||||
self.env['account.payment.register']\
|
||||
.with_context(active_ids=bill.ids, active_model='account.move')\
|
||||
.create({})\
|
||||
._create_payments()
|
||||
|
||||
partial_rec = bill.mapped('line_ids.matched_debit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
|
||||
|
||||
# Tax values based on payment
|
||||
# Invoice amount 300
|
||||
self.assertRecordValues(caba_move.line_ids, [
|
||||
# pylint: disable=C0326
|
||||
# Base amount:
|
||||
{'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'account_id': cash_basis_base_account.id},
|
||||
{'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'account_id': cash_basis_base_account.id},
|
||||
# tax:
|
||||
{'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'account_id': cash_basis_transfer_account.id},
|
||||
{'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'account_id': tax_account_1.id},
|
||||
])
|
||||
@@ -637,3 +637,49 @@ class TestReorderingRule(SavepointCase):
|
||||
[("product_id", "=", product.id)])
|
||||
self.assertTrue(po_line)
|
||||
self.assertEqual("[A] product TEST", po_line.name)
|
||||
|
||||
def test_multi_locations_and_reordering_rule(self):
|
||||
"""
|
||||
Suppose two orderpoints for the same product, each one to a different location
|
||||
If the user triggers each orderpoint separately, it should still produce two
|
||||
different purchase order lines (one for each orderpoint)
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
stock_location = warehouse.lot_stock_id
|
||||
sub_location = self.env['stock.location'].create({'name': 'subloc_1', 'location_id': stock_location.id})
|
||||
|
||||
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
orderpoint_form.warehouse_id = warehouse
|
||||
orderpoint_form.location_id = stock_location
|
||||
orderpoint_form.product_id = self.product_01
|
||||
orderpoint_form.product_min_qty = 1
|
||||
orderpoint_form.product_max_qty = 1
|
||||
stock_op = orderpoint_form.save()
|
||||
|
||||
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
orderpoint_form.warehouse_id = warehouse
|
||||
orderpoint_form.location_id = sub_location
|
||||
orderpoint_form.product_id = self.product_01
|
||||
orderpoint_form.product_min_qty = 2
|
||||
orderpoint_form.product_max_qty = 2
|
||||
sub_op = orderpoint_form.save()
|
||||
|
||||
stock_op.action_replenish()
|
||||
sub_op.action_replenish()
|
||||
|
||||
po = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)])
|
||||
self.assertRecordValues(po.order_line, [
|
||||
{'product_id': self.product_01.id, 'product_qty': 1.0, 'orderpoint_id': stock_op.id},
|
||||
{'product_id': self.product_01.id, 'product_qty': 2.0, 'orderpoint_id': sub_op.id},
|
||||
])
|
||||
|
||||
po.button_confirm()
|
||||
picking = po.picking_ids
|
||||
action = picking.button_validate()
|
||||
wizard = Form(self.env[(action.get('res_model'))].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
self.assertRecordValues(picking.move_line_ids, [
|
||||
{'product_id': self.product_01.id, 'qty_done': 1.0, 'state': 'done', 'location_dest_id': stock_location.id},
|
||||
{'product_id': self.product_01.id, 'qty_done': 2.0, 'state': 'done', 'location_dest_id': sub_location.id},
|
||||
])
|
||||
@@ -5,4 +5,5 @@ from . import test_sale_mrp_flow
|
||||
from . import test_sale_mrp_kit_bom
|
||||
from . import test_sale_mrp_lead_time
|
||||
from . import test_sale_mrp_procurement
|
||||
from . import test_multistep_manufacturing
|
||||
from . import test_multistep_manufacturing
|
||||
from . import test_sale_mrp_report
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.tests import common, Form
|
||||
|
||||
from flectra.tools import html2plaintext
|
||||
|
||||
@common.tagged('post_install', '-at_install')
|
||||
class TestSaleMrpInvoices(common.SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.product_by_lot = cls.env['product.product'].create({
|
||||
'name': 'Product By Lot',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.env.company.id)], limit=1)
|
||||
cls.stock_location = cls.warehouse.lot_stock_id
|
||||
cls.lot = cls.env['stock.production.lot'].create({
|
||||
'name': 'LOT0001',
|
||||
'product_id': cls.product_by_lot.id,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
cls.env['stock.quant']._update_available_quantity(cls.product_by_lot, cls.stock_location, 10, lot_id=cls.lot)
|
||||
|
||||
cls.tracked_kit = cls.env['product.product'].create({
|
||||
'name': 'Simple Kit',
|
||||
'type': 'consu',
|
||||
})
|
||||
cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.tracked_kit.product_tmpl_id.id,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': cls.product_by_lot.id,
|
||||
'product_qty': 1,
|
||||
})]
|
||||
})
|
||||
cls.partner = cls.env.ref('base.res_partner_1')
|
||||
|
||||
def test_deliver_and_invoice_tracked_components(self):
|
||||
"""
|
||||
Suppose the lots are printed on the invoices.
|
||||
The user sells a kit that has one tracked component.
|
||||
The lot of the delivered component should be on the invoice.
|
||||
"""
|
||||
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
|
||||
display_uom = self.env.ref('uom.group_uom')
|
||||
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': self.tracked_kit.name, 'product_id': self.tracked_kit.id, 'product_uom_qty': 1}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
action = so.picking_ids.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
invoice = so._create_invoices()
|
||||
invoice.action_post()
|
||||
|
||||
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
html = report._render_qweb_html(invoice.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By Lot\n1.00\nUnits\nLOT0001', "There should be a line that specifies 1 x LOT0001")
|
||||
@@ -4,7 +4,8 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from flectra import fields, models
|
||||
from flectra.tools import float_is_zero
|
||||
from flectra.tools import float_is_zero, float_compare
|
||||
from flectra.tools.misc import formatLang
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
@@ -25,86 +26,62 @@ class AccountMove(models.Model):
|
||||
def _get_invoiced_lot_values(self):
|
||||
""" Get and prepare data to show a table of invoiced lot on the invoice's report. """
|
||||
self.ensure_one()
|
||||
|
||||
if self.state == 'draft':
|
||||
if self.state == 'draft' or not self.invoice_date or self.move_type not in ('out_invoice', 'out_refund'):
|
||||
return []
|
||||
|
||||
sale_lines = self.invoice_line_ids.sale_line_ids
|
||||
sale_orders = sale_lines.order_id
|
||||
stock_move_lines = sale_lines.move_ids.filtered(lambda r: r.state == 'done').move_line_ids
|
||||
current_invoice_amls = self.invoice_line_ids.filtered(lambda aml: not aml.display_type and aml.product_id and aml.quantity)
|
||||
all_invoices_amls = current_invoice_amls.sale_line_ids.invoice_lines.filtered(lambda aml: aml.move_id.state == 'posted').sorted(lambda aml: (aml.date, aml.move_name, aml.id))
|
||||
index = all_invoices_amls.ids.index(current_invoice_amls[:1].id) if current_invoice_amls[:1] in all_invoices_amls else 0
|
||||
previous_amls = all_invoices_amls[:index]
|
||||
|
||||
# Get the other customer invoices and refunds.
|
||||
ordered_invoice_ids = sale_orders.mapped('invoice_ids')\
|
||||
.filtered(lambda i: i.state not in ['draft', 'cancel'])\
|
||||
.sorted(lambda i: (i.invoice_date, i.id))
|
||||
previous_qties_invoiced = previous_amls._get_invoiced_qty_per_product()
|
||||
invoiced_qties = current_invoice_amls._get_invoiced_qty_per_product()
|
||||
invoiced_products = invoiced_qties.keys()
|
||||
|
||||
# Get the position of self in other customer invoices and refunds.
|
||||
self_index = None
|
||||
i = 0
|
||||
for invoice in ordered_invoice_ids:
|
||||
if invoice.id == self.id:
|
||||
self_index = i
|
||||
break
|
||||
i += 1
|
||||
|
||||
# Get the previous invoices if any.
|
||||
previous_invoices = ordered_invoice_ids[:self_index]
|
||||
|
||||
# Get the incoming and outgoing sml between self.invoice_date and the previous invoice (if any) of the related product.
|
||||
write_dates = [wd for wd in self.invoice_line_ids.mapped('write_date') if wd]
|
||||
self_datetime = max(write_dates) if write_dates else None
|
||||
last_invoice_datetime = dict()
|
||||
for product in self.invoice_line_ids.product_id:
|
||||
last_invoice = previous_invoices.filtered(lambda inv: product in inv.invoice_line_ids.product_id)
|
||||
last_invoice = last_invoice[-1] if len(last_invoice) else None
|
||||
last_write_dates = last_invoice and [wd for wd in last_invoice.invoice_line_ids.mapped('write_date') if wd]
|
||||
last_invoice_datetime[product] = max(last_write_dates) if last_write_dates else None
|
||||
|
||||
def _filter_incoming_sml(ml):
|
||||
if ml.state == 'done' and ml.location_id.usage == 'customer' and ml.lot_id:
|
||||
last_date = last_invoice_datetime.get(ml.product_id)
|
||||
if last_date:
|
||||
return last_date <= ml.date <= self_datetime
|
||||
else:
|
||||
return ml.date <= self_datetime
|
||||
return False
|
||||
|
||||
def _filter_outgoing_sml(ml):
|
||||
if ml.state == 'done' and ml.location_dest_id.usage == 'customer' and ml.lot_id:
|
||||
last_date = last_invoice_datetime.get(ml.product_id)
|
||||
if last_date:
|
||||
return last_date <= ml.date <= self_datetime
|
||||
else:
|
||||
return ml.date <= self_datetime
|
||||
return False
|
||||
|
||||
incoming_sml = stock_move_lines.filtered(_filter_incoming_sml)
|
||||
outgoing_sml = stock_move_lines.filtered(_filter_outgoing_sml)
|
||||
|
||||
# Prepare and return lot_values
|
||||
qties_per_lot = defaultdict(lambda: 0)
|
||||
if self.move_type == 'out_refund':
|
||||
for ml in outgoing_sml:
|
||||
qties_per_lot[ml.lot_id] -= ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
|
||||
for ml in incoming_sml:
|
||||
qties_per_lot[ml.lot_id] += ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
|
||||
else:
|
||||
for ml in outgoing_sml:
|
||||
qties_per_lot[ml.lot_id] += ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
|
||||
for ml in incoming_sml:
|
||||
qties_per_lot[ml.lot_id] -= ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
|
||||
lot_values = []
|
||||
for lot_id, qty in qties_per_lot.items():
|
||||
if float_is_zero(qty, precision_rounding=lot_id.product_id.uom_id.rounding):
|
||||
qties_per_lot = defaultdict(float)
|
||||
previous_qties_delivered = defaultdict(float)
|
||||
stock_move_lines = current_invoice_amls.sale_line_ids.move_ids.move_line_ids.filtered(lambda sml: sml.state == 'done' and sml.lot_id).sorted(lambda sml: (sml.date, sml.id))
|
||||
for sml in stock_move_lines:
|
||||
if sml.product_id not in invoiced_products:
|
||||
continue
|
||||
product = sml.product_id
|
||||
product_uom = product.uom_id
|
||||
qty_done = sml.product_uom_id._compute_quantity(sml.qty_done, product_uom)
|
||||
|
||||
if sml.location_id.usage == 'customer':
|
||||
returned_qty = min(qties_per_lot[sml.lot_id], qty_done)
|
||||
qties_per_lot[sml.lot_id] -= returned_qty
|
||||
qty_done = returned_qty - qty_done
|
||||
|
||||
previous_qty_invoiced = previous_qties_invoiced[product]
|
||||
previous_qty_delivered = previous_qties_delivered[product]
|
||||
# If we return more than currently delivered (i.e., qty_done < 0), we remove the surplus
|
||||
# from the previously delivered (and qty_done becomes zero). If it's a delivery, we first
|
||||
# try to reach the previous_qty_invoiced
|
||||
if float_compare(qty_done, 0, precision_rounding=product_uom.rounding) < 0 or \
|
||||
float_compare(previous_qty_delivered, previous_qty_invoiced, precision_rounding=product_uom.rounding) < 0:
|
||||
previously_done = qty_done if sml.location_id.usage == 'customer' else min(previous_qty_invoiced - previous_qty_delivered, qty_done)
|
||||
previous_qties_delivered[product] += previously_done
|
||||
qty_done -= previously_done
|
||||
|
||||
qties_per_lot[sml.lot_id] += qty_done
|
||||
|
||||
lot_values = []
|
||||
for lot, qty in qties_per_lot.items():
|
||||
if float_is_zero(invoiced_qties[lot.product_id], precision_rounding=lot.product_uom_id.rounding) \
|
||||
or float_compare(qty, 0, precision_rounding=lot.product_uom_id.rounding) <= 0:
|
||||
continue
|
||||
invoiced_lot_qty = min(qty, invoiced_qties[lot.product_id])
|
||||
invoiced_qties[lot.product_id] -= invoiced_lot_qty
|
||||
lot_values.append({
|
||||
'product_name': lot_id.product_id.display_name,
|
||||
'quantity': self.env['ir.qweb.field.float'].value_to_html(qty, {'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure')}),
|
||||
'uom_name': lot_id.product_uom_id.name,
|
||||
'lot_name': lot_id.name,
|
||||
'product_name': lot.product_id.display_name,
|
||||
'quantity': formatLang(self.env, invoiced_lot_qty, dp='Product Unit of Measure'),
|
||||
'uom_name': lot.product_uom_id.name,
|
||||
'lot_name': lot.name,
|
||||
# The lot id is needed by localizations to inherit the method and add custom fields on the invoice's report.
|
||||
'lot_id': lot_id.id
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
|
||||
return lot_values
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ from . import test_sale_stock_lead_time
|
||||
from . import test_sale_stock_report
|
||||
from . import test_sale_order_dates
|
||||
from . import test_sale_stock_multicompany
|
||||
from . import test_report
|
||||
@@ -0,0 +1,283 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.tests import tagged, Form
|
||||
from flectra.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
from flectra.tools import html2plaintext
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleStockInvoices(TestSaleCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSaleStockInvoices, self).setUp()
|
||||
|
||||
self.product_by_lot = self.env['product.product'].create({
|
||||
'name': 'Product By Lot',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
self.product_by_usn = self.env['product.product'].create({
|
||||
'name': 'Product By USN',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
self.warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
self.stock_location = self.warehouse.lot_stock_id
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT0001',
|
||||
'product_id': self.product_by_lot.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
usn01 = self.env['stock.production.lot'].create({
|
||||
'name': 'USN0001',
|
||||
'product_id': self.product_by_usn.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
usn02 = self.env['stock.production.lot'].create({
|
||||
'name': 'USN0002',
|
||||
'product_id': self.product_by_usn.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 10, lot_id=lot)
|
||||
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn01)
|
||||
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn02)
|
||||
|
||||
def test_invoice_less_than_delivered(self):
|
||||
"""
|
||||
Suppose the lots are printed on the invoices.
|
||||
A user invoice a tracked product with a smaller quantity than delivered.
|
||||
On the invoice, the quantity of the used lot should be the invoiced one.
|
||||
"""
|
||||
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
|
||||
display_uom = self.env.ref('uom.group_uom')
|
||||
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 5}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.quantity_done = 5
|
||||
picking.button_validate()
|
||||
|
||||
invoice = so._create_invoices()
|
||||
with Form(invoice) as form:
|
||||
with form.invoice_line_ids.edit(0) as line:
|
||||
line.quantity = 2
|
||||
invoice.action_post()
|
||||
|
||||
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
html = report._render_qweb_html(invoice.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By Lot\n2.00\nUnits\nLOT0001', "There should be a line that specifies 2 x LOT0001")
|
||||
|
||||
def test_invoice_before_delivery(self):
|
||||
"""
|
||||
Suppose the lots are printed on the invoices.
|
||||
The user sells a tracked product, its invoicing policy is "Ordered quantities"
|
||||
A user invoice a tracked product with a smaller quantity than delivered.
|
||||
On the invoice, the quantity of the used lot should be the invoiced one.
|
||||
"""
|
||||
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
|
||||
display_uom = self.env.ref('uom.group_uom')
|
||||
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
self.product_by_lot.invoice_policy = "order"
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 4}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
invoice = so._create_invoices()
|
||||
invoice.action_post()
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.quantity_done = 4
|
||||
picking.button_validate()
|
||||
|
||||
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
html = report._render_qweb_html(invoice.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By Lot\n4.00\nUnits\nLOT0001', "There should be a line that specifies 4 x LOT0001")
|
||||
|
||||
def test_backorder_and_several_invoices(self):
|
||||
"""
|
||||
Suppose the lots are printed on the invoices.
|
||||
The user sells 2 tracked-by-usn products, he delivers 1 product and invoices it
|
||||
Then, he delivers the other one and invoices it too. Each invoice should have the
|
||||
correct USN
|
||||
"""
|
||||
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
|
||||
display_uom = self.env.ref('uom.group_uom')
|
||||
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.move_line_ids[0].qty_done = 1
|
||||
picking.button_validate()
|
||||
action = picking.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
invoice01 = so._create_invoices()
|
||||
with Form(invoice01) as form:
|
||||
with form.invoice_line_ids.edit(0) as line:
|
||||
line.quantity = 1
|
||||
invoice01.action_post()
|
||||
|
||||
backorder = picking.backorder_ids
|
||||
backorder.move_lines.move_line_ids.qty_done = 1
|
||||
backorder.button_validate()
|
||||
|
||||
html = report._render_qweb_html(invoice01.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")
|
||||
self.assertNotIn('USN0002', text)
|
||||
|
||||
invoice02 = so._create_invoices()
|
||||
invoice02.action_post()
|
||||
html = report._render_qweb_html(invoice02.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002")
|
||||
self.assertNotIn('USN0001', text)
|
||||
|
||||
# Posting the second invoice shouldn't change the result of the first one
|
||||
html = report._render_qweb_html(invoice01.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should still be a line that specifies 1 x USN0001")
|
||||
self.assertNotIn('USN0002', text)
|
||||
|
||||
# Resetting and posting again the first invoice shouldn't change the results
|
||||
invoice01.button_draft()
|
||||
invoice01.action_post()
|
||||
html = report._render_qweb_html(invoice01.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should still be a line that specifies 1 x USN0001")
|
||||
self.assertNotIn('USN0002', text)
|
||||
html = report._render_qweb_html(invoice02.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002")
|
||||
self.assertNotIn('USN0001', text)
|
||||
|
||||
def test_invoice_with_several_returns(self):
|
||||
"""
|
||||
Mix of returns and partial invoice
|
||||
- Product P tracked by lot
|
||||
- SO with 10 x P
|
||||
- Deliver 10 x Lot01
|
||||
- Return 10 x Lot01
|
||||
- Deliver 03 x Lot02
|
||||
- Invoice 02 x P
|
||||
- Deliver 05 x Lot02 + 02 x Lot03
|
||||
- Invoice 08 x P
|
||||
"""
|
||||
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
|
||||
display_uom = self.env.ref('uom.group_uom')
|
||||
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
lot01 = self.env['stock.production.lot'].search([('name', '=', 'LOT0001')])
|
||||
lot02, lot03 = self.env['stock.production.lot'].create([{
|
||||
'name': name,
|
||||
'product_id': self.product_by_lot.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for name in ['LOT0002', 'LOT0003']])
|
||||
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 8, lot_id=lot02)
|
||||
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 2, lot_id=lot03)
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 10}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
# Deliver 10 x LOT0001
|
||||
delivery01 = so.picking_ids
|
||||
delivery01.move_lines.quantity_done = 10
|
||||
delivery01.button_validate()
|
||||
self.assertEqual(delivery01.move_line_ids.lot_id.name, 'LOT0001')
|
||||
|
||||
# Return delivery01 (-> 10 x LOT0001)
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[delivery01.id], active_id=delivery01.id, active_model='stock.picking'))
|
||||
return_wizard = return_form.save()
|
||||
action = return_wizard.create_returns()
|
||||
pick_return = self.env['stock.picking'].browse(action['res_id'])
|
||||
|
||||
move_form = Form(pick_return.move_lines, view='stock.view_stock_move_nosuggest_operations')
|
||||
with move_form.move_line_nosuggest_ids.new() as line:
|
||||
line.lot_id = lot01
|
||||
line.qty_done = 10
|
||||
move_form.save()
|
||||
pick_return.button_validate()
|
||||
|
||||
# Return pick_return
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[pick_return.id], active_id=pick_return.id, active_model='stock.picking'))
|
||||
return_wizard = return_form.save()
|
||||
action = return_wizard.create_returns()
|
||||
delivery02 = self.env['stock.picking'].browse(action['res_id'])
|
||||
|
||||
# Deliver 3 x LOT0002
|
||||
delivery02.do_unreserve()
|
||||
move_form = Form(delivery02.move_lines, view='stock.view_stock_move_nosuggest_operations')
|
||||
with move_form.move_line_nosuggest_ids.new() as line:
|
||||
line.lot_id = lot02
|
||||
line.qty_done = 3
|
||||
move_form.save()
|
||||
action = delivery02.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
# Invoice 2 x P
|
||||
invoice01 = so._create_invoices()
|
||||
with Form(invoice01) as form:
|
||||
with form.invoice_line_ids.edit(0) as line:
|
||||
line.quantity = 2
|
||||
invoice01.action_post()
|
||||
|
||||
html = report._render_qweb_html(invoice01.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By Lot\n2.00\nUnits\nLOT0002', "There should be a line that specifies 2 x LOT0002")
|
||||
self.assertNotIn('LOT0001', text)
|
||||
|
||||
# Deliver 5 x LOT0002 + 2 x LOT0003
|
||||
delivery03 = delivery02.backorder_ids
|
||||
delivery03.do_unreserve()
|
||||
move_form = Form(delivery03.move_lines, view='stock.view_stock_move_nosuggest_operations')
|
||||
with move_form.move_line_nosuggest_ids.new() as line:
|
||||
line.lot_id = lot02
|
||||
line.qty_done = 5
|
||||
with move_form.move_line_nosuggest_ids.new() as line:
|
||||
line.lot_id = lot03
|
||||
line.qty_done = 2
|
||||
move_form.save()
|
||||
delivery03.button_validate()
|
||||
|
||||
# Invoice 8 x P
|
||||
invoice02 = so._create_invoices()
|
||||
invoice02.action_post()
|
||||
|
||||
html = report._render_qweb_html(invoice02.ids)[0]
|
||||
text = html2plaintext(html)
|
||||
self.assertRegex(text, r'Product By Lot\n6.00\nUnits\nLOT0002', "There should be a line that specifies 6 x LOT0002")
|
||||
self.assertRegex(text, r'Product By Lot\n2.00\nUnits\nLOT0003', "There should be a line that specifies 2 x LOT0003")
|
||||
self.assertNotIn('LOT0001', text)
|
||||
@@ -65,11 +65,11 @@
|
||||
<div class="o_survey_nav pt16 mb-2">
|
||||
<div class="container m-0 p-0">
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<div class="col-lg-10">
|
||||
<h1 t-if="answer.state == 'new' or survey.questions_layout != 'page_per_question'"
|
||||
t-esc="survey.title" class="o_survey_main_title pt-4"></h1>
|
||||
</div>
|
||||
<div class="o_survey_timer col-2 pt-4">
|
||||
<div class="o_survey_timer col-lg-2 pt-4">
|
||||
<h1 class="o_survey_timer_container timer text-right">
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,10 @@ from flectra.addons.test_mail.tests.common import TestMailCommon
|
||||
from flectra.addons.test_mail.models.test_mail_models import MailTestSimple
|
||||
from flectra.exceptions import AccessError
|
||||
from flectra.tools import mute_logger, formataddr
|
||||
from flectra.tests import tagged
|
||||
from flectra.tests import tagged, users
|
||||
|
||||
|
||||
@tagged('mail_message')
|
||||
class TestMessageValues(TestMailCommon):
|
||||
|
||||
@classmethod
|
||||
@@ -60,8 +61,79 @@ class TestMessageValues(TestMailCommon):
|
||||
res = message.with_user(self.user_employee).message_format()
|
||||
self.assertEqual(res[0].get('record_name'), 'Test1')
|
||||
|
||||
def test_mail_message_values_body_base64_image(self):
|
||||
msg = self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
|
||||
})
|
||||
self.assertEqual(len(msg.attachment_ids), 1)
|
||||
self.assertEqual(
|
||||
msg.body,
|
||||
'<p>taratata <img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"> '
|
||||
'<img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"></p>'.format(attachment=msg.attachment_ids[0])
|
||||
)
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_no_document_values(self):
|
||||
@users('employee')
|
||||
def test_mail_message_values_fromto_long_name(self):
|
||||
""" Long headers may break in python if above 78 chars as folding is not
|
||||
done correctly (see ``_notify_get_reply_to_formatted_email`` docstring
|
||||
+ commit linked to this test). """
|
||||
# name would make it blow up: keep only email
|
||||
test_record = self.env['mail.test.container'].browse(self.alias_record.ids)
|
||||
test_record.write({
|
||||
'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'
|
||||
})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
reply_to_email = f"{test_record.alias_name}@{self.alias_domain}"
|
||||
self.assertEqual(msg.reply_to, reply_to_email,
|
||||
'Reply-To: use only email when formataddr > 78 chars')
|
||||
|
||||
# name + company_name would make it blow up: keep record_name in formatting
|
||||
test_record.write({'name': 'Name that would be more than 78 with company name'})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, formataddr((test_record.name, reply_to_email)),
|
||||
'Reply-To: use recordname as name in format if recordname + company > 78 chars')
|
||||
|
||||
# no record_name: keep company_name in formatting if ok
|
||||
test_record.write({'name': ''})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, formataddr((self.env.user.company_id.name, reply_to_email)),
|
||||
'Reply-To: use company as name in format when no record name and still < 78 chars')
|
||||
|
||||
# no record_name and company_name make it blow up: keep only email
|
||||
self.env.user.company_id.write({'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'})
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, reply_to_email,
|
||||
'Reply-To: use only email when formataddr > 78 chars')
|
||||
|
||||
# whatever the record and company names, email is too long: keep only email
|
||||
test_record.write({
|
||||
'alias_name': 'Waaaay too long alias name that should make any reply-to blow the 78 characters limit',
|
||||
'name': 'Short',
|
||||
})
|
||||
self.env.user.company_id.write({'name': 'Comp'})
|
||||
sanitized_alias_name = 'waaaay-too-long-alias-name-that-should-make-any-reply-to-blow-the-78-characters-limit'
|
||||
msg = self.env['mail.message'].create({
|
||||
'model': test_record._name,
|
||||
'res_id': test_record.id
|
||||
})
|
||||
self.assertEqual(msg.reply_to, f"{sanitized_alias_name}@{self.alias_domain}",
|
||||
'Reply-To: even a long email is ok as only formataddr is problematic')
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_fromto_no_document_values(self):
|
||||
msg = self.Message.create({
|
||||
'reply_to': 'test.reply@example.com',
|
||||
'email_from': 'test.from@example.com',
|
||||
@@ -71,7 +143,7 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertEqual(msg.email_from, 'test.from@example.com')
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_no_document(self):
|
||||
def test_mail_message_values_fromto_no_document(self):
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
reply_to_name = self.env.user.company_id.name
|
||||
@@ -97,7 +169,7 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_document_alias(self):
|
||||
def test_mail_message_values_fromto_document_alias(self):
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
'res_id': self.alias_record.id
|
||||
@@ -134,7 +206,7 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_document_no_alias(self):
|
||||
def test_mail_message_values_fromto_document_no_alias(self):
|
||||
test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
|
||||
msg = self.Message.create({
|
||||
@@ -148,7 +220,7 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
@mute_logger('flectra.models.unlink')
|
||||
def test_mail_message_values_document_manual_alias(self):
|
||||
def test_mail_message_values_fromto_document_manual_alias(self):
|
||||
test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
alias = self.env['mail.alias'].create({
|
||||
'alias_name': 'MegaLias',
|
||||
@@ -169,7 +241,7 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
|
||||
|
||||
def test_mail_message_values_no_auto_thread(self):
|
||||
def test_mail_message_values_fromto_no_auto_thread(self):
|
||||
msg = self.Message.create({
|
||||
'model': 'mail.test.container',
|
||||
'res_id': self.alias_record.id,
|
||||
@@ -179,15 +251,6 @@ class TestMessageValues(TestMailCommon):
|
||||
self.assertNotIn('mail.test.container', msg.message_id.split('@')[0])
|
||||
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
|
||||
def test_mail_message_base64_image(self):
|
||||
msg = self.env['mail.message'].with_user(self.user_employee).create({
|
||||
'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
|
||||
})
|
||||
self.assertEqual(len(msg.attachment_ids), 1)
|
||||
body = '<p>taratata <img src="/web/image/%s?access_token=%s" alt="image0" width="2"> <img src="/web/image/%s?access_token=%s" alt="image0" width="2"></p>'
|
||||
body = body % (msg.attachment_ids[0].id, msg.attachment_ids[0].access_token, msg.attachment_ids[0].id, msg.attachment_ids[0].access_token)
|
||||
self.assertEqual(msg.body, body)
|
||||
|
||||
|
||||
class TestMessageAccess(TestMailCommon):
|
||||
|
||||
|
||||
Reference in new issue
Block a user