[PATCH] Upstream patch - 17042022

This commit is contained in:
Parthiv Patel committed 2022-04-17 08:34:35 +00:00
1 parent a18957625a
commit 2b8cb28de2
25 files changed
+1015 -132

No files matched your search

+12 -2
View File
@@ -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,
+1 -1
View File
@@ -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 = ""
+1 -1
View File
@@ -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)]
+34 -4
View File
@@ -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
# ------------------------------------------------------------
+2 -1
View File
@@ -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
+21 -7
View File
@@ -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):
+80
View File
@@ -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')
+80
View File
@@ -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)
+1
View File
@@ -4,3 +4,4 @@ from . import mrp_workcenter
from . import mrp_production
from . import product
from . import stock_move
from . import account_move
+26
View File
@@ -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
+12 -2
View File
@@ -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;
});
+21
View File
@@ -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;
}
});
});
+31 -21
View File
@@ -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>
+1 -1
View File
@@ -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},
])
+2 -1
View File
@@ -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")
+50 -73
View File
@@ -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
+1
View File
@@ -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
+283
View File
@@ -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)
+2 -2
View File
@@ -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>
+79 -16
View File
@@ -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):