[PATCH] Upstream patch - 04112022

This commit is contained in:
Parthiv Patel
2022-11-04 08:35:29 +00:00
parent 94ed14609e
commit 0dc0d5c725
17 changed files with 230 additions and 57 deletions

View File

@@ -221,7 +221,7 @@ class AccountPayment(models.Model):
def prepare_vals(invoice, partials):
number = ' - '.join([invoice.name, invoice.ref] if invoice.ref else [invoice.name])
if invoice.is_outbound():
if invoice.is_outbound() or invoice.move_type == 'entry':
invoice_sign = 1
partial_field = 'debit_amount_currency'
else:
@@ -242,10 +242,11 @@ class AccountPayment(models.Model):
'currency': invoice.currency_id,
}
# Decode the reconciliation to keep only invoices.
# Decode the reconciliation to keep only bills.
term_lines = self.line_ids.filtered(lambda line: line.account_id.internal_type in ('receivable', 'payable'))
invoices = (term_lines.matched_debit_ids.debit_move_id.move_id + term_lines.matched_credit_ids.credit_move_id.move_id)\
.filtered(lambda x: x.is_outbound())
invoices = (term_lines.matched_debit_ids.debit_move_id.move_id + term_lines.matched_credit_ids.credit_move_id.move_id) \
.filtered(lambda move: move.is_outbound() or move.move_type == 'entry')
invoices = invoices.sorted(lambda x: x.invoice_date_due or x.date)
# Group partials by invoices.
@@ -272,7 +273,7 @@ class AccountPayment(models.Model):
else:
stub_lines = [prepare_vals(invoice, partials)
for invoice, partials in invoice_map.items()
if invoice.move_type == 'in_invoice']
if invoice.move_type in ('in_invoice', 'entry')]
# Crop the stub lines or split them on multiple pages
if not self.company_id.account_check_printing_multi_stub:

View File

@@ -490,9 +490,13 @@ class AccountEdiFormat(models.Model):
res = False
try:
if file_data['type'] == 'xml':
res = edi_format._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice)
res = edi_format\
.with_context(default_move_type=invoice.move_type)\
._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice)
elif file_data['type'] == 'pdf':
res = edi_format._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice)
res = edi_format\
.with_context(default_move_type=invoice.move_type)\
._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice)
file_data['pdf_reader'].stream.close()
else: # file_data['type'] == 'binary'
res = edi_format._update_invoice_from_binary(file_data['filename'], file_data['content'], file_data['extension'], invoice)

View File

@@ -2,6 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.addons.hr_expense.tests.common import TestExpenseCommon
from flectra.tests import tagged, Form
from flectra.tools.misc import formatLang
from flectra import fields
from flectra.exceptions import UserError
@@ -296,3 +297,56 @@ class TestExpenses(TestExpenseCommon):
move = self.env['account.payment'].browse(action['res_id']).move_id
move.button_cancel()
self.assertEqual(sheet.state, 'cancel', 'Sheet state must be cancel when the payment linked to that sheet is canceled')
def test_print_expense_check(self):
"""
Test the check content when printing a check
that comes from an expense
"""
sheet = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
(0, 0, {
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'employee_id': self.expense_employee.id,
}),
(0, 0, {
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1.0,
'employee_id': self.expense_employee.id,
}),
],
})
#actions
sheet.action_submit_sheet()
sheet.approve_expense_sheets()
sheet.action_sheet_move_create()
action_data = sheet.action_register_payment()
payment_method = self.env.company.bank_journal_ids.outbound_payment_method_ids.filtered(lambda m: m.code == 'check_printing')
with Form(self.env[action_data['res_model']].with_context(action_data['context'])) as wiz_form:
wiz_form.payment_method_id = payment_method
wizard = wiz_form.save()
action = wizard.action_create_payments()
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
payment = self.env[action['res_model']].browse(action['res_id'])
pages = payment._check_get_pages()
stub_line = pages[0]['stub_lines'][:1]
self.assertTrue(stub_line)
move = self.env[action_data['context']['active_model']].browse(action_data['context']['active_ids'])
self.assertDictEqual(stub_line[0], {
'due_date': '',
'number': ' - '.join([move.name, move.ref] if move.ref else [move.name]),
'amount_total': formatLang(self.env, 11.0, currency_obj=self.env.company.currency_id),
'amount_residual': '-',
'amount_paid': formatLang(self.env, 11.0, currency_obj=self.env.company.currency_id),
'currency': self.env.company.currency_id
})

View File

@@ -229,6 +229,9 @@ class AccountEdiFormat(models.Model):
if (not partner.country_id or partner.country_id.code == 'ES') and partner.vat:
# ES partner with VAT.
partner_info['NIF'] = partner.vat[2:] if partner.vat.startswith('ES') else partner.vat
if self.env.context.get('error_1117'):
partner_info['IDOtro'] = {'IDType': '07', 'ID': IDOtro_ID}
elif partner.country_id.code in eu_country_codes and partner.vat:
# European partner.
partner_info['IDOtro'] = {'IDType': '02', 'ID': IDOtro_ID}
@@ -562,6 +565,8 @@ class AccountEdiFormat(models.Model):
results[inv] = {'success': True}
inv.message_post(body=_("We saw that this invoice was sent correctly before, but we did not treat "
"the response. Make sure it is not because of a wrong configuration."))
elif respl.CodigoErrorRegistro == 1117 and not self.env.context.get('error_1117'):
return self.with_context(error_1117=True)._post_invoice_edi(invoices)
else:
results[inv] = {
'error': _("[%s] %s", respl.CodigoErrorRegistro, respl.DescripcionErrorRegistro),

View File

@@ -54,7 +54,7 @@ class TestAccountFrFec(AccountTestInvoicingCommon):
"INV|Customer Invoices|INV/2021/05/0001|20210502|701100|Ventes de produits finis (ou groupe) A|||-|20210502|Hello Darkness|0,00| 000000000001437,12|||20210502|-000000000001437,12|EUR\r\n"
"INV|Customer Invoices|INV/2021/05/0001|20210502|701100|Ventes de produits finis (ou groupe) A|||-|20210502|my old friend|0,00| 000000000001676,64|||20210502|-000000000001676,64|EUR\r\n"
"INV|Customer Invoices|INV/2021/05/0001|20210502|701100|Ventes de produits finis (ou groupe) A|||-|20210502|/|0,00| 000000000003353,28|||20210502|-000000000003353,28|EUR\r\n"
"INV|Customer Invoices|INV/2021/05/0001|20210502|445710|TVA collectée|||-|20210502|TVA collectée (vente) 20,0%|0,00| 000000000001293,41|||20210502|-000000000001293,41|EUR\r\n"
"INV|Customer Invoices|INV/2021/05/0001|20210502|445710|TVA collectée|||-|20210502|TVA 20,0%|0,00| 000000000001293,41|||20210502|-000000000001293,41|EUR\r\n"
f"INV|Customer Invoices|INV/2021/05/0001|20210502|411100|Clients - Ventes de biens ou de prestations de services|{self.partner_a.id}|partner_a|-|20210502|INV/2021/05/0001| 000000000007760,45|0,00|||20210502| 000000000007760,45|EUR"
)
content = base64.b64decode(self.wizard.fec_data).decode()

View File

@@ -279,7 +279,7 @@ class AccountMove(models.Model):
'document_total': document_total,
'representative': company.l10n_it_tax_representative_partner_id,
'codice_destinatario': codice_destinatario,
'regime_fiscale': company.l10n_it_tax_system if not is_self_invoice else 'RF01',
'regime_fiscale': company.l10n_it_tax_system if not is_self_invoice else 'RF18',
'is_self_invoice': is_self_invoice,
'partner_bank': self.partner_bank_id,
'format_date': format_date,

View File

@@ -2,4 +2,4 @@ id,name
tax_group_0,TAX 0%
tax_group_gst_15,GST 15%
tax_group_15,TAX 15%
tax_group_100000000,GST 100000000%
tax_group_100000000,GST 100%
1 id name
2 tax_group_0 TAX 0%
3 tax_group_gst_15 GST 15%
4 tax_group_15 TAX 15%
5 tax_group_100000000 GST 100000000% GST 100%

View File

@@ -259,21 +259,12 @@
</record>
<record id="nz_tax_purchase_gst_only" model="account.tax.template">
<field name="chart_template_id" ref="l10n_nz_chart_template"/>
<field name="name">GST Only Imports</field>
<field name="name">GST Only - Imports</field>
<field name="sequence">5</field>
<field name="description">GST Only on Imports</field>
<field name="type_tax_use">purchase</field>
<field name="amount_type">percent</field>
<field name="amount">100000000000</field>
<!--
The tax percentage is so high because on imported goods we
needed to link the tax line acknowledgment (not to be paid)
on the customer invoice and what need to actually be
paid from another invoice given by a clearance house
(i.e. customs)
For more info see the complete discussion below
https://github.com/flectra/flectra/pull/48700#issuecomment-607586417
-->
<field name="amount_type">division</field>
<field name="amount">100</field>
<field name="price_include">TRUE</field>
<field name="tax_group_id" ref="tax_group_100000000"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),

View File

@@ -12,6 +12,7 @@ from flectra.osv import expression
from flectra.tools import ormcache, formataddr
from flectra.exceptions import AccessError
from flectra.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
from flectra.tools import html_escape
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
_logger = logging.getLogger(__name__)
@@ -1114,13 +1115,13 @@ class Channel(models.Model):
def _execute_command_help(self, **kwargs):
partner = self.env.user.partner_id
if self.channel_type == 'channel':
msg = _("You are in channel <b>#%s</b>.", self.name)
msg = _("You are in channel <b>#%s</b>.", html_escape(self.name))
if self.public == 'private':
msg += _(" This channel is private. People must be invited to join it.")
else:
all_channel_partners = self.env['mail.channel.partner'].with_context(active_test=False)
channel_partners = all_channel_partners.search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
msg = _("You are in a private conversation with <b>@%s</b>.", channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
msg = _("You are in a private conversation with <b>@%s</b>.", _(" @").join(html_escape(member.partner_id.name) for member in channel_partners) if channel_partners else _('Anonymous'))
msg += _("""<br><br>
Type <b>@username</b> to mention someone, and grab his attention.<br>
Type <b>#channel</b> to mention a channel.<br>

View File

@@ -138,26 +138,13 @@ class ComposerSuggestedRecipient extends Component {
/**
* @private
* @param {object} record the newly-created record
*/
_onDialogSaved(record) {
_onDialogSaved() {
const thread = this.suggestedRecipientInfo && this.suggestedRecipientInfo.thread;
if (!thread) {
return;
}
thread.fetchAndUpdateSuggestedRecipients();
if (!this.suggestedRecipientInfo.partner) {
this.env.services.notification.notify({
title: this.env._t('Invalid Partner'),
message: this.env._t('The information you have entered does not match the existing contact information for this record. The partner was not created.'),
type: 'warning'
});
this.env.services.rpc({
args: [record.res_id],
model: 'res.partner',
method: 'unlink',
});
}
}
}

View File

@@ -209,7 +209,7 @@ class MrpBom(models.Model):
if not products:
return bom_by_product
product_templates = products.mapped('product_tmpl_id')
domain = ['|', ('product_id', 'in', products.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', product_templates.ids)]
domain = ['|', ('product_id', 'in', products.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', product_templates.ids), ('active', '=', True)]
if picking_type:
domain += ['|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False)]
if company_id or self.env.context.get('company_id'):
@@ -453,11 +453,11 @@ class MrpBomLine(models.Model):
self.ensure_one()
if product._name == 'product.template':
return False
if self.bom_product_template_attribute_value_ids:
for ptal, iter_ptav in groupby(self.bom_product_template_attribute_value_ids.sorted('attribute_line_id'), lambda ptav: ptav.attribute_line_id):
if not any(ptav in product.product_template_attribute_value_ids for ptav in iter_ptav):
return True
return False
# The intersection of the values of the product and those of the line satisfy:
# * the number of items equals the number of attributes (since a product cannot
# have multiple values for the same attribute),
# * the attributes are a subset of the attributes of the line.
return len(product.product_template_attribute_value_ids & self.bom_product_template_attribute_value_ids) != len(self.bom_product_template_attribute_value_ids.attribute_id)
def action_see_attachments(self):
domain = [

View File

@@ -593,6 +593,8 @@ class MrpWorkorder(models.Model):
else:
if self.date_planned_start > start_date:
vals['date_planned_start'] = start_date
if self.duration_expected:
vals['date_planned_finished'] = self._calculate_date_planned_finished(start_date)
if self.date_planned_finished and self.date_planned_finished < start_date:
vals['date_planned_finished'] = start_date
return self.with_context(bypass_duration_calculation=True).write(vals)

View File

@@ -25,6 +25,8 @@ class TestStockValuation(TransactionCase):
'name': 'Large Desk',
'standard_price': 1299.0,
'list_price': 1799.0,
# Ignore tax calculations for these tests.
'supplier_taxes_id': False,
'type': 'product',
})
Account = self.env['account.account']

View File

@@ -406,24 +406,43 @@ Dialog.alert = function (owner, message, options) {
// static method to open simple confirm dialog
Dialog.confirm = function (owner, message, options) {
let clickProm;
/**
* Creates an improved callback from the given callback value at the given
* key from the parent function's options parameter. This is improved to:
*
* - Prevent calling given callbacks once one has been called.
*
* - Re-allow calling callbacks once a previous callback call's returned
* Promise is rejected.
*/
let isBlocked = false;
function makeCallback(key) {
const callback = options && options[key];
return function () {
if (isBlocked) {
// Do not (re)call any callback and return a rejected Promise
// to prevent closing the Dialog.
return Promise.reject();
}
isBlocked = true;
const callbackRes = callback && callback.apply(this, arguments);
Promise.resolve(callbackRes).guardedCatch(() => {
isBlocked = false;
});
return callbackRes;
};
}
var buttons = [
{
text: _t("Ok"),
classes: 'btn-primary',
close: true,
click: options && options.confirm_callback && (() => {
clickProm = clickProm || options.confirm_callback() || Promise.resolve();
return clickProm;
}),
click: makeCallback('confirm_callback'),
},
{
text: _t("Cancel"),
close: true,
click: options && options.cancel_callback && (() => {
clickProm = clickProm || options.cancel_callback() || Promise.resolve();
return clickProm;
}),
click: makeCallback('cancel_callback'),
}
];
return new Dialog(owner, _.extend({

View File

@@ -104,7 +104,7 @@ QUnit.module('core', {}, function () {
});
QUnit.test("click twice on 'Ok' button of a confirm dialog", async function (assert) {
assert.expect(3);
assert.expect(5);
var testPromise = testUtils.makeTestPromise();
var parent = await createEmptyParent();
@@ -121,7 +121,12 @@ QUnit.module('core', {}, function () {
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
await testUtils.nextTick();
assert.verifySteps(['confirm']);
assert.ok($('.modal[role="dialog"]').hasClass('show'), "Should still be opened");
testPromise.resolve();
await testUtils.nextTick();
assert.notOk($('.modal[role="dialog"]').hasClass('show'), "Should now be closed");
parent.destroy();
});
@@ -150,6 +155,107 @@ QUnit.module('core', {}, function () {
parent.destroy();
});
QUnit.test("click on 'Cancel' and then 'Ok' in a confirm dialog (no cancel callback)", async function (assert) {
assert.expect(2);
var parent = await createEmptyParent();
var options = {
confirm_callback: () => {
throw new Error("should not be called");
},
// Cannot add a step in cancel_callback, that's the point of this
// test, we'll rely on checking the Dialog is opened then closed
// without a crash.
};
Dialog.confirm(parent, "", options);
await testUtils.nextTick();
assert.ok($('.modal[role="dialog"]').hasClass('show'));
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
await testUtils.nextTick();
assert.notOk($('.modal[role="dialog"]').hasClass('show'));
parent.destroy();
});
QUnit.test("Confirm dialog callbacks properly handle rejections", async function (assert) {
assert.expect(5);
var parent = await createEmptyParent();
var options = {
confirm_callback: () => {
assert.step("confirm");
return Promise.reject();
},
cancel_callback: () => {
assert.step("cancel");
return $.Deferred().reject(); // Test jquery deferred too
}
};
Dialog.confirm(parent, "", options);
await testUtils.nextTick();
assert.verifySteps([]);
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
await testUtils.nextTick();
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
await testUtils.nextTick();
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
assert.verifySteps(['cancel', 'confirm', 'cancel']);
parent.destroy();
});
QUnit.test("Properly can rely on the this in confirm and cancel callbacks of confirm dialog", async function (assert) {
assert.expect(2);
let dialogInstance = null;
var parent = await createEmptyParent();
var options = {
confirm_callback: function () {
assert.equal(this, dialogInstance, "'this' is properly a reference to the dialog instance");
return Promise.reject();
},
cancel_callback: function () {
assert.equal(this, dialogInstance, "'this' is properly a reference to the dialog instance");
return Promise.reject();
}
};
dialogInstance = Dialog.confirm(parent, "", options);
await testUtils.nextTick();
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
await testUtils.nextTick();
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
parent.destroy();
});
QUnit.test("Confirm dialog callbacks can return anything without crash", async function (assert) {
assert.expect(3);
// Note that this test could be removed in master if the related code
// is reworked. This only prevents a stable fix to break this again by
// relying on the fact what is returned by those callbacks are undefined
// or promises.
var parent = await createEmptyParent();
var options = {
confirm_callback: () => {
assert.step("confirm");
return 5;
},
};
Dialog.confirm(parent, "", options);
await testUtils.nextTick();
assert.verifySteps([]);
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
assert.verifySteps(['confirm']);
parent.destroy();
});
QUnit.test("Closing alert dialog without using buttons calls confirm callback", async function (assert) {
assert.expect(3);

View File

@@ -1119,7 +1119,7 @@ function _clonePage(pageId) {
method: 'clone_page',
args: [
pageId,
this.$content.find('#page_name').val(),
this.$('#page_name').val(),
],
}).then(function (path) {
window.location.href = path;

View File

@@ -420,9 +420,10 @@ var MetaTitleDescription = Widget.extend({
_seoNameChanged: function () {
var self = this;
// don't use _, because we need to keep trailing whitespace during edition
const slugified = this.$seoName.val().toString().toLowerCase()
const slugified = this.$seoName.val().toString().trim().normalize('NFKD').toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '-') // Remove all non-word chars
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-$/g, '') // Remove trailing -
.replace(/\-\-+/g, '-'); // Replace multiple - with single -
this.$seoName.val(slugified);
self._renderPreview();