[PATCH] Upstream patch - 12062022

This commit is contained in:
Parthiv Patel
2022-06-12 08:34:23 +00:00
parent 621bc0a14f
commit e8f7e22283
34 changed files with 256 additions and 83 deletions
@@ -462,20 +462,14 @@ class AccountReconcileModel(models.Model):
return lines_vals_list + writeoff_vals_list
def _prepare_widget_writeoff_vals(self, st_line_id, write_off_vals):
fixed_write_off_vals = dict(write_off_vals, currency_id=st_line_id.company_id.currency_id.id)
counterpart_vals = st_line_id._prepare_counterpart_move_line_vals(fixed_write_off_vals)
counterpart_vals = st_line_id._prepare_counterpart_move_line_vals({
**write_off_vals,
'currency_id': st_line_id.company_id.currency_id.id,
})
return {
'name': counterpart_vals['name'],
**counterpart_vals,
'balance': counterpart_vals['amount_currency'],
'debit': counterpart_vals['debit'],
'credit': counterpart_vals['credit'],
'account_id': counterpart_vals['account_id'],
'currency_id': counterpart_vals['currency_id'],
'analytic_account_id': counterpart_vals.get('analytic_account_id'),
'analytic_tag_ids': counterpart_vals.get('analytic_tag_ids', []),
'reconcile_model_id': self.id,
'journal_id': counterpart_vals['journal_id']
}
####################################################
+4 -8
View File
@@ -53,11 +53,7 @@ class AccountTaxReport(models.Model):
copied_report = super(AccountTaxReport, self).copy(default=copy_default) #This copies the report without its lines
lines_map = {} # maps original lines to their copies (using ids)
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id))
while lines_to_treat:
line = lines_to_treat.pop()
lines_to_treat += list(line.children_line_ids)
for line in self.get_lines_in_hierarchy():
copy = line.copy({'parent_id': lines_map.get(line.parent_id.id, None), 'report_id': copied_report.id})
lines_map[line.id] = copy.id
@@ -68,10 +64,10 @@ class AccountTaxReport(models.Model):
ar all directly followed by their children.
"""
self.ensure_one()
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id).sorted(lambda x: x.sequence)) # Used as a stack, whose index 0 is the top
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id)) # Used as a stack, whose index 0 is the top
while lines_to_treat:
to_yield = lines_to_treat[0]
lines_to_treat = list(to_yield.children_line_ids.sorted(lambda x: x.sequence)) + lines_to_treat[1:]
lines_to_treat = list(to_yield.children_line_ids) + lines_to_treat[1:]
yield to_yield
def get_checks_to_perform(self, d):
@@ -93,7 +89,7 @@ class AccountTaxReport(models.Model):
class AccountTaxReportLine(models.Model):
_name = "account.tax.report.line"
_description = 'Account Tax Report Line'
_order = 'sequence'
_order = 'sequence, id'
_parent_store = True
name = fields.Char(string="Name", required=True, help="Complete name for this report line, to be used in report.")
+1 -1
View File
@@ -133,7 +133,7 @@ class HrEmployeeBase(models.AbstractModel):
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
employee.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id')
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft']
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') in ['validate']
@api.depends('parent_id')
def _compute_leave_manager(self):
+3 -1
View File
@@ -12,6 +12,7 @@ class User(models.Model):
allocation_used_count = fields.Float(related='employee_id.allocation_used_count')
allocation_count = fields.Float(related='employee_id.allocation_count')
leave_date_to = fields.Date(related='employee_id.leave_date_to')
current_leave_state = fields.Selection(related='employee_id.current_leave_state')
is_absent = fields.Boolean(related='employee_id.is_absent')
allocation_used_display = fields.Char(related='employee_id.allocation_used_display')
allocation_display = fields.Char(related='employee_id.allocation_display')
@@ -29,6 +30,7 @@ class User(models.Model):
'allocation_used_count',
'allocation_count',
'leave_date_to',
'current_leave_state',
'is_absent',
'allocation_used_display',
'allocation_display',
@@ -55,7 +57,7 @@ class User(models.Model):
field = 'partner_id' if partner else 'id'
self.env.cr.execute('''SELECT res_users.%s FROM res_users
JOIN hr_leave ON hr_leave.user_id = res_users.id
AND state not in ('cancel', 'refuse')
AND state in ('validate')
AND res_users.active = 't'
AND date_from <= %%s AND date_to >= %%s''' % field, (now, now))
return [r[0] for r in self.env.cr.fetchall()]
@@ -53,7 +53,10 @@ registerInstancePatchModel('mail.partner', 'hr_holidays/static/src/models/partne
if (currentDate.getFullYear() !== date.getFullYear()) {
options.year = 'numeric';
}
const localeCode = this.env.messaging.locale.language.replace(/_/g,'-');
let localeCode = this.env.messaging.locale.language.replace(/_/g,'-');
if (localeCode == "sr@latin") {
localeCode = "sr-Latn-RS";
}
const formattedDate = date.toLocaleDateString(localeCode, options);
return _.str.sprintf(this.env._t("Out of office until %s"), formattedDate);
},
@@ -187,7 +187,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
@mute_logger('flectra.models.unlink', 'flectra.addons.mail.models.mail_mail')
def test_employee_is_absent(self):
""" Only the concerned employee should be considered absent """
self.env['hr.leave'].with_user(self.user_employee_id).create({
user_employee_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
@@ -196,6 +196,13 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'number_of_days': 2,
})
(self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch
self.assertFalse(self.employee_emp.is_absent, "He should not be considered absent")
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
user_employee_leave.sudo().write({
'state': 'validate',
})
(self.employee_emp | self.employee_hrmanager)._compute_leave_status()
self.assertTrue(self.employee_emp.is_absent, "He should be considered absent")
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
@@ -90,11 +90,13 @@ class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDe
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_user_leave_offline(self):
self.leave.write({'state': 'validate'})
with self.assertQueryCount(__system__=2, demo=2):
self.assertEqual(self.hr_user.im_status, 'leave_offline')
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_partner_leave_offline(self):
self.leave.write({'state': 'validate'})
with self.assertQueryCount(__system__=2, demo=2):
self.assertEqual(self.hr_partner.im_status, 'leave_offline')
+8 -4
View File
@@ -133,7 +133,7 @@ def unslug_url(s):
def url_lang(path_or_uri, lang_code=None):
''' Given a relative URL, make it absolute and add the required lang or
remove useless lang.
Nothing will be done for absolute URL.
Nothing will be done for absolute or invalid URL.
If there is only one language installed, the lang will not be handled
unless forced with `lang` parameter.
@@ -143,9 +143,13 @@ def url_lang(path_or_uri, lang_code=None):
Lang = request.env['res.lang']
location = pycompat.to_text(path_or_uri).strip()
force_lang = lang_code is not None
url = werkzeug.urls.url_parse(location)
try:
url = werkzeug.urls.url_parse(location)
except ValueError:
# e.g. Invalid IPv6 URL, `werkzeug.urls.url_parse('http://]')`
url = False
# relative URL with either a path or a force_lang
if not url.netloc and not url.scheme and (url.path or force_lang):
if url and not url.netloc and not url.scheme and (url.path or force_lang):
location = werkzeug.urls.url_join(request.httprequest.path, location)
lang_url_codes = [url_code for _, url_code, *_ in Lang.get_available()]
lang_code = pycompat.to_text(lang_code or request.context['lang'])
@@ -171,7 +175,7 @@ def url_lang(path_or_uri, lang_code=None):
def url_for(url_from, lang_code=None, no_rewrite=False):
''' Return the url with the rewriting applied.
Nothing will be done for absolute URL, or short URL from 1 char.
Nothing will be done for absolute URL, invalid URL, or short URL from 1 char.
:param url_from: The URL to convert.
:param lang_code: Must be the lang `code`. It could also be something
@@ -174,7 +174,7 @@
"account_common_396","Deterioro de valor de los subproductos, residuos y materiales recuperados","396","account.data_account_type_current_assets","l10n_es.account_chart_template_common","False"
"account_common_4000","Proveedores (euros)","4000","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
"account_common_4004","Proveedores (moneda extranjera)","4004","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
"account_common_4009","Proveedores, facturas pendientes de recibir o de formalizar","4009","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
"account_common_4009","Proveedores, facturas pendientes de recibir o de formalizar","4009","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","True"
"account_common_401","Proveedores, efectos comerciales a pagar","401","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
"account_common_4030","Proveedores, empresas del grupo (euros)","4030","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
"account_common_4031","Efectos comerciales a pagar, empresas del grupo","4031","account.data_account_type_payable","l10n_es.account_chart_template_common","True"
@@ -210,7 +210,7 @@
"account_common_435","Clientes, otras partes vinculadas","435","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_436","Clientes de dudoso cobro","436","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_437","Envases y embalajes a devolver por clientes","437","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_438","Anticipos de clientes","438","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_438","Anticipos de clientes","438","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","True"
"account_common_4400","Deudores (euros)","4400","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_4404","Deudores (moneda extranjera)","4404","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
"account_common_4409","Deudores, facturas pendientes de formalizar","4409","account.data_account_type_receivable","l10n_es.account_chart_template_common","True"
1 id name code user_type_id/id chart_template_id/id reconcile
174 account_common_396 Deterioro de valor de los subproductos, residuos y materiales recuperados 396 account.data_account_type_current_assets l10n_es.account_chart_template_common False
175 account_common_4000 Proveedores (euros) 4000 account.data_account_type_payable l10n_es.account_chart_template_common True
176 account_common_4004 Proveedores (moneda extranjera) 4004 account.data_account_type_payable l10n_es.account_chart_template_common True
177 account_common_4009 Proveedores, facturas pendientes de recibir o de formalizar 4009 account.data_account_type_payable account.data_account_type_current_liabilities l10n_es.account_chart_template_common True
178 account_common_401 Proveedores, efectos comerciales a pagar 401 account.data_account_type_payable l10n_es.account_chart_template_common True
179 account_common_4030 Proveedores, empresas del grupo (euros) 4030 account.data_account_type_payable l10n_es.account_chart_template_common True
180 account_common_4031 Efectos comerciales a pagar, empresas del grupo 4031 account.data_account_type_payable l10n_es.account_chart_template_common True
210 account_common_435 Clientes, otras partes vinculadas 435 account.data_account_type_receivable l10n_es.account_chart_template_common True
211 account_common_436 Clientes de dudoso cobro 436 account.data_account_type_receivable l10n_es.account_chart_template_common True
212 account_common_437 Envases y embalajes a devolver por clientes 437 account.data_account_type_receivable l10n_es.account_chart_template_common True
213 account_common_438 Anticipos de clientes 438 account.data_account_type_receivable account.data_account_type_current_liabilities l10n_es.account_chart_template_common True
214 account_common_4400 Deudores (euros) 4400 account.data_account_type_receivable l10n_es.account_chart_template_common True
215 account_common_4404 Deudores (moneda extranjera) 4404 account.data_account_type_receivable l10n_es.account_chart_template_common True
216 account_common_4409 Deudores, facturas pendientes de formalizar 4409 account.data_account_type_receivable l10n_es.account_chart_template_common True
+2 -2
View File
@@ -84,9 +84,9 @@ class FetchmailServer(models.Model):
# To leave the mail in the state in which they were.
if "Seen" not in data[1].decode("utf-8"):
imap_server.store(uid, '+FLAGS', '\\Seen')
imap_server.uid('STORE', uid, '+FLAGS', '(\\Seen)')
else:
imap_server.store(uid, '-FLAGS', '\\Seen')
imap_server.uid('STORE', uid, '-FLAGS', '(\\Seen)')
# See details in message_process() in mail_thread.py
if isinstance(message, xmlrpclib.Binary):
@@ -128,8 +128,9 @@ class Activity extends Component {
* @param {Object} ev.detail
* @param {mail.attachment} ev.detail.attachment
*/
_onAttachmentCreated(ev) {
this.activity.markAsDone({ attachments: [ev.detail.attachment] });
async _onAttachmentCreated(ev) {
await this.activity.markAsDone({ attachments: [ev.detail.attachment] });
this.trigger('o-attachments-changed');
}
/**
@@ -735,9 +735,7 @@ QUnit.test('basic rendering of canceled notification', async function (assert) {
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "thread become loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -968,9 +966,7 @@ QUnit.test("delete all attachments of message without content should no longer d
// wait for messages of the thread to be loaded
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "thread become loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1026,9 +1022,7 @@ QUnit.test('delete all attachments of a message with some text content should st
// wait for messages of the thread to be loaded
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "thread become loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1091,9 +1085,7 @@ QUnit.test('delete all attachments of a message with tracking fields should stil
// wait for messages of the thread to be loaded
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "thread become loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1527,9 +1519,7 @@ QUnit.test('show empty placeholder when thread contains no message', async funct
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "should wait until thread becomes loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1571,9 +1561,7 @@ QUnit.test('show empty placeholder when thread contains only empty messages', as
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "thread become loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1620,9 +1608,7 @@ QUnit.test('message with subtype should be displayed (and not considered as empt
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView);
},
func: () => this.createThreadViewComponent(threadViewer.threadView),
message: "should wait until thread becomes loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -1675,9 +1661,7 @@ QUnit.test('[technical] message list with a full page of empty messages should s
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true });
},
func: () => this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true }),
message: "should wait until thread becomes loaded with messages",
predicate: ({ hint, threadViewer }) => {
return (
@@ -343,7 +343,7 @@ QUnit.test('basic chatter rendering without followers', async function (assert)
assert.containsNone(
document.body,
'.o_FollowerListMenu',
"there should be no followers menu"
"there should be no followers menu because the 'message_follower_ids' field is not present in 'oe_chatter'"
);
assert.containsOnce(
document.body,
@@ -393,7 +393,7 @@ QUnit.test('basic chatter rendering without activities', async function (assert)
assert.containsNone(
document.body,
'.o_ChatterTopbar_buttonScheduleActivity',
"there should be a schedule activity button"
"there should be no schedule activity button because the 'activity_ids' field is not present in 'oe_chatter'"
);
assert.containsOnce(
document.body,
@@ -458,7 +458,7 @@ QUnit.test('basic chatter rendering without messages', async function (assert) {
assert.containsNone(
document.body,
'.o_Chatter_thread',
"there should be a thread"
"there should be no thread because the 'message_ids' field is not present in 'oe_chatter'"
);
});
@@ -1721,7 +1721,7 @@ MockServer.include({
['partner_id', 'in', partner_ids || []],
['channel_id', 'in', channel_ids || []],
]);
this._mockUnlink(model, [followers.map(follower => follower.id)]);
this._mockUnlink('mail.followers', [followers.map(follower => follower.id)]);
},
/**
* Simulates `get_mention_suggestions` on `res.partner`.
@@ -38,8 +38,8 @@ class TestMicrosoftService(TransactionCase):
self.call_without_sync_token = call(
"/v1.0/me/calendarView/delta",
{
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
},
{**self.header, 'Prefer': self.header_prefer},
method="GET", timeout=DEFAULT_TIMEOUT,
@@ -224,8 +224,8 @@ class TestMicrosoftService(TransactionCase):
mock_do_request.assert_called_with(
"/v1.0/me/events/123/instances",
{
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
},
{**self.header, 'Prefer': self.header_prefer},
method='GET', timeout=DEFAULT_TIMEOUT,
@@ -57,8 +57,8 @@ class MicrosoftCalendarService():
}
if not params:
params = {
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=3).strftime("%Y-%m-%dT00:00:00Z"),
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
'endDateTime': fields.Datetime.add(fields.Datetime.now(), years=2).strftime("%Y-%m-%dT00:00:00Z"),
}
# get the first page of events
+1 -1
View File
@@ -258,7 +258,7 @@ class PosOrder(models.Model):
)
payment_ids = fields.One2many('pos.payment', 'pos_order_id', string='Payments', readonly=True)
session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False)
to_invoice = fields.Boolean('To invoice')
to_invoice = fields.Boolean('To invoice', copy=False)
is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced')
is_tipped = fields.Boolean('Is this already tipped?', readonly=True)
tip_amount = fields.Float(string='Tip Amount', digits=0, readonly=True)
@@ -2481,6 +2481,7 @@ td {
font-weight: normal;
font-size: 18px;
margin: 16px;
white-space: pre-line;
}
.pos .popup-lg .body {
@@ -1028,3 +1028,53 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# check the difference line
diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session')
self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0")
def test_order_refund_picking(self):
self.pos_config.open_session_cb(check_coa=False)
current_session = self.pos_config.current_session_id
current_session.update_stock_at_closing = True
# I create a new PoS order with 1 line
order = self.PosOrder.create({
'company_id': self.env.company.id,
'session_id': current_session.id,
'partner_id': self.partner1.id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'lines': [(0, 0, {
'name': "OL/0001",
'product_id': self.product3.id,
'price_unit': 450,
'discount': 5.0,
'qty': 2.0,
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
'price_subtotal': 450 * (1 - 5/100.0) * 2,
'price_subtotal_incl': 450 * (1 - 5/100.0) * 2,
})],
'amount_total': 1710.0,
'amount_tax': 0.0,
'amount_paid': 0.0,
'amount_return': 0.0,
'to_invoice': True
})
payment_context = {"active_ids": order.ids, "active_id": order.id}
order_payment = self.PosMakePayment.with_context(**payment_context).create({
'amount': order.amount_total,
'payment_method_id': self.cash_payment_method.id
})
order_payment.with_context(**payment_context).check()
# I create a refund
refund_action = order.refund()
refund = self.PosOrder.browse(refund_action['res_id'])
payment_context = {"active_ids": refund.ids, "active_id": refund.id}
refund_payment = self.PosMakePayment.with_context(**payment_context).create({
'amount': refund.amount_total,
'payment_method_id': self.cash_payment_method.id,
})
# I click on the validate button to register the payment.
refund_payment.with_context(**payment_context).check()
refund.action_pos_order_invoice()
self.assertEqual(refund.picking_count, 1)
@@ -24,7 +24,11 @@ var EpsonPrinter = core.Class.extend(PrinterMixin, {
} else {
Gui.showPopup('ErrorPopup', {
'title': _t('Connection to the printer failed'),
'body': _t('Please check if the printer is still connected, if the configured IP address is correct and if your printer supports the ePOS protocol.'),
'body': _t('Please check if the printer is still connected, if the configured IP address is correct and if your printer supports the ePOS protocol. \n' +
'Some browsers don\'t allow HTTP calls from websites to devices in the network (for security reasons). ' +
'If it is the case, you will need to follow Flectra\'s documentation for ' +
'\'Self-signed certificate for ePOS printers\' and \'Secure connection (HTTPS)\' to solve the issue'
),
});
}
},
+1 -1
View File
@@ -547,7 +547,7 @@ class PurchaseOrder(models.Model):
raise UserError(_('Please define an accounting purchase journal for the company %s (%s).') % (self.company_id.name, self.company_id.id))
partner_invoice_id = self.partner_id.address_get(['invoice'])['invoice']
partner_bank_id = self.partner_id.bank_ids.filtered_domain(['|', ('company_id', '=', False), ('company_id', '=', self.company_id.id)])[:1]
partner_bank_id = self.partner_id.commercial_partner_id.bank_ids.filtered_domain(['|', ('company_id', '=', False), ('company_id', '=', self.company_id.id)])[:1]
invoice_vals = {
'ref': self.partner_ref or '',
'move_type': move_type,
+9 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from datetime import timedelta, time
from flectra import api, fields, models
from flectra import api, fields, models, _
from flectra.tools.float_utils import float_round
@@ -34,6 +34,14 @@ class ProductProduct(models.Model):
product.sales_count = float_round(r.get(product.id, 0), precision_rounding=product.uom_id.rounding)
return r
@api.onchange('type')
def _onchange_type(self):
if self._origin and self.sales_count > 0:
return {'warning': {
'title': _("Warning"),
'message': _("You cannot change the product's type because it is already used in sales orders.")
}}
def action_view_sales(self):
action = self.env["ir.actions.actions"]._for_xml_id("sale.report_all_channels_sales_action")
action['domain'] = [('product_id', 'in', self.ids)]
+5
View File
@@ -137,6 +137,11 @@ class ProductTemplate(models.Model):
if not self.invoice_policy:
self.invoice_policy = 'order'
self.service_type = 'manual'
if self._origin and self.sales_count > 0:
res['warning'] = {
'title': _("Warning"),
'message': _("You cannot change the product's type because it is already used in sales orders.")
}
return res
@api.model
+2
View File
@@ -1138,6 +1138,7 @@ Reason(s) of this behavior could be:
:param optional_values: any parameter that should be added to the returned down payment section
"""
context = {'lang': self.partner_id.lang}
down_payments_section_line = {
'display_type': 'line_section',
'name': _('Down Payments'),
@@ -1148,6 +1149,7 @@ Reason(s) of this behavior could be:
'price_unit': 0,
'account_id': False
}
del context
if optional_values:
down_payments_section_line.update(optional_values)
return down_payments_section_line
+20
View File
@@ -66,6 +66,9 @@ class ProductTemplate(models.Model):
self.service_policy = 'ordered_timesheet'
elif self.type == 'consu' and not self.invoice_policy and self.service_policy == 'ordered_timesheet':
self.invoice_policy = 'order'
if self.type != 'service':
self.service_tracking = 'no'
return res
@api.model
@@ -100,6 +103,11 @@ class ProductTemplate(models.Model):
time_product = self.env.ref('sale_timesheet.time_product')
if time_product.product_tmpl_id in self:
raise ValidationError(_('The %s product is required by the Timesheet app and cannot be archived/deleted.') % time_product.name)
if 'type' in vals and vals['type'] != 'service':
vals.update({
'service_tracking': 'no',
'project_id': False
})
return super(ProductTemplate, self).write(vals)
@@ -120,6 +128,13 @@ class ProductProduct(models.Model):
if vals:
self.update(vals)
@api.onchange('type')
def _onchange_type(self):
res = super(ProductProduct, self)._onchange_type()
if self.type != 'service':
self.service_tracking = 'no'
return res
def unlink(self):
time_product = self.env.ref('sale_timesheet.time_product')
if time_product in self:
@@ -133,4 +148,9 @@ class ProductProduct(models.Model):
time_product = self.env.ref('sale_timesheet.time_product')
if time_product in self:
raise ValidationError(_('The %s product is required by the Timesheet app and cannot be archived/deleted.') % time_product.name)
if 'type' in vals and vals['type'] != 'service':
vals.update({
'service_tracking': 'no',
'project_id': False
})
return super(ProductProduct, self).write(vals)
@@ -109,6 +109,7 @@ const ReplenishReport = clientAction.extend({
modelName: model,
domain: this._getReportDomain(),
hasActionMenus: false,
context: {fill_temporal: false},
};
const graphView = new GraphView(viewInfo, params);
return graphView.getController(this);
+3 -2
View File
@@ -3,7 +3,7 @@
from flectra import api, fields, models, _
from flectra.exceptions import UserError
from flectra.tools import float_is_zero, float_repr, float_compare
from flectra.tools import float_is_zero, float_repr, float_round, float_compare
from flectra.exceptions import ValidationError
from collections import defaultdict
@@ -227,7 +227,8 @@ class ProductProduct(models.Model):
quantity_svl = product.sudo().quantity_svl
if float_compare(quantity_svl, 0.0, precision_rounding=product.uom_id.rounding) <= 0:
continue
rounded_new_price = company_id.currency_id.round(new_price)
digits = self.env['decimal.precision'].precision_get('Product Price')
rounded_new_price = float_round(new_price, precision_digits=digits)
diff = rounded_new_price - product.standard_price
value = company_id.currency_id.round(quantity_svl * diff)
if company_id.currency_id.is_zero(value):
@@ -16,14 +16,20 @@ class ReplenishmentReport(models.AbstractModel):
domain = self._product_domain(product_template_ids, product_variant_ids)
company = self.env['stock.location'].browse(wh_location_ids).mapped('company_id')
svl = self.env['stock.valuation.layer'].search(domain + [('company_id', '=', company.id)])
domain_quants = [
('company_id', '=', company.id),
('location_id', 'in', wh_location_ids)
]
if product_template_ids:
domain_quants += [('product_id.product_tmpl_id', 'in', product_template_ids)]
else:
domain_quants += [('product_id', 'in', product_variant_ids)]
quants = self.env['stock.quant'].search(domain_quants)
currency = svl.currency_id or self.env.company.currency_id
total_quantity = sum(svl.mapped('quantity'))
# Because we can have negative quantities, `total_quantity` may be equal to zero even if the warehouse's `quantity` is positive.
if svl and not float_is_zero(total_quantity, precision_rounding=svl.product_id.uom_id.rounding):
def filter_on_locations(layer):
return layer.stock_move_id.location_dest_id.id in wh_location_ids or layer.stock_move_id.location_id.id in wh_location_ids
quantity = sum(svl.filtered(filter_on_locations).mapped('quantity'))
value = sum(svl.mapped('value')) * (quantity / total_quantity)
value = sum(svl.mapped('value')) * (sum(quants.mapped('quantity')) / total_quantity)
else:
value = 0
value = float_repr(value, precision_digits=currency.decimal_places)
@@ -158,6 +158,38 @@ class TestStockValuationLayerRevaluation(TestStockValuationCommon):
self.assertEqual(layers[0].value, 200)
self.assertEqual(layers[1].value, 300)
def test_stock_valuation_layer_revaluation_avco_rounding_5_digits(self):
"""
Check that the rounding of the new price (cost) is equivalent to the rounding of the standard price (cost)
The check is done indirectly via the layers valuations.
If correct => rounding method is correct too
"""
self.product1.categ_id.property_cost_method = 'average'
self.env['decimal.precision'].search([
('name', '=', 'Product Price'),
]).digits = 5
# First Move
self.product1.write({'standard_price': 0.00875})
self._make_in_move(self.product1, 10000)
self.assertEqual(self.product1.standard_price, 0.00875)
self.assertEqual(self.product1.quantity_svl, 10000)
layer = self.product1.stock_valuation_layer_ids
self.assertEqual(layer.value, 87.5)
# Second Move
self.product1.write({'standard_price': 0.00975})
self.assertEqual(self.product1.standard_price, 0.00975)
self.assertEqual(self.product1.quantity_svl, 10000)
layers = self.product1.stock_valuation_layer_ids
self.assertEqual(layers[0].value, 87.5)
self.assertEqual(layers[1].value, 10)
def test_stock_valuation_layer_revaluation_fifo(self):
self.product1.categ_id.property_cost_method = 'fifo'
context = {
@@ -5,7 +5,7 @@ from flectra.addons.website.tools import MockRequest
from flectra.tests import standalone
@standalone('cow_views')
@standalone('cow_views', 'website_standalone')
def test_01_cow_views_unlink_on_module_update(env):
""" Ensure COW views are correctly removed during module update.
Not removing the view could lead to traceback:
@@ -84,7 +84,7 @@ def test_01_cow_views_unlink_on_module_update(env):
]), "Specific COW views did not get removed!"
@standalone('theme_views')
@standalone('theme_views', 'website_standalone')
def test_02_copy_ids_views_unlink_on_module_update(env):
""" Ensure copy_ids views are correctly removed during module update.
- Having an ir.ui.view A in the codebase, eg `website.layout`
@@ -934,7 +934,7 @@ var FieldDate = InputField.extend({
let value = this.$input.val();
try {
value = this._parseValue(value);
if (this.field.type === "datetime") {
if (this.datewidget.type_of_date === "datetime") {
value.add(-this.getSession().getTZOffset(value), "minutes");
}
} catch (err) {}
@@ -4840,6 +4840,41 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('datetime field with date widget: hit enter should update value', async function (assert) {
assert.expect(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners"><field name="datetime" widget="date"/></form>',
res_id: 1,
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
},
viewOptions: {
mode: 'edit',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
const datetime = form.el.querySelector('input[name="datetime"]');
await testUtils.fields.editInput(datetime, '01/08/22');
await testUtils.fields.triggerKeydown(datetime, 'enter');
assert.strictEqual(datetime.value, '01/08/2022');
// Click outside the field to check that the field is not changed
await testUtils.dom.click(form.$el);
assert.strictEqual(datetime.value, '01/08/2022');
form.destroy();
});
QUnit.module('RemainingDays');
QUnit.test('remaining_days widget on a date field in list view', async function (assert) {
+16 -1
View File
@@ -74,6 +74,9 @@ class IrModuleModule(models.Model):
-> We want to upgrade every website using this theme.
"""
if request and request.context.get('apply_new_theme'):
self = self.with_context(apply_new_theme=True)
for module in self:
if module.name.startswith('theme_') and vals.get('state') == 'installed':
_logger.info('Module %s has been loaded as theme template (%s)' % (module.name, module.state))
@@ -206,7 +209,15 @@ class IrModuleModule(models.Model):
for model_name in self._theme_model_names:
module._update_records(model_name, website)
self.env['theme.utils'].with_context(website_id=website.id)._post_copy(module)
if self._context.get('apply_new_theme'):
# Both the theme install and upgrade flow ends up here.
# The _post_copy() is supposed to be called only when the theme
# is installed for the first time on a website.
# It will basically select some header and footer template.
# We don't want the system to select again the theme footer or
# header template when that theme is updated later. It could
# erase the change the user made after the theme install.
self.env['theme.utils'].with_context(website_id=website.id)._post_copy(module)
def _theme_unload(self, website):
"""
@@ -360,6 +371,10 @@ class IrModuleModule(models.Model):
website.theme_id = self
# this will install 'self' if it is not installed yet
if request:
context = dict(request.context)
context['apply_new_theme'] = True
request.context = context
self._theme_upgrade_upstream()
active_todo = self.env['ir.actions.todo'].search([('state', '=', 'open')], limit=1)
@@ -17,7 +17,7 @@ The view receiving the `inherit_id` update is either:
"""
@standalone('cow_views_inherit')
@standalone('cow_views_inherit', 'website_standalone')
def test_01_cow_views_inherit_on_module_update(env):
# A B A B
# / \ => / \
@@ -49,7 +49,7 @@ def test_01_cow_views_inherit_on_module_update(env):
assert child_cow_view.inherit_id == expected_parent_view, "COW view should also have received the `inherit_id` update."
@standalone('cow_views_inherit')
@standalone('cow_views_inherit', 'website_standalone')
def test_02_cow_views_inherit_on_module_update(env):
# A B B' A B B'
# / \ => | |