mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
[PATCH] Upstream patch - 12062022
This commit is contained in:
@@ -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']
|
||||
}
|
||||
|
||||
####################################################
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
@@ -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
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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'
|
||||
# / \ => | |
|
||||
|
||||
Reference in New Issue
Block a user