mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
[PATCH] Upstream patch - 18012022
This commit is contained in:
@@ -615,29 +615,37 @@ class AccountGroup(models.Model):
|
||||
The most specific is the one with the longest prefixes and with the starting
|
||||
prefix being smaller than the account code and the ending prefix being greater.
|
||||
"""
|
||||
if not self and not account_ids:
|
||||
company_ids = account_ids.company_id.ids if account_ids else self.company_id.ids
|
||||
account_ids = account_ids.ids if account_ids else []
|
||||
if not company_ids and not account_ids:
|
||||
return
|
||||
self.env['account.group'].flush(self.env['account.group']._fields)
|
||||
self.env['account.account'].flush(self.env['account.account']._fields)
|
||||
query = """
|
||||
WITH relation AS (
|
||||
SELECT DISTINCT FIRST_VALUE(agroup.id) OVER (PARTITION BY account.id ORDER BY char_length(agroup.code_prefix_start) DESC, agroup.id) AS group_id,
|
||||
account.id AS account_id
|
||||
FROM account_group agroup
|
||||
JOIN account_account account
|
||||
|
||||
account_where_clause = ''
|
||||
where_params = [tuple(company_ids)]
|
||||
if account_ids:
|
||||
account_where_clause = 'AND account.id IN %s'
|
||||
where_params.append(tuple(account_ids))
|
||||
|
||||
self._cr.execute(f'''
|
||||
WITH candidates_account_groups AS (
|
||||
SELECT
|
||||
account.id AS account_id,
|
||||
ARRAY_AGG(agroup.id ORDER BY char_length(agroup.code_prefix_start) DESC, agroup.id) AS group_ids
|
||||
FROM account_account account
|
||||
LEFT JOIN account_group agroup
|
||||
ON agroup.code_prefix_start <= LEFT(account.code, char_length(agroup.code_prefix_start))
|
||||
AND agroup.code_prefix_end >= LEFT(account.code, char_length(agroup.code_prefix_end))
|
||||
AND agroup.company_id = account.company_id
|
||||
WHERE account.company_id IN %(company_ids)s {where_account}
|
||||
AND agroup.code_prefix_end >= LEFT(account.code, char_length(agroup.code_prefix_end))
|
||||
AND agroup.company_id = account.company_id
|
||||
WHERE account.company_id IN %s {account_where_clause}
|
||||
GROUP BY account.id
|
||||
)
|
||||
UPDATE account_account account
|
||||
SET group_id = relation.group_id
|
||||
FROM relation
|
||||
WHERE relation.account_id = account.id;
|
||||
""".format(
|
||||
where_account=account_ids and 'AND account.id IN %(account_ids)s' or ''
|
||||
)
|
||||
self.env.cr.execute(query, {'company_ids': tuple((self.company_id or account_ids.company_id).ids), 'account_ids': account_ids and tuple(account_ids.ids)})
|
||||
UPDATE account_account
|
||||
SET group_id = rel.group_ids[1]
|
||||
FROM candidates_account_groups rel
|
||||
WHERE account_account.id = rel.account_id
|
||||
''', where_params)
|
||||
self.env['account.account'].invalidate_cache(fnames=['group_id'])
|
||||
|
||||
def _adapt_parent_account_group(self):
|
||||
|
||||
@@ -507,6 +507,7 @@ class AccountBankStatementLine(models.Model):
|
||||
# == Business fields ==
|
||||
move_id = fields.Many2one(
|
||||
comodel_name='account.move',
|
||||
auto_join=True,
|
||||
string='Journal Entry', required=True, readonly=True, ondelete='cascade',
|
||||
check_company=True)
|
||||
statement_id = fields.Many2one(
|
||||
@@ -1268,8 +1269,8 @@ class AccountBankStatementLine(models.Model):
|
||||
if not self.partner_id:
|
||||
rec_overview_partners = set(overview['counterpart_line'].partner_id.id
|
||||
for overview in reconciliation_overview
|
||||
if overview.get('counterpart_line') and overview['counterpart_line'].partner_id)
|
||||
if len(rec_overview_partners) == 1:
|
||||
if overview.get('counterpart_line'))
|
||||
if len(rec_overview_partners) == 1 and rec_overview_partners != {False}:
|
||||
self.line_ids.write({'partner_id': rec_overview_partners.pop()})
|
||||
|
||||
# Refresh analytic lines.
|
||||
|
||||
@@ -175,7 +175,8 @@ class account_journal(models.Model):
|
||||
next_date = start_date + timedelta(days=7)
|
||||
query += " UNION ALL ("+select_sql_clause+" and invoice_date_due >= '"+start_date.strftime(DF)+"' and invoice_date_due < '"+next_date.strftime(DF)+"')"
|
||||
start_date = next_date
|
||||
|
||||
# Ensure results returned by postgres match the order of data list
|
||||
query += " ORDER BY aggr_date ASC"
|
||||
self.env.cr.execute(query, query_args)
|
||||
query_results = self.env.cr.dictfetchall()
|
||||
is_sample_data = True
|
||||
@@ -231,9 +232,9 @@ class account_journal(models.Model):
|
||||
last_balance = last_statement.balance_end
|
||||
has_at_least_one_statement = bool(last_statement)
|
||||
bank_account_balance, nb_lines_bank_account_balance = self._get_journal_bank_account_balance(
|
||||
domain=[('move_id.state', '=', 'posted')])
|
||||
domain=[('parent_state', '=', 'posted')])
|
||||
outstanding_pay_account_balance, nb_lines_outstanding_pay_account_balance = self._get_journal_outstanding_payments_account_balance(
|
||||
domain=[('move_id.state', '=', 'posted')])
|
||||
domain=[('parent_state', '=', 'posted')])
|
||||
|
||||
self._cr.execute('''
|
||||
SELECT COUNT(st_line.id)
|
||||
|
||||
@@ -39,6 +39,14 @@ class AccountMove(models.Model):
|
||||
_check_company_auto = True
|
||||
_sequence_index = "journal_id"
|
||||
|
||||
def init(self):
|
||||
self.env.cr.execute("""
|
||||
CREATE INDEX IF NOT EXISTS account_move_to_check_idx
|
||||
ON account_move(journal_id) WHERE to_check = true;
|
||||
CREATE INDEX IF NOT EXISTS account_move_payment_idx
|
||||
ON account_move(journal_id, state, payment_state, move_type, date);
|
||||
""")
|
||||
|
||||
@property
|
||||
def _sequence_monthly_regex(self):
|
||||
return self.journal_id.sequence_override_regex or super()._sequence_monthly_regex
|
||||
@@ -450,6 +458,7 @@ class AccountMove(models.Model):
|
||||
if self.is_sale_document(include_receipts=True) and self.partner_id:
|
||||
self.invoice_payment_term_id = self.partner_id.property_payment_term_id or self.invoice_payment_term_id
|
||||
new_term_account = self.partner_id.commercial_partner_id.property_account_receivable_id
|
||||
self.narration = self.company_id.with_context(lang=self.partner_id.lang or self.env.lang).invoice_terms
|
||||
elif self.is_purchase_document(include_receipts=True) and self.partner_id:
|
||||
self.invoice_payment_term_id = self.partner_id.property_supplier_payment_term_id or self.invoice_payment_term_id
|
||||
new_term_account = self.partner_id.commercial_partner_id.property_account_payable_id
|
||||
@@ -1735,9 +1744,9 @@ class AccountMove(models.Model):
|
||||
raise ValidationError(_('Posted journal entry must have an unique sequence number per company.\n'
|
||||
'Problematic numbers: %s\n') % ', '.join(r[1] for r in res))
|
||||
|
||||
@api.constrains('ref', 'move_type', 'partner_id', 'journal_id', 'invoice_date')
|
||||
@api.constrains('ref', 'move_type', 'partner_id', 'journal_id', 'invoice_date', 'state')
|
||||
def _check_duplicate_supplier_reference(self):
|
||||
moves = self.filtered(lambda move: move.is_purchase_document() and move.ref)
|
||||
moves = self.filtered(lambda move: move.state == 'posted' and move.is_purchase_document() and move.ref)
|
||||
if not moves:
|
||||
return
|
||||
|
||||
@@ -2124,7 +2133,7 @@ class AccountMove(models.Model):
|
||||
values['total_amount_currency'] += sign * line.amount_currency
|
||||
values['total_residual_currency'] += sign * line.amount_residual_currency
|
||||
|
||||
elif not line.tax_exigible:
|
||||
elif not line.tax_exigible and not line.reconciled:
|
||||
|
||||
values['to_process_lines'] += line
|
||||
currencies.add(line.currency_id or line.company_currency_id)
|
||||
@@ -2401,15 +2410,18 @@ class AccountMove(models.Model):
|
||||
refund_repartition_line = tax_repartition_lines_mapping[invoice_repartition_line]
|
||||
|
||||
# Find the right account.
|
||||
account_id = self.env['account.move.line']._get_default_tax_account(refund_repartition_line).id
|
||||
if not account_id:
|
||||
if not invoice_repartition_line.account_id:
|
||||
# Keep the current account as the current one comes from the base line.
|
||||
account_id = line_vals['account_id']
|
||||
else:
|
||||
tax = invoice_repartition_line.invoice_tax_id
|
||||
base_line = self.line_ids.filtered(lambda line: tax in line.tax_ids.flatten_taxes_hierarchy())[0]
|
||||
account_id = base_line.account_id.id
|
||||
if cancel:
|
||||
account_id = line_vals['account_id']
|
||||
else:
|
||||
account_id = self.env['account.move.line']._get_default_tax_account(refund_repartition_line).id
|
||||
if not account_id:
|
||||
if not invoice_repartition_line.account_id:
|
||||
# Keep the current account as the current one comes from the base line.
|
||||
account_id = line_vals['account_id']
|
||||
else:
|
||||
tax = invoice_repartition_line.invoice_tax_id
|
||||
base_line = self.line_ids.filtered(lambda line: tax in line.tax_ids.flatten_taxes_hierarchy())[0]
|
||||
account_id = base_line.account_id.id
|
||||
|
||||
tags = refund_repartition_line.tag_ids
|
||||
if line_vals.get('tax_ids'):
|
||||
@@ -2594,6 +2606,8 @@ class AccountMove(models.Model):
|
||||
if not self.env.su and not self.env.user.has_group('account.group_account_invoice'):
|
||||
raise AccessError(_("You don't have the access rights to post an invoice."))
|
||||
for move in to_post:
|
||||
if move.partner_bank_id and not move.partner_bank_id.active:
|
||||
raise UserError(_("The recipient bank account link to this invoice is archived.\nSo you cannot confirm the invoice."))
|
||||
if move.state == 'posted':
|
||||
raise UserError(_('The entry %s (id %s) is already posted.') % (move.name, move.id))
|
||||
if not move.line_ids.filtered(lambda line: not line.display_type):
|
||||
@@ -3098,7 +3112,7 @@ class AccountMoveLine(models.Model):
|
||||
help="The move of this entry line.")
|
||||
move_name = fields.Char(string='Number', related='move_id.name', store=True, index=True)
|
||||
date = fields.Date(related='move_id.date', store=True, readonly=True, index=True, copy=False, group_operator='min')
|
||||
ref = fields.Char(related='move_id.ref', store=True, copy=False, index=True, readonly=False)
|
||||
ref = fields.Char(related='move_id.ref', store=True, copy=False, index=True, readonly=True)
|
||||
parent_state = fields.Selection(related='move_id.state', store=True, readonly=True)
|
||||
journal_id = fields.Many2one(related='move_id.journal_id', store=True, index=True, copy=False)
|
||||
company_id = fields.Many2one(related='move_id.company_id', store=True, readonly=True, default=lambda self: self.env.company)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
|
||||
from flectra import models, fields, api, _
|
||||
from flectra.exceptions import UserError, ValidationError
|
||||
@@ -44,10 +45,14 @@ class AccountPayment(models.Model):
|
||||
is_matched = fields.Boolean(string="Is Matched With a Bank Statement", store=True,
|
||||
compute='_compute_reconciliation_status',
|
||||
help="Technical field indicating if the payment has been matched with a statement line.")
|
||||
available_partner_bank_ids = fields.Many2many(
|
||||
comodel_name='res.partner.bank',
|
||||
compute='_compute_available_partner_bank_ids',
|
||||
)
|
||||
partner_bank_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account",
|
||||
readonly=False, store=True,
|
||||
compute='_compute_partner_bank_id',
|
||||
domain="[('partner_id', '=', partner_id)]",
|
||||
domain="[('id', 'in', available_partner_bank_ids)]",
|
||||
check_company=True)
|
||||
is_internal_transfer = fields.Boolean(string="Is Internal Transfer",
|
||||
readonly=False, store=True,
|
||||
@@ -332,20 +337,20 @@ class AccountPayment(models.Model):
|
||||
payment.require_partner_bank_account = payment.state == 'draft' and payment.payment_method_code in self._get_method_codes_needing_bank_account()
|
||||
|
||||
@api.depends('partner_id', 'company_id', 'payment_type')
|
||||
def _compute_available_partner_bank_ids(self):
|
||||
for pay in self:
|
||||
if pay.payment_type == 'inbound':
|
||||
pay.available_partner_bank_ids = pay.journal_id.bank_account_id
|
||||
else:
|
||||
pay.available_partner_bank_ids = pay.partner_id.bank_ids\
|
||||
.filtered(lambda x: x.company_id.id in (False, pay.company_id.id))._origin
|
||||
|
||||
@api.depends('available_partner_bank_ids', 'journal_id')
|
||||
def _compute_partner_bank_id(self):
|
||||
''' The default partner_bank_id will be the first available on the partner. '''
|
||||
for pay in self:
|
||||
if pay.payment_type == 'inbound':
|
||||
bank_partner = pay.company_id.partner_id
|
||||
else:
|
||||
bank_partner = pay.partner_id
|
||||
|
||||
available_partner_bank_accounts = bank_partner.bank_ids.filtered(lambda x: x.company_id.id in (False, pay.company_id.id))
|
||||
if available_partner_bank_accounts:
|
||||
if pay.partner_bank_id not in available_partner_bank_accounts:
|
||||
pay.partner_bank_id = available_partner_bank_accounts[0]._origin
|
||||
else:
|
||||
pay.partner_bank_id = False
|
||||
if pay.partner_bank_id not in pay.available_partner_bank_ids._origin:
|
||||
pay.partner_bank_id = pay.available_partner_bank_ids[:1]._origin
|
||||
|
||||
@api.depends('partner_id', 'destination_account_id', 'journal_id')
|
||||
def _compute_is_internal_transfer(self):
|
||||
@@ -573,6 +578,29 @@ class AccountPayment(models.Model):
|
||||
# LOW-LEVEL METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
|
||||
# OVERRIDE to add the 'available_partner_bank_ids' field dynamically inside the view.
|
||||
# TO BE REMOVED IN MASTER
|
||||
res = super().fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||
if view_type == 'form':
|
||||
form_view_id = self.env['ir.model.data'].xmlid_to_res_id('account.view_account_payment_form')
|
||||
if res.get('view_id') == form_view_id:
|
||||
tree = etree.fromstring(res['arch'])
|
||||
if len(tree.xpath("//field[@name='available_partner_bank_ids']")) == 0:
|
||||
# Don't force people to update the account module.
|
||||
form_view = self.env.ref('account.view_account_payment_form')
|
||||
arch_tree = etree.fromstring(form_view.arch)
|
||||
if arch_tree.tag == 'form':
|
||||
arch_tree.insert(0, etree.Element('field', attrib={
|
||||
'name': 'available_partner_bank_ids',
|
||||
'invisible': '1',
|
||||
}))
|
||||
form_view.sudo().write({'arch': etree.tostring(arch_tree, encoding='unicode')})
|
||||
return super().fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||
|
||||
return res
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# OVERRIDE
|
||||
@@ -695,10 +723,11 @@ class AccountPayment(models.Model):
|
||||
"To be consistent, the journal items must share the same partner."
|
||||
) % move.display_name)
|
||||
|
||||
if counterpart_lines.account_id.user_type_id.type == 'receivable':
|
||||
partner_type = 'customer'
|
||||
else:
|
||||
partner_type = 'supplier'
|
||||
if not pay.is_internal_transfer:
|
||||
if counterpart_lines.account_id.user_type_id.type == 'receivable':
|
||||
payment_vals_to_write['partner_type'] = 'customer'
|
||||
else:
|
||||
payment_vals_to_write['partner_type'] = 'supplier'
|
||||
|
||||
liquidity_amount = liquidity_lines.amount_currency
|
||||
|
||||
@@ -708,7 +737,6 @@ class AccountPayment(models.Model):
|
||||
})
|
||||
payment_vals_to_write.update({
|
||||
'amount': abs(liquidity_amount),
|
||||
'partner_type': partner_type,
|
||||
'currency_id': liquidity_lines.currency_id.id,
|
||||
'destination_account_id': counterpart_lines.account_id.id,
|
||||
'partner_id': liquidity_lines.partner_id.id,
|
||||
|
||||
@@ -291,11 +291,14 @@ class AccountReconcileModel(models.Model):
|
||||
|
||||
new_aml_dicts = []
|
||||
for tax_res in res['taxes']:
|
||||
if self.company_id.currency_id.is_zero(tax_res['amount']):
|
||||
continue
|
||||
tax = self.env['account.tax'].browse(tax_res['id'])
|
||||
balance = tax_res['amount']
|
||||
name = ' '.join([x for x in [base_line_dict.get('name', ''), tax_res['name']] if x])
|
||||
new_aml_dicts.append({
|
||||
'account_id': tax_res['account_id'] or base_line_dict['account_id'],
|
||||
'journal_id': base_line_dict.get('journal_id', False),
|
||||
'name': name,
|
||||
'partner_id': base_line_dict.get('partner_id'),
|
||||
'balance': balance,
|
||||
@@ -649,30 +652,42 @@ class AccountReconcileModel(models.Model):
|
||||
if partner:
|
||||
st_line_subquery += r" AND aml.partner_id = %s" % partner.id
|
||||
else:
|
||||
st_line_subquery += r"""
|
||||
AND
|
||||
(
|
||||
substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9\s]', '', 'g'), '\S(?:.*\S)*') != ''
|
||||
AND
|
||||
(
|
||||
(""" + self._get_select_communication_flag() + """)
|
||||
OR
|
||||
(""" + self._get_select_payment_reference_flag() + """)
|
||||
)
|
||||
)
|
||||
OR
|
||||
(
|
||||
/* We also match statement lines without partners with amls
|
||||
whose partner's name's parts (splitting on space) are all present
|
||||
within the payment_ref, in any order, with any characters between them. */
|
||||
st_line_fields_consideration = [
|
||||
(self.match_text_location_label, 'st_line.payment_ref'),
|
||||
(self.match_text_location_note, 'st_line_move.narration'),
|
||||
(self.match_text_location_reference, 'st_line_move.ref'),
|
||||
]
|
||||
|
||||
aml_partner.name IS NOT NULL
|
||||
AND """ + unaccent("st_line.payment_ref") + r""" ~* ('^' || (
|
||||
SELECT string_agg(concat('(?=.*\m', chunk[1], '\M)'), '')
|
||||
FROM regexp_matches(""" + unaccent("aml_partner.name") + r""", '\w{3,}', 'g') AS chunk
|
||||
))
|
||||
)
|
||||
"""
|
||||
no_partner_query = " OR ".join([
|
||||
r"""
|
||||
(
|
||||
substring(REGEXP_REPLACE(""" + sql_field + """, '[^0-9\s]', '', 'g'), '\S(?:.*\S)*') != ''
|
||||
AND
|
||||
(
|
||||
(""" + self._get_select_communication_flag() + """)
|
||||
OR
|
||||
(""" + self._get_select_payment_reference_flag() + """)
|
||||
)
|
||||
)
|
||||
OR
|
||||
(
|
||||
/* We also match statement lines without partners with amls
|
||||
whose partner's name's parts (splitting on space) are all present
|
||||
within the payment_ref, in any order, with any characters between them. */
|
||||
|
||||
aml_partner.name IS NOT NULL
|
||||
AND """ + unaccent(sql_field) + r""" ~* ('^' || (
|
||||
SELECT string_agg(concat('(?=.*\m', chunk[1], '\M)'), '')
|
||||
FROM regexp_matches(""" + unaccent("aml_partner.name") + r""", '\w{3,}', 'g') AS chunk
|
||||
))
|
||||
)
|
||||
"""
|
||||
for consider_field, sql_field in st_line_fields_consideration
|
||||
if consider_field
|
||||
])
|
||||
|
||||
if no_partner_query:
|
||||
st_line_subquery += " AND " + no_partner_query
|
||||
|
||||
st_lines_queries.append(r"st_line.id = %s AND (%s)" % (st_line.id, st_line_subquery))
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ class AccountTax(models.Model):
|
||||
price_include = self._context.get('force_price_include', tax.price_include)
|
||||
|
||||
#compute the tax_amount
|
||||
if not skip_checkpoint and price_include and total_included_checkpoints.get(i):
|
||||
if not skip_checkpoint and price_include and total_included_checkpoints.get(i) and sum_repartition_factor != 0:
|
||||
# We know the total to reach for that tax, so we make a substraction to avoid any rounding issues
|
||||
tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount)
|
||||
cumulated_tax_included_amount = 0
|
||||
|
||||
@@ -166,7 +166,7 @@ class AccountChartTemplate(models.Model):
|
||||
return self.env['account.account'].create({
|
||||
'name': _("Bank Suspense Account"),
|
||||
'code': self.env['account.account']._search_new_account_code(company, code_digits, company.bank_account_code_prefix or ''),
|
||||
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
|
||||
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
|
||||
@@ -168,11 +168,17 @@ class AccountFiscalPosition(models.Model):
|
||||
# This can be easily overridden to apply more complex fiscal rules
|
||||
PartnerObj = self.env['res.partner']
|
||||
partner = PartnerObj.browse(partner_id)
|
||||
delivery = PartnerObj.browse(delivery_id)
|
||||
|
||||
# if no delivery use invoicing
|
||||
if delivery_id:
|
||||
delivery = PartnerObj.browse(delivery_id)
|
||||
else:
|
||||
company = self.env.company
|
||||
eu_country_codes = set(self.env.ref('base.europe').country_ids.mapped('code'))
|
||||
intra_eu = vat_exclusion = False
|
||||
if company.vat and partner.vat:
|
||||
intra_eu = company.vat[:2] in eu_country_codes and partner.vat[:2] in eu_country_codes
|
||||
vat_exclusion = company.vat[:2] == partner.vat[:2]
|
||||
|
||||
# If company and partner have the same vat prefix (and are both within the EU), use invoicing
|
||||
if not delivery or (intra_eu and vat_exclusion):
|
||||
delivery = partner
|
||||
|
||||
# partner manually set fiscal position always win
|
||||
|
||||
@@ -100,7 +100,7 @@ class AccountInvoiceReport(models.Model):
|
||||
-line.balance * currency_table.rate AS price_subtotal,
|
||||
-COALESCE(
|
||||
-- Average line price
|
||||
(line.balance / NULLIF(line.quantity, 0.0))
|
||||
(line.balance / NULLIF(line.quantity, 0.0)) * (CASE WHEN move.move_type IN ('in_invoice','out_refund','in_receipt') THEN -1 ELSE 1 END)
|
||||
-- convert to template uom
|
||||
* (NULLIF(COALESCE(uom_line.factor, 1), 0.0) / NULLIF(COALESCE(uom_template.factor, 1), 0.0)),
|
||||
0.0) * currency_table.rate AS price_average,
|
||||
|
||||
@@ -135,3 +135,21 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
||||
self.company_data['default_journal_bank'].payment_debit_account_id.reconcile = False
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.company_data['default_journal_bank'].payment_credit_account_id.reconcile = False
|
||||
|
||||
def test_remove_account_from_account_group(self):
|
||||
"""Test if an account is well removed from account group"""
|
||||
group = self.env['account.group'].create({
|
||||
'name': 'test_group',
|
||||
'code_prefix_start': 401000,
|
||||
'code_prefix_end': 402000,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
|
||||
account_1 = self.company_data['default_account_revenue'].copy({'code': 401000})
|
||||
account_2 = self.company_data['default_account_revenue'].copy({'code': 402000})
|
||||
|
||||
self.assertRecordValues(account_1 + account_2, [{'group_id': group.id}] * 2)
|
||||
|
||||
group.code_prefix_end = 401000
|
||||
|
||||
self.assertRecordValues(account_1 + account_2, [{'group_id': group.id}, {'group_id': False}])
|
||||
|
||||
@@ -1592,3 +1592,86 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
self.assertRecordValues(statement_line.line_ids.analytic_line_ids, [
|
||||
{'amount': 100.0, 'account_id': analytic_account.id},
|
||||
])
|
||||
|
||||
def test_reconciliation_line_with_no_partner(self):
|
||||
"""
|
||||
Ensure that entry lines and statement line have no partner when reconciling
|
||||
lines without partner with others with partner
|
||||
"""
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2022-01-01',
|
||||
'payment_ref': "Happy new year",
|
||||
'amount': 200.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement.button_post()
|
||||
|
||||
partner = self.env['res.partner'].create({'name': 'test'})
|
||||
|
||||
receivable_account = self.company_data['default_account_receivable']
|
||||
outstanding_account = self.company_data['default_journal_bank']['payment_debit_account_id']
|
||||
|
||||
payments = self.env['account.payment'].create([
|
||||
{
|
||||
'name': 'Payment without partner',
|
||||
'date': fields.Date.from_string('2022-01-01'),
|
||||
'is_internal_transfer': False,
|
||||
'amount': 100.0,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'destination_account_id': receivable_account.id,
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'Payment with partner',
|
||||
'date': fields.Date.from_string('2022-01-01'),
|
||||
'is_internal_transfer': False,
|
||||
'amount': 100.0,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'partner_id': partner.id,
|
||||
'destination_account_id': receivable_account.id,
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
},
|
||||
])
|
||||
payments.action_post()
|
||||
|
||||
statement_line = statement.line_ids
|
||||
|
||||
statement_line.reconcile([
|
||||
{'id': payments[0].move_id.line_ids.filtered(lambda line: line.account_id == outstanding_account).id},
|
||||
{'id': payments[1].move_id.line_ids.filtered(lambda line: line.account_id == outstanding_account).id},
|
||||
])
|
||||
|
||||
self.assertRecordValues(
|
||||
statement.line_ids.move_id.line_ids,
|
||||
[
|
||||
{
|
||||
'debit': 200.0,
|
||||
'credit': 0.0,
|
||||
'partner_id': False,
|
||||
'account_id': self.bank_journal_1.default_account_id.id
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
'partner_id': False,
|
||||
'account_id': outstanding_account.id
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
'partner_id': partner.id,
|
||||
'account_id': outstanding_account.id
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(statement.line_ids, [{
|
||||
'partner_id': False,
|
||||
}])
|
||||
|
||||
@@ -103,6 +103,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
||||
'price_subtotal': vals[1],
|
||||
'quantity': vals[2],
|
||||
} for vals in expected_values_list]
|
||||
|
||||
self.assertRecordValues(reports, expected_values_dict)
|
||||
|
||||
def test_invoice_report_multiple_types(self):
|
||||
@@ -112,7 +113,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
||||
[1000, 1000, 1],
|
||||
[250, 750, 3],
|
||||
[6, 6, 1],
|
||||
[-20, -20, -1],
|
||||
[-20, -20, -1],
|
||||
[-600, -600, -1],
|
||||
[20, -20, -1],
|
||||
[20, -20, -1],
|
||||
[600, -600, -1],
|
||||
])
|
||||
|
||||
@@ -1502,9 +1502,9 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
''' Ensure two vendor bills can't share the same vendor reference. '''
|
||||
self.invoice.ref = 'a supplier reference'
|
||||
invoice2 = self.invoice.copy(default={'invoice_date': self.invoice.invoice_date})
|
||||
|
||||
invoice2.ref = 'a supplier reference'
|
||||
with self.assertRaises(ValidationError):
|
||||
invoice2.ref = 'a supplier reference'
|
||||
invoice2.action_post()
|
||||
|
||||
def test_in_invoice_switch_in_refund_1(self):
|
||||
# Test creating an account_move with an in_invoice_type and switch it in an in_refund.
|
||||
|
||||
@@ -1942,10 +1942,124 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
(self.tax_account_1, -20.0, -13.33),
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_4(self):
|
||||
''' Test the generation of the exchange difference for a tax cash basis journal entry when the tax
|
||||
account is a reconcile one.
|
||||
'''
|
||||
currency_id = self.currency_data['currency'].id
|
||||
cash_basis_transition_account = self.env['account.account'].create({
|
||||
'code': '209.01.01',
|
||||
'name': 'Cash Basis Transition Account',
|
||||
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'reconcile': True,
|
||||
})
|
||||
self.cash_basis_tax_a_third_amount.write({
|
||||
'cash_basis_transition_account_id': cash_basis_transition_account.id,
|
||||
})
|
||||
|
||||
# Rate 1/3 in 2016.
|
||||
cash_basis_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
# Base Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
'amount_currency': -300.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
|
||||
'tax_exigible': False,
|
||||
}),
|
||||
|
||||
# Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 33.33,
|
||||
'amount_currency': -100.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': cash_basis_transition_account.id,
|
||||
'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
}),
|
||||
|
||||
# Receivable lines
|
||||
(0, 0, {
|
||||
'debit': 133.33,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 400.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.extra_receivable_account_1.id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
# Rate 1/2 in 2017.
|
||||
payment_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 200.0,
|
||||
'amount_currency': -400.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.extra_receivable_account_1.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 200.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 400.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
(cash_basis_move + payment_move).action_post()
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(cash_basis_transition_account, -33.33, -100.0),
|
||||
(self.tax_account_1, 0.0, 0.0),
|
||||
])
|
||||
|
||||
receivable_lines = (cash_basis_move + payment_move).line_ids\
|
||||
.filtered(lambda line: line.account_id == self.extra_receivable_account_1)
|
||||
res = receivable_lines.reconcile()
|
||||
|
||||
self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
|
||||
|
||||
# Tax values based on payment
|
||||
# Invoice amount 300 (amount currency) with payment rate 2 (400 payment amount divided by 200 invoice balance)
|
||||
# - Base amount: 150 company currency
|
||||
# - Tax amount: 50 company currency
|
||||
self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
|
||||
# Base amount:
|
||||
{'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
# tax:
|
||||
{'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': cash_basis_transition_account.id},
|
||||
{'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
|
||||
])
|
||||
|
||||
exchange_diff = res['full_reconcile'].exchange_move_id
|
||||
|
||||
# Exchange difference
|
||||
# 66.67 amount residual on the payment line after reconciling receivable line of the cash basis move with the payment counterpart
|
||||
# 50.00 difference of the cash_basis_move base line and the CABA entry created by the system
|
||||
self.assertRecordValues(exchange_diff.line_ids, [
|
||||
{'debit': 66.67, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id},
|
||||
{'debit': 0.0, 'credit': 66.67, 'currency_id': currency_id, 'account_id': self.company_data['company'].income_currency_exchange_account_id.id},
|
||||
{'debit': 50.0, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 50.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_revert(self):
|
||||
''' Ensure the cash basis journal entry can be reverted. '''
|
||||
self.cash_basis_transfer_account.reconcile = True
|
||||
self.cash_basis_tax_a_third_amount.cash_basis_transition_account_id = self.tax_account_1
|
||||
|
||||
invoice_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
|
||||
@@ -14,6 +14,9 @@ class TestAccountPayment(AccountTestInvoicingCommon):
|
||||
cls.payment_debit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_debit_account_id)
|
||||
cls.payment_credit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_credit_account_id)
|
||||
|
||||
cls.bank_journal_1 = cls.company_data['default_journal_bank']
|
||||
cls.bank_journal_2 = cls.company_data['default_journal_bank'].copy()
|
||||
|
||||
cls.partner_bank_account1 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "0123456789",
|
||||
'partner_id': cls.partner_a.id,
|
||||
@@ -24,13 +27,18 @@ class TestAccountPayment(AccountTestInvoicingCommon):
|
||||
'partner_id': cls.partner_a.id,
|
||||
'acc_type': 'bank',
|
||||
})
|
||||
cls.comp_bank_account = cls.env['res.partner.bank'].create({
|
||||
cls.comp_bank_account1 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "985632147",
|
||||
'partner_id': cls.env.company.partner_id.id,
|
||||
'acc_type': 'bank',
|
||||
})
|
||||
cls.comp_bank_account2 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "741258963",
|
||||
'partner_id': cls.env.company.partner_id.id,
|
||||
'acc_type': 'bank',
|
||||
})
|
||||
|
||||
cls.company_data['default_journal_bank'].write({
|
||||
cls.bank_journal_1.write({
|
||||
'payment_debit_account_id': cls.payment_debit_account_id.id,
|
||||
'payment_credit_account_id': cls.payment_credit_account_id.id,
|
||||
'inbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_in').ids)],
|
||||
@@ -766,17 +774,49 @@ class TestAccountPayment(AccountTestInvoicingCommon):
|
||||
},
|
||||
])
|
||||
|
||||
def test_payment_partner_bank_inbound(self):
|
||||
""" Test the bank account is well recomputed for inbound payments. In that case, the recipient
|
||||
bank account must be the one set on the company.
|
||||
def test_suggested_default_partner_bank(self):
|
||||
""" Ensure the 'partner_bank_id' is well computed on payments. When the payment is inbound, the money must be
|
||||
received by a bank account linked to the company. In case of outbound payment, the bank account must be found
|
||||
on the partner.
|
||||
"""
|
||||
payment = self.env['account.payment'].create({
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'amount': 50.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
self.assertRecordValues(payment, [{'partner_bank_id': self.partner_bank_account1.id}])
|
||||
self.assertRecordValues(payment, [{
|
||||
'available_partner_bank_ids': self.partner_a.bank_ids.ids,
|
||||
'partner_bank_id': self.partner_bank_account1.id,
|
||||
}])
|
||||
|
||||
payment.payment_type = 'inbound'
|
||||
self.assertRecordValues(payment, [{'partner_bank_id': self.comp_bank_account.id}])
|
||||
self.assertRecordValues(payment, [{
|
||||
'available_partner_bank_ids': [],
|
||||
'partner_bank_id': False,
|
||||
}])
|
||||
|
||||
self.bank_journal_2.bank_account_id = self.comp_bank_account2
|
||||
# A sequence is automatically added on the first move. We need to clean it before changing the journal.
|
||||
payment.name = False
|
||||
payment.journal_id = self.bank_journal_2
|
||||
self.assertRecordValues(payment, [{
|
||||
'available_partner_bank_ids': self.comp_bank_account2.ids,
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
}])
|
||||
|
||||
def test_internal_transfer_custom_partner_bank_id(self):
|
||||
""" Ensure partner_bank_id user choice is not systematically ignored by compute method. """
|
||||
self.bank_journal_1.bank_account_id = self.comp_bank_account1
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'amount': 50.0,
|
||||
'is_internal_transfer': True,
|
||||
'payment_type': 'outbound',
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
})
|
||||
self.assertRecordValues(payment, [{
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
}])
|
||||
|
||||
@@ -10,7 +10,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
|
||||
cls.currency_data_3 = cls.setup_multi_currency_data({
|
||||
'name': "Umbrella",
|
||||
'symbol': '☂',
|
||||
@@ -35,7 +35,10 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})
|
||||
cls.manual_payment_method_out = cls.env.ref('account.account_payment_method_manual_out')
|
||||
|
||||
cls.company_data['default_journal_bank'].write({
|
||||
cls.bank_journal_1 = cls.company_data['default_journal_bank']
|
||||
cls.bank_journal_2 = cls.company_data['default_journal_bank'].copy()
|
||||
|
||||
cls.bank_journal_1.write({
|
||||
'payment_debit_account_id': cls.payment_debit_account_id.id,
|
||||
'payment_credit_account_id': cls.payment_credit_account_id.id,
|
||||
'inbound_payment_method_ids': [(6, 0, (
|
||||
@@ -49,11 +52,16 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
))],
|
||||
})
|
||||
|
||||
cls.partner_bank_account = cls.env['res.partner.bank'].create({
|
||||
cls.partner_bank_account1 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "0123456789",
|
||||
'partner_id': cls.partner_a.id,
|
||||
'acc_type': 'bank',
|
||||
})
|
||||
cls.partner_bank_account2 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "9876543210",
|
||||
'partner_id': cls.partner_a.id,
|
||||
'acc_type': 'bank',
|
||||
})
|
||||
cls.comp_bank_account1 = cls.env['res.partner.bank'].create({
|
||||
'acc_number': "985632147",
|
||||
'partner_id': cls.env.company.partner_id.id,
|
||||
@@ -477,6 +485,9 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
|
||||
and one with a vendor bill of 600, by splitting payments.
|
||||
'''
|
||||
self.in_invoice_1.partner_bank_id = self.partner_bank_account1
|
||||
self.in_invoice_2.partner_bank_id = self.partner_bank_account2
|
||||
|
||||
active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids
|
||||
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
|
||||
'group_payment': False,
|
||||
@@ -484,16 +495,22 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'ref': 'BILL/2017/01/0001',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
'partner_bank_id': self.partner_bank_account1.id,
|
||||
},
|
||||
{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'ref': 'BILL/2017/01/0002',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
'partner_bank_id': self.partner_bank_account2.id,
|
||||
},
|
||||
{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'ref': 'BILL/2017/01/0003',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
'partner_bank_id': False,
|
||||
},
|
||||
])
|
||||
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [
|
||||
@@ -550,27 +567,6 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
},
|
||||
])
|
||||
|
||||
def test_register_payment_custom_bank_account(self):
|
||||
""" Ensure the user is able to select a custom bank account when registering a payment and this bank account
|
||||
lands correctly on the generated payment.
|
||||
"""
|
||||
self.out_invoice_1.partner_bank_id = self.comp_bank_account1
|
||||
|
||||
ctx = {'active_model': 'account.move', 'active_ids': self.out_invoice_1.ids}
|
||||
wizard_form = Form(self.env['account.payment.register'].with_context(**ctx))
|
||||
wizard = wizard_form.save()
|
||||
|
||||
# The bank account set on the invoice must be the default suggested value.
|
||||
self.assertRecordValues(wizard, [{'partner_bank_id': self.comp_bank_account1.id}])
|
||||
|
||||
wizard_form = Form(wizard)
|
||||
wizard_form.partner_bank_id = self.comp_bank_account2
|
||||
wizard = wizard_form.save()
|
||||
payments = wizard._create_payments()
|
||||
|
||||
# The user should be able to set a custom bank account.
|
||||
self.assertRecordValues(payments, [{'partner_bank_id': self.comp_bank_account2.id}])
|
||||
|
||||
def test_register_payment_constraints(self):
|
||||
# Test to register a payment for a draft journal entry.
|
||||
self.out_invoice_1.button_draft()
|
||||
@@ -835,3 +831,82 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
'reconciled': False,
|
||||
},
|
||||
])
|
||||
|
||||
def test_suggested_default_partner_bank_inbound_payment(self):
|
||||
""" Test the suggested bank account on the wizard for inbound payment. """
|
||||
self.out_invoice_1.partner_bank_id = False
|
||||
|
||||
ctx = {'active_model': 'account.move', 'active_ids': self.out_invoice_1.ids}
|
||||
wizard = self.env['account.payment.register'].with_context(**ctx).create({})
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'available_partner_bank_ids': [],
|
||||
'partner_bank_id': False,
|
||||
}])
|
||||
|
||||
self.bank_journal_2.bank_account_id = self.out_invoice_1.partner_bank_id = self.comp_bank_account2
|
||||
wizard = self.env['account.payment.register'].with_context(**ctx).create({})
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'available_partner_bank_ids': self.comp_bank_account2.ids,
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
}])
|
||||
|
||||
wizard.journal_id = self.bank_journal_1
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'available_partner_bank_ids': [],
|
||||
'partner_bank_id': False,
|
||||
}])
|
||||
|
||||
def test_suggested_default_partner_bank_outbound_payment(self):
|
||||
""" Test the suggested bank account on the wizard for outbound payment. """
|
||||
self.in_invoice_1.partner_bank_id = False
|
||||
|
||||
ctx = {'active_model': 'account.move', 'active_ids': self.in_invoice_1.ids}
|
||||
wizard = self.env['account.payment.register'].with_context(**ctx).create({})
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'available_partner_bank_ids': self.partner_a.bank_ids.ids,
|
||||
'partner_bank_id': self.partner_bank_account1.id,
|
||||
}])
|
||||
|
||||
self.in_invoice_1.partner_bank_id = self.partner_bank_account2
|
||||
wizard = self.env['account.payment.register'].with_context(**ctx).create({})
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'available_partner_bank_ids': self.partner_a.bank_ids.ids,
|
||||
'partner_bank_id': self.partner_bank_account2.id,
|
||||
}])
|
||||
|
||||
wizard.journal_id = self.bank_journal_2
|
||||
self.assertRecordValues(wizard, [{
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'available_partner_bank_ids': self.partner_a.bank_ids.ids,
|
||||
'partner_bank_id': self.partner_bank_account2.id,
|
||||
}])
|
||||
|
||||
def test_register_payment_inbound_multiple_bank_account(self):
|
||||
""" Pay customer invoices with different bank accounts. """
|
||||
self.out_invoice_1.partner_bank_id = self.comp_bank_account1
|
||||
self.out_invoice_2.partner_bank_id = self.comp_bank_account2
|
||||
self.bank_journal_2.bank_account_id = self.comp_bank_account2
|
||||
|
||||
ctx = {'active_model': 'account.move', 'active_ids': (self.out_invoice_1 + self.out_invoice_2).ids}
|
||||
wizard = self.env['account.payment.register'].with_context(**ctx).create({'journal_id': self.bank_journal_2.id})
|
||||
payments = wizard._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'ref': 'INV/2017/01/0001',
|
||||
'payment_method_id': self.manual_payment_method_in.id,
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
},
|
||||
{
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'ref': 'INV/2017/01/0002',
|
||||
'payment_method_id': self.manual_payment_method_in.id,
|
||||
'partner_bank_id': self.comp_bank_account2.id,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -258,6 +258,51 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
|
||||
}, statements=self.bank_st_2)
|
||||
|
||||
def test_matching_fields_match_text_location_no_partner(self):
|
||||
self.bank_line_2.unlink() # One line is enough for this test
|
||||
self.bank_line_1.partner_id = None
|
||||
|
||||
self.partner_1.name = "Bernard Gagnant"
|
||||
|
||||
self.rule_1.write({
|
||||
'match_partner': False,
|
||||
'match_partner_ids': [(5, 0, 0)],
|
||||
'line_ids': [(5, 0, 0)],
|
||||
})
|
||||
|
||||
st_line_initial_vals = {'ref': None, 'payment_ref': 'nothing', 'narration': None}
|
||||
recmod_initial_vals = {'match_text_location_label': False, 'match_text_location_note': False, 'match_text_location_reference': False}
|
||||
|
||||
rec_mod_options_to_fields = {
|
||||
'match_text_location_label': 'payment_ref',
|
||||
'match_text_location_note': 'narration',
|
||||
'match_text_location_reference': 'ref',
|
||||
}
|
||||
|
||||
for rec_mod_field, st_line_field in rec_mod_options_to_fields.items():
|
||||
self.rule_1.write({**recmod_initial_vals, rec_mod_field: True})
|
||||
# Fully reinitialize the statement line
|
||||
self.bank_line_1.write(st_line_initial_vals)
|
||||
|
||||
# Nothing should match
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': []},
|
||||
}, statements=self.bank_st)
|
||||
|
||||
# Test matching with the invoice ref
|
||||
self.bank_line_1.write({st_line_field: self.invoice_line_1.move_id.payment_reference})
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']},
|
||||
}, statements=self.bank_st)
|
||||
|
||||
# Test matching with the partner name (reinitializing the statement line first)
|
||||
self.bank_line_1.write({**st_line_initial_vals, st_line_field: self.partner_1.name})
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']},
|
||||
}, statements=self.bank_st)
|
||||
|
||||
def test_matching_fields_match_journal_ids(self):
|
||||
self.rule_1.match_journal_ids |= self.cash_st.journal_id
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
|
||||
@@ -1073,3 +1073,61 @@ class TestTax(TestTaxCommon):
|
||||
],
|
||||
(tax_10_fix + tax_21).compute_all(1210),
|
||||
)
|
||||
|
||||
def test_price_included_repartition_sum_0(self):
|
||||
""" Tests the case where a tax with a non-zero value has a sum
|
||||
of tax repartition factors of zero and is included in price. It
|
||||
shouldn't behave in the same way as a 0% tax.
|
||||
"""
|
||||
test_tax = self.env['account.tax'].create({
|
||||
'name': "Definitely not a 0% tax",
|
||||
'amount_type': 'percent',
|
||||
'amount': 42,
|
||||
'price_include': True,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': -100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
compute_all_res = test_tax.compute_all(100)
|
||||
self._check_compute_all_results(
|
||||
100, # 'total_included'
|
||||
100, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(100, 42),
|
||||
(100, -42),
|
||||
# ---------------
|
||||
],
|
||||
compute_all_res
|
||||
)
|
||||
|
||||
@@ -332,18 +332,18 @@
|
||||
<field name="tax_line_id" string="Originator Tax"/>
|
||||
<field name="reconcile_model_id"/>
|
||||
<separator/>
|
||||
<filter string="Unposted" name="unposted" domain="[('move_id.state', '=', 'draft')]" help="Unposted Journal Items"/>
|
||||
<filter string="Posted" name="posted" domain="[('move_id.state', '=', 'posted')]" help="Posted Journal Items"/>
|
||||
<filter string="Unposted" name="unposted" domain="[('parent_state', '=', 'draft')]" help="Unposted Journal Items"/>
|
||||
<filter string="Posted" name="posted" domain="[('parent_state', '=', 'posted')]" help="Posted Journal Items"/>
|
||||
<separator/>
|
||||
<filter string="To Check" name="to_check" domain="[('move_id.to_check', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Unreconciled" domain="[('full_reconcile_id', '=', False), ('balance', '!=', 0), ('account_id.reconcile', '=', True)]" help="Journal items where matching number isn't set" name="unreconciled"/>
|
||||
<separator/>
|
||||
<filter string="Sales" name="sales" domain="[('move_id.journal_id.type', '=', 'sale')]" context="{'default_journal_type': 'sale'}"/>
|
||||
<filter string="Purchases" name="purchases" domain="[('move_id.journal_id.type', '=', 'purchase')]" context="{'default_journal_type': 'purchase'}"/>
|
||||
<filter string="Bank" name="bank" domain="[('move_id.journal_id.type', '=', 'bank')]" context="{'default_journal_type': 'bank'}"/>
|
||||
<filter string="Cash" name="cash" domain="[('move_id.journal_id.type', '=', 'cash')]" context="{'default_journal_type': 'cash'}"/>
|
||||
<filter string="Miscellaneous" domain="[('move_id.journal_id.type', '=', 'general')]" name="misc_filter" context="{'default_journal_type': 'general'}"/>
|
||||
<filter string="Sales" name="sales" domain="[('journal_id.type', '=', 'sale')]" context="{'default_journal_type': 'sale'}"/>
|
||||
<filter string="Purchases" name="purchases" domain="[('journal_id.type', '=', 'purchase')]" context="{'default_journal_type': 'purchase'}"/>
|
||||
<filter string="Bank" name="bank" domain="[('journal_id.type', '=', 'bank')]" context="{'default_journal_type': 'bank'}"/>
|
||||
<filter string="Cash" name="cash" domain="[('journal_id.type', '=', 'cash')]" context="{'default_journal_type': 'cash'}"/>
|
||||
<filter string="Miscellaneous" domain="[('journal_id.type', '=', 'general')]" name="misc_filter" context="{'default_journal_type': 'general'}"/>
|
||||
<separator/>
|
||||
<filter string="Payable" domain="[('account_id.internal_type', '=', 'payable')]" help="From Payable accounts" name="payable"/>
|
||||
<filter string="Receivable" domain="[('account_id.internal_type', '=', 'receivable')]" help="From Receivable accounts" name="receivable"/>
|
||||
@@ -1300,7 +1300,7 @@
|
||||
<field name="context">{'journal_type':'general', 'search_default_posted':1}</field>
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note')), ('move_id.state', '!=', 'cancel')]</field>
|
||||
<field name="domain">[('display_type', 'not in', ('line_section', 'line_note')), ('parent_state', '!=', 'cancel')]</field>
|
||||
<field name="view_id" ref="view_move_line_tree"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
<field name="show_partner_bank_account" invisible="1"/>
|
||||
<field name="require_partner_bank_account" invisible="1"/>
|
||||
<field name="hide_payment_method" invisible="1"/>
|
||||
<field name="available_partner_bank_ids" invisible="1"/>
|
||||
<field name="available_payment_method_ids" invisible="1"/>
|
||||
<field name="suitable_journal_ids" invisible="1"/>
|
||||
<field name="country_code" invisible="1"/>
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
attrs="{'invisible': [('match_partner', '=', False)]}"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('rule_type', '!=', 'invoice_matching')]}">
|
||||
<label for="match_text_location_label" string="Match Invoice/bill with"/>
|
||||
<span class="o_form_label o_td_label">Match Invoice/bill with</span>
|
||||
<div>
|
||||
<span class="o_form_label" style="width: 2% !important"> </span>
|
||||
<label for="match_text_location_label" string="Label"/>
|
||||
|
||||
@@ -127,6 +127,58 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
labels = set(line.name or line.move_id.ref or line.move_id.name for line in batch_result['lines'])
|
||||
return ' '.join(sorted(labels))
|
||||
|
||||
@api.model
|
||||
def _get_batch_journal(self, batch_result):
|
||||
""" Helper to compute the journal based on the batch.
|
||||
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: An account.journal record.
|
||||
"""
|
||||
key_values = batch_result['key_values']
|
||||
foreign_currency_id = key_values['currency_id']
|
||||
partner_bank_id = key_values['partner_bank_id']
|
||||
|
||||
currency_domain = [('currency_id', '=', foreign_currency_id)]
|
||||
partner_bank_domain = [('bank_account_id', '=', partner_bank_id)]
|
||||
|
||||
default_domain = [
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
('company_id', '=', batch_result['lines'].company_id.id),
|
||||
]
|
||||
|
||||
if partner_bank_id:
|
||||
extra_domains = (
|
||||
currency_domain + partner_bank_domain,
|
||||
partner_bank_domain,
|
||||
currency_domain,
|
||||
[],
|
||||
)
|
||||
else:
|
||||
extra_domains = (
|
||||
currency_domain,
|
||||
[],
|
||||
)
|
||||
|
||||
for extra_domain in extra_domains:
|
||||
journal = self.env['account.journal'].search(default_domain + extra_domain, limit=1)
|
||||
if journal:
|
||||
return journal
|
||||
|
||||
return self.env['account.journal']
|
||||
|
||||
@api.model
|
||||
def _get_batch_available_partner_banks(self, batch_result, journal):
|
||||
key_values = batch_result['key_values']
|
||||
company = batch_result['lines'].company_id
|
||||
|
||||
# A specific bank account is set on the journal. The user must use this one.
|
||||
if key_values['payment_type'] == 'inbound':
|
||||
# Receiving money on a bank account linked to the journal.
|
||||
return journal.bank_account_id
|
||||
else:
|
||||
# Sending money to a bank account owned by a partner.
|
||||
return batch_result['lines'].partner_id.bank_ids.filtered(lambda x: x.company_id.id in (False, company.id))._origin
|
||||
|
||||
@api.model
|
||||
def _get_line_batch_key(self, line):
|
||||
''' Turn the line passed as parameter to a dictionary defining on which way the lines
|
||||
@@ -134,15 +186,15 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
:return: A python dictionary.
|
||||
'''
|
||||
move = line.move_id
|
||||
partner_bank_account = self.env['res.partner.bank']
|
||||
|
||||
partner_bank_account = self.env['res.partner.bank']
|
||||
if move.is_invoice(include_receipts=True):
|
||||
partner_bank_account = move.partner_bank_id._origin
|
||||
|
||||
return {
|
||||
'partner_id': line.partner_id.id,
|
||||
'account_id': line.account_id.id,
|
||||
'currency_id': (line.currency_id or line.company_currency_id).id,
|
||||
'currency_id': line.currency_id.id,
|
||||
'partner_bank_id': partner_bank_account.id,
|
||||
'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
|
||||
'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
|
||||
@@ -173,6 +225,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'lines': self.env['account.move.line'],
|
||||
})
|
||||
batches[serialized_key]['lines'] += line
|
||||
|
||||
return list(batches.values())
|
||||
|
||||
@api.model
|
||||
@@ -227,7 +280,6 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'partner_id': False,
|
||||
'partner_type': False,
|
||||
'payment_type': wizard_values_from_batch['payment_type'],
|
||||
'partner_bank_id': False,
|
||||
'source_currency_id': False,
|
||||
'source_amount': False,
|
||||
'source_amount_currency': False,
|
||||
@@ -256,39 +308,40 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
else:
|
||||
wizard.group_payment = False
|
||||
|
||||
@api.depends('company_id', 'source_currency_id')
|
||||
@api.depends('can_edit_wizard', 'company_id')
|
||||
def _compute_journal_id(self):
|
||||
for wizard in self:
|
||||
domain = [
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
('company_id', '=', wizard.company_id.id),
|
||||
]
|
||||
journal = None
|
||||
if wizard.source_currency_id:
|
||||
journal = self.env['account.journal'].search(domain + [('currency_id', '=', wizard.source_currency_id.id)], limit=1)
|
||||
if not journal:
|
||||
journal = self.env['account.journal'].search(domain, limit=1)
|
||||
wizard.journal_id = journal
|
||||
if wizard.can_edit_wizard:
|
||||
batch = wizard._get_batches()[0]
|
||||
wizard.journal_id = wizard._get_batch_journal(batch)
|
||||
else:
|
||||
wizard.journal_id = self.env['account.journal'].search([
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
('company_id', '=', wizard.company_id.id),
|
||||
], limit=1)
|
||||
|
||||
@api.depends('company_id', 'can_edit_wizard')
|
||||
@api.depends('can_edit_wizard', 'journal_id')
|
||||
def _compute_available_partner_bank_ids(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batches = wizard._get_batches()
|
||||
bank_partners = batches[0]['lines'].move_id.bank_partner_id
|
||||
wizard.available_partner_bank_ids = bank_partners.bank_ids\
|
||||
.filtered(lambda x: x.company_id.id in (False, wizard.company_id.id))._origin
|
||||
batch = wizard._get_batches()[0]
|
||||
wizard.available_partner_bank_ids = wizard._get_batch_available_partner_banks(batch, wizard.journal_id)
|
||||
else:
|
||||
wizard.available_partner_bank_ids = False
|
||||
wizard.available_partner_bank_ids = None
|
||||
|
||||
@api.depends('available_partner_bank_ids')
|
||||
@api.depends('journal_id', 'available_partner_bank_ids')
|
||||
def _compute_partner_bank_id(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batches = wizard._get_batches()
|
||||
wizard.partner_bank_id = self.env['res.partner.bank'].browse(batches[0]['key_values']['partner_bank_id'])
|
||||
batch = wizard._get_batches()[0]
|
||||
partner_bank_id = batch['key_values']['partner_bank_id']
|
||||
available_partner_banks = wizard.available_partner_bank_ids._origin
|
||||
if partner_bank_id and partner_bank_id in available_partner_banks.ids:
|
||||
wizard.partner_bank_id = self.env['res.partner.bank'].browse(partner_bank_id)
|
||||
else:
|
||||
wizard.partner_bank_id = available_partner_banks[:1]
|
||||
else:
|
||||
wizard.partner_bank_id = False
|
||||
wizard.partner_bank_id = None
|
||||
|
||||
@api.depends('journal_id')
|
||||
def _compute_currency_id(self):
|
||||
@@ -379,7 +432,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'name': 'available_partner_bank_ids',
|
||||
'invisible': '1',
|
||||
}))
|
||||
form_view.arch = etree.tostring(arch_tree, encoding='unicode')
|
||||
form_view.sudo().write({'arch': etree.tostring(arch_tree, encoding='unicode')})
|
||||
return super().fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||
|
||||
return res
|
||||
@@ -427,7 +480,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
raise UserError(_("You can't register payments for journal items being either all inbound, either all outbound."))
|
||||
|
||||
res['line_ids'] = [(6, 0, available_lines.ids)]
|
||||
|
||||
|
||||
return res
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -459,6 +512,12 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
|
||||
def _create_payment_vals_from_batch(self, batch_result):
|
||||
batch_values = self._get_wizard_values_from_batch(batch_result)
|
||||
|
||||
if batch_values['payment_type'] == 'inbound':
|
||||
partner_bank_id = self.journal_id.bank_account_id.id
|
||||
else:
|
||||
partner_bank_id = batch_result['key_values']['partner_bank_id']
|
||||
|
||||
return {
|
||||
'date': self.payment_date,
|
||||
'amount': batch_values['source_amount_currency'],
|
||||
@@ -468,46 +527,34 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'journal_id': self.journal_id.id,
|
||||
'currency_id': batch_values['source_currency_id'],
|
||||
'partner_id': batch_values['partner_id'],
|
||||
'partner_bank_id': batch_result['key_values']['partner_bank_id'],
|
||||
'partner_bank_id': partner_bank_id,
|
||||
'payment_method_id': self.payment_method_id.id,
|
||||
'destination_account_id': batch_result['lines'][0].account_id.id
|
||||
}
|
||||
|
||||
def _create_payments(self):
|
||||
self.ensure_one()
|
||||
batches = self._get_batches()
|
||||
edit_mode = self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment)
|
||||
def _init_payments(self, to_process, edit_mode=False):
|
||||
""" Create the payments.
|
||||
|
||||
to_reconcile = []
|
||||
if edit_mode:
|
||||
payment_vals = self._create_payment_vals_from_wizard()
|
||||
payment_vals_list = [payment_vals]
|
||||
to_reconcile.append(batches[0]['lines'])
|
||||
else:
|
||||
# Don't group payments: Create one batch per move.
|
||||
if not self.group_payment:
|
||||
new_batches = []
|
||||
for batch_result in batches:
|
||||
for line in batch_result['lines']:
|
||||
new_batches.append({
|
||||
**batch_result,
|
||||
'lines': line,
|
||||
})
|
||||
batches = new_batches
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
|
||||
payment_vals_list = []
|
||||
for batch_result in batches:
|
||||
payment_vals_list.append(self._create_payment_vals_from_batch(batch_result))
|
||||
to_reconcile.append(batch_result['lines'])
|
||||
payments = self.env['account.payment'].create([x['create_vals'] for x in to_process])
|
||||
|
||||
payments = self.env['account.payment'].create(payment_vals_list)
|
||||
for payment, vals in zip(payments, to_process):
|
||||
vals['payment'] = payment
|
||||
|
||||
# If payments are made using a currency different than the source one, ensure the balance match exactly in
|
||||
# order to fully paid the source journal items.
|
||||
# For example, suppose a new currency B having a rate 100:1 regarding the company currency A.
|
||||
# If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A.
|
||||
if edit_mode:
|
||||
lines = vals['to_reconcile']
|
||||
|
||||
# If payments are made using a currency different than the source one, ensure the balance match exactly in
|
||||
# order to fully paid the source journal items.
|
||||
# For example, suppose a new currency B having a rate 100:1 regarding the company currency A.
|
||||
# If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A.
|
||||
if edit_mode:
|
||||
for payment, lines in zip(payments, to_reconcile):
|
||||
# Batches are made using the same currency so making 'lines.currency_id' is ok.
|
||||
if payment.currency_id != lines.currency_id:
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
@@ -538,23 +585,78 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
(1, debit_lines[0].id, {'debit': debit_lines[0].debit + delta_balance}),
|
||||
(1, credit_lines[0].id, {'credit': credit_lines[0].credit + delta_balance}),
|
||||
]})
|
||||
return payments
|
||||
|
||||
def _post_payments(self, to_process, edit_mode=False):
|
||||
""" Post the newly created payments.
|
||||
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
payments = self.env['account.payment']
|
||||
for vals in to_process:
|
||||
payments |= vals['payment']
|
||||
payments.action_post()
|
||||
|
||||
def _reconcile_payments(self, to_process, edit_mode=False):
|
||||
""" Reconcile the payments.
|
||||
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
domain = [('account_internal_type', 'in', ('receivable', 'payable')), ('reconciled', '=', False)]
|
||||
for payment, lines in zip(payments, to_reconcile):
|
||||
for vals in to_process:
|
||||
payment_lines = vals['payment'].line_ids.filtered_domain(domain)
|
||||
lines = vals['to_reconcile']
|
||||
|
||||
# When using the payment tokens, the payment could not be posted at this point (e.g. the transaction failed)
|
||||
# and then, we can't perform the reconciliation.
|
||||
if payment.state != 'posted':
|
||||
continue
|
||||
|
||||
payment_lines = payment.line_ids.filtered_domain(domain)
|
||||
for account in payment_lines.account_id:
|
||||
(payment_lines + lines)\
|
||||
.filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\
|
||||
.reconcile()
|
||||
|
||||
def _create_payments(self):
|
||||
self.ensure_one()
|
||||
batches = self._get_batches()
|
||||
edit_mode = self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment)
|
||||
to_process = []
|
||||
|
||||
if edit_mode:
|
||||
payment_vals = self._create_payment_vals_from_wizard()
|
||||
to_process.append({
|
||||
'create_vals': payment_vals,
|
||||
'to_reconcile': batches[0]['lines'],
|
||||
'batch': batches[0],
|
||||
})
|
||||
else:
|
||||
# Don't group payments: Create one batch per move.
|
||||
if not self.group_payment:
|
||||
new_batches = []
|
||||
for batch_result in batches:
|
||||
for line in batch_result['lines']:
|
||||
new_batches.append({
|
||||
**batch_result,
|
||||
'lines': line,
|
||||
})
|
||||
batches = new_batches
|
||||
|
||||
for batch_result in batches:
|
||||
to_process.append({
|
||||
'create_vals': self._create_payment_vals_from_batch(batch_result),
|
||||
'to_reconcile': batch_result['lines'],
|
||||
'batch': batch_result,
|
||||
})
|
||||
|
||||
payments = self._init_payments(to_process, edit_mode=edit_mode)
|
||||
self._post_payments(to_process, edit_mode=edit_mode)
|
||||
self._reconcile_payments(to_process, edit_mode=edit_mode)
|
||||
return payments
|
||||
|
||||
def action_create_payments(self):
|
||||
|
||||
@@ -33,8 +33,7 @@ class CashBox(models.TransientModel):
|
||||
if record.state == 'confirm':
|
||||
raise UserError(_("You cannot put/take money in/out for a bank statement which is closed."))
|
||||
values = box._calculate_values_for_statement_line(record)
|
||||
account = record.journal_id.company_id.transfer_account_id
|
||||
self.env['account.bank.statement.line'].with_context(counterpart_account_id=account.id).sudo().create(values)
|
||||
self.env['account.bank.statement.line'].sudo().create(values)
|
||||
|
||||
|
||||
class CashBoxOut(CashBox):
|
||||
|
||||
@@ -11,6 +11,7 @@ import base64
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -506,21 +507,88 @@ class AccountEdiFormat(models.Model):
|
||||
:param vat: The vat number of the partner.
|
||||
:returns: A partner or an empty recordset if not found.
|
||||
'''
|
||||
domains = []
|
||||
for value, domain in (
|
||||
(name, [('name', 'ilike', name)]),
|
||||
(phone, expression.OR([[('phone', '=', phone)], [('mobile', '=', phone)]])),
|
||||
(mail, [('email', '=', mail)]),
|
||||
(vat, [('vat', 'like', vat)]),
|
||||
):
|
||||
if value is not None:
|
||||
domains.append(domain)
|
||||
def search_with_vat(extra_domain):
|
||||
if not vat:
|
||||
return None
|
||||
|
||||
domain = expression.AND([
|
||||
expression.OR(domains),
|
||||
[('company_id', 'in', [False, self.env.company.id])],
|
||||
])
|
||||
return self.env['res.partner'].search(domain, limit=1)
|
||||
# Sometimes, the vat is specified with some whitespaces.
|
||||
normalized_vat = vat.replace(' ', '')
|
||||
country_prefix = re.match('^[a-zA-Z]{2}|^', vat).group()
|
||||
|
||||
partner = self.env['res.partner'].search(extra_domain + [('vat', 'in', (normalized_vat, vat))], limit=1)
|
||||
|
||||
# Try to remove the country code prefix from the vat.
|
||||
if not partner and country_prefix:
|
||||
partner = self.env['res.partner'].search(extra_domain + [
|
||||
('vat', 'in', (normalized_vat[2:], vat[2:])),
|
||||
('country_id.code', '=', country_prefix.upper()),
|
||||
], limit=1)
|
||||
|
||||
# The country could be not specified on the partner.
|
||||
if not partner:
|
||||
partner = self.env['res.partner'].search(extra_domain + [
|
||||
('vat', 'in', (normalized_vat[2:], vat[2:])),
|
||||
('country_id', '=', False),
|
||||
], limit=1)
|
||||
|
||||
# The vat could be a string of alphanumeric values without country code but with missing zeros at the
|
||||
# beginning.
|
||||
if not partner:
|
||||
try:
|
||||
vat_only_numeric = str(int(re.sub('^\D{2}', '', normalized_vat) or 0))
|
||||
except ValueError:
|
||||
vat_only_numeric = None
|
||||
|
||||
if vat_only_numeric:
|
||||
query = self.env['res.partner']._where_calc(extra_domain + [('active', '=', True)])
|
||||
tables, where_clause, where_params = query.get_sql()
|
||||
|
||||
if country_prefix:
|
||||
vat_prefix_regex = f'({country_prefix})?'
|
||||
else:
|
||||
vat_prefix_regex = '([A-z]{2})?'
|
||||
|
||||
self._cr.execute(f'''
|
||||
SELECT res_partner.id
|
||||
FROM {tables}
|
||||
WHERE {where_clause}
|
||||
AND res_partner.vat ~ %s
|
||||
LIMIT 1
|
||||
''', where_params + ['^%s0*%s$' % (vat_prefix_regex, vat_only_numeric)])
|
||||
partner_row = self._cr.fetchone()
|
||||
if partner_row:
|
||||
partner = self.env['res.partner'].browse(partner_row[0])
|
||||
|
||||
return partner
|
||||
|
||||
def search_with_phone_mail(extra_domain):
|
||||
domains = []
|
||||
if phone:
|
||||
domains.append([('phone', '=', phone)])
|
||||
domains.append([('mobile', '=', phone)])
|
||||
if mail:
|
||||
domains.append([('email', '=', mail)])
|
||||
|
||||
if not domains:
|
||||
return None
|
||||
|
||||
domain = expression.OR(domains)
|
||||
if extra_domain:
|
||||
domain = expression.AND([domain, extra_domain])
|
||||
return self.env['res.partner'].search(domain, limit=1)
|
||||
|
||||
def search_with_name(extra_domain):
|
||||
if not name:
|
||||
return None
|
||||
return self.env['res.partner'].search([('name', 'ilike', name)] + extra_domain, limit=1)
|
||||
|
||||
for search_method in (search_with_vat, search_with_phone_mail, search_with_name):
|
||||
for extra_domain in ([('company_id', '=', self.env.company.id)], []):
|
||||
partner = search_method(extra_domain)
|
||||
if partner:
|
||||
return partner
|
||||
|
||||
return self.env['res.partner']
|
||||
|
||||
def _retrieve_product(self, name=None, default_code=None, barcode=None):
|
||||
'''Search all products and find one that matches one of the parameters.
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import common
|
||||
from . import test_edi
|
||||
from . import test_import_vendor_bill
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from flectra.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestImportVendorBill(AccountTestInvoicingCommon):
|
||||
|
||||
def test_retrieve_partner(self):
|
||||
|
||||
def retrieve_partner(vat, import_vat):
|
||||
self.partner_a.with_context(no_vat_validation=True).vat = vat
|
||||
self.partner_a.flush()
|
||||
return self.env['account.edi.format']._retrieve_partner(vat=import_vat)
|
||||
|
||||
self.assertEqual(self.partner_a, retrieve_partner('BE0477472701', 'BE0477472701'))
|
||||
self.assertEqual(self.partner_a, retrieve_partner('BE0477472701', '0477472701'))
|
||||
self.assertEqual(self.partner_a, retrieve_partner('BE0477472701', '477472701'))
|
||||
self.assertEqual(self.partner_a, retrieve_partner('0477472701', 'BE0477472701'))
|
||||
self.assertEqual(self.partner_a, retrieve_partner('477472701', 'BE0477472701'))
|
||||
self.assertEqual(self.env['res.partner'], retrieve_partner('DE0477472701', 'BE0477472701'))
|
||||
self.assertEqual(self.partner_a, retrieve_partner('CHE-107.787.577 IVA', 'CHE-107.787.577 IVA')) # note that base_vat forces the space
|
||||
@@ -58,7 +58,7 @@ class AccountEdiFormat(models.Model):
|
||||
if not edi_document.attachment_id:
|
||||
return
|
||||
|
||||
pdf_writer.embed_flectra_attachment(edi_document.attachment_id)
|
||||
pdf_writer.embed_flectra_attachment(edi_document.attachment_id, subtype='application/xml')
|
||||
if not pdf_writer.is_pdfa and str2bool(self.env['ir.config_parameter'].sudo().get_param('edi.use_pdfa', 'False')):
|
||||
try:
|
||||
pdf_writer.convert_to_pdfa()
|
||||
@@ -137,7 +137,7 @@ class AccountEdiFormat(models.Model):
|
||||
return self.env['ir.attachment'].create({
|
||||
'name': 'factur-x.xml',
|
||||
'datas': base64.encodebytes(xml_content),
|
||||
'mimetype': '/application#2Fxml'
|
||||
'mimetype': 'application/xml'
|
||||
})
|
||||
|
||||
def _is_facturx(self, filename, tree):
|
||||
|
||||
@@ -17,7 +17,7 @@ import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SERVER_URL = 'https://l10n-it-edi.api.flectrahq.com'
|
||||
DEFAULT_SERVER_URL = 'https://l10n-it-edi.api.flectrahq.com'
|
||||
TIMEOUT = 30
|
||||
|
||||
|
||||
@@ -129,12 +129,14 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
response = {'id_client': 'demo', 'refresh_token': 'demo'}
|
||||
else:
|
||||
try:
|
||||
response = self._make_request(SERVER_URL + '/iap/account_edi/1/create_user', params={
|
||||
# b64encode returns a bytestring, we need it as a string
|
||||
server_url = self.env['ir.config_parameter'].get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
response = self._make_request(server_url + '/iap/account_edi/1/create_user', params={
|
||||
'dbuuid': company.env['ir.config_parameter'].get_param('database.uuid'),
|
||||
'company_id': company.id,
|
||||
'edi_format_code': edi_format.code,
|
||||
'edi_identification': edi_identification,
|
||||
'public_key': base64.b64encode(public_pem)
|
||||
'public_key': base64.b64encode(public_pem).decode()
|
||||
})
|
||||
except AccountEdiProxyError as e:
|
||||
raise UserError(e.message)
|
||||
@@ -157,7 +159,8 @@ class AccountEdiProxyClientUser(models.Model):
|
||||
that multiple database use the same credentials. When receiving an error for an expired refresh_token,
|
||||
This method makes a request to get a new refresh token.
|
||||
'''
|
||||
response = self._make_request(SERVER_URL + '/iap/account_edi/1/renew_token')
|
||||
server_url = self.env['ir.config_parameter'].get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
response = self._make_request(server_url + '/iap/account_edi/1/renew_token')
|
||||
if 'error' in response:
|
||||
# can happen if the database was duplicated and the refresh_token was refreshed by the other database.
|
||||
# we don't want two database to be able to query the proxy with the same user
|
||||
|
||||
@@ -92,11 +92,12 @@ class AccountEdiFormat(models.Model):
|
||||
invoice_form.invoice_incoterm_id = self.env['account.incoterms'].search([('code', '=', elements[0].text)], limit=1)
|
||||
|
||||
# Partner
|
||||
counterpart = 'Customer' if invoice_form.move_type in ('out_invoice', 'out_refund') else 'Supplier'
|
||||
invoice_form.partner_id = self_ctx._retrieve_partner(
|
||||
name=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:Name'),
|
||||
phone=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone'),
|
||||
mail=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:ElectronicMail'),
|
||||
vat=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:CompanyID'),
|
||||
name=_find_value(f'//cac:Accounting{counterpart}Party/cac:Party//cbc:Name'),
|
||||
phone=_find_value(f'//cac:Accounting{counterpart}Party/cac:Party//cbc:Telephone'),
|
||||
mail=_find_value(f'//cac:Accounting{counterpart}Party/cac:Party//cbc:ElectronicMail'),
|
||||
vat=_find_value(f'//cac:Accounting{counterpart}Party/cac:Party//cbc:CompanyID'),
|
||||
)
|
||||
|
||||
# Lines
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<h3>Contractual Relationship</h3>
|
||||
<p>The payment processing services ordered by you by placing this order will be
|
||||
provided to you by Adyen N.V. (hereafter “Processor”), with which you are
|
||||
entering into a direct agreement by confirming this order. FlectraHQ Inc., Odoo S.A.
|
||||
entering into a direct agreement by confirming this order. FlectraHQ, Inc., Odoo S.A.
|
||||
(hereafter “We /Us”) will assist and support you in your use of the services to be
|
||||
provided by the Processor and we will provide you first line assistance with and
|
||||
enable you to connect to the systems of Processor to be able to use its services.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
Allow users to login through OAuth2 Provider.
|
||||
=============================================
|
||||
""",
|
||||
'maintainer': 'FlectraHQ Inc., Odoo S.A.',
|
||||
'maintainer': 'FlectraHQ, Inc., Odoo S.A.',
|
||||
'depends': ['base', 'web', 'base_setup', 'auth_signup'],
|
||||
'data': [
|
||||
'data/auth_oauth_data.xml',
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<flectra>
|
||||
<data noupdate="1">
|
||||
<record id="provider_openerp" model="auth.oauth.provider">
|
||||
<field name="name">flectrahq.com Accounts</field>
|
||||
<field name="name">Flectrahq.com Accounts</field>
|
||||
<field name="auth_endpoint">https://accounts.flectrahq.com/oauth2/auth</field>
|
||||
<field name="scope">userinfo</field>
|
||||
<field name="validation_endpoint">https://accounts.flectrahq.com/oauth2/tokeninfo</field>
|
||||
<field name="data_endpoint"></field>
|
||||
<field name="css_class">fa fa-fw o_custom_icon</field>
|
||||
<field name="body">Log in with flectrahq.com</field>
|
||||
<field name="body">Log in with Flectrahq.com</field>
|
||||
<field name="enabled" eval="True"/>
|
||||
</record>
|
||||
<record id="provider_facebook" model="auth.oauth.provider">
|
||||
|
||||
@@ -5,7 +5,7 @@ import werkzeug
|
||||
|
||||
from flectra import http, _
|
||||
from flectra.addons.auth_signup.models.res_users import SignupError
|
||||
from flectra.addons.web.controllers.main import ensure_db, Home
|
||||
from flectra.addons.web.controllers.main import ensure_db, Home, SIGN_UP_REQUEST_PARAMS
|
||||
from flectra.addons.base_setup.controllers.main import BaseSetup
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.http import request
|
||||
@@ -101,7 +101,7 @@ class AuthSignupHome(Home):
|
||||
|
||||
def get_auth_signup_qcontext(self):
|
||||
""" Shared helper returning the rendering context for signup and reset password """
|
||||
qcontext = request.params.copy()
|
||||
qcontext = {k: v for (k, v) in request.params.items() if k in SIGN_UP_REQUEST_PARAMS}
|
||||
qcontext.update(self.get_auth_signup_config())
|
||||
if not qcontext.get('token') and request.session.get('auth_signup_token'):
|
||||
qcontext['token'] = request.session.get('auth_signup_token')
|
||||
|
||||
@@ -24,6 +24,11 @@ class Partner(models.Model):
|
||||
self.zip = False
|
||||
self.state_id = False
|
||||
|
||||
@api.model
|
||||
def _address_fields(self):
|
||||
"""Returns the list of address fields that are synced from the parent."""
|
||||
return super(Partner, self)._address_fields() + ['city_id',]
|
||||
|
||||
@api.model
|
||||
def _fields_view_get_address(self, arch):
|
||||
arch = super(Partner, self)._fields_view_get_address(arch)
|
||||
|
||||
@@ -230,7 +230,7 @@ class BaseAutomation(models.Model):
|
||||
e.context['exception_class'] = 'base_automation'
|
||||
e.context['base_automation'] = {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'name': self.sudo().name,
|
||||
}
|
||||
|
||||
def _process(self, records, domain_post=None):
|
||||
@@ -256,7 +256,8 @@ class BaseAutomation(models.Model):
|
||||
records.write(values)
|
||||
|
||||
# execute server actions
|
||||
if self.action_server_id:
|
||||
action_server = self.action_server_id
|
||||
if action_server:
|
||||
for record in records:
|
||||
# we process the action if any watched field has been modified
|
||||
if self._check_trigger_fields(record):
|
||||
@@ -267,7 +268,7 @@ class BaseAutomation(models.Model):
|
||||
'domain_post': domain_post,
|
||||
}
|
||||
try:
|
||||
self.action_server_id.sudo().with_context(**ctx).run()
|
||||
action_server.sudo().with_context(**ctx).run()
|
||||
except Exception as e:
|
||||
self._add_postmortem_action(e)
|
||||
raise e
|
||||
|
||||
@@ -46,7 +46,9 @@ class Attendee(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if values.get('partner_id') == self.env.user.partner_id.id:
|
||||
# by default, if no state is given for the attendee corresponding to the current user
|
||||
# that means he's the event organizer so we can set his state to "accepted"
|
||||
if 'state' not in values and values.get('partner_id') == self.env.user.partner_id.id:
|
||||
values['state'] = 'accepted'
|
||||
if not values.get("email") and values.get("common_name"):
|
||||
common_nameval = values.get("common_name").split(':')
|
||||
|
||||
@@ -707,8 +707,12 @@ class Meeting(models.Model):
|
||||
activity_vals['user_id'] = user_id
|
||||
values['activity_ids'] = [(0, 0, activity_vals)]
|
||||
|
||||
# Add commands to create attendees from partners (if present) if no attendee command
|
||||
# is already given (coming from Google event for example).
|
||||
vals_list = [
|
||||
dict(vals, attendee_ids=self._attendees_values(vals['partner_ids'])) if 'partner_ids' in vals else vals
|
||||
dict(vals, attendee_ids=self._attendees_values(vals['partner_ids']))
|
||||
if 'partner_ids' in vals and not vals.get('attendee_ids')
|
||||
else vals
|
||||
for vals in vals_list
|
||||
]
|
||||
recurrence_fields = self._get_recurrent_fields()
|
||||
@@ -784,6 +788,21 @@ class Meeting(models.Model):
|
||||
|
||||
return public_events + my_private_events + obfuscated(others_private_events)
|
||||
|
||||
def name_get(self):
|
||||
""" Hide private events' name for events which don't belong to the current user
|
||||
"""
|
||||
hidden = self.filtered(
|
||||
lambda evt:
|
||||
evt.privacy == 'private' and
|
||||
evt.user_id.id != self.env.uid and
|
||||
self.env.user.partner_id not in evt.partner_ids
|
||||
)
|
||||
|
||||
shown = self - hidden
|
||||
shown_names = super(Meeting, shown).name_get()
|
||||
obfuscated_names = [(eid, _('Busy')) for eid in hidden.ids]
|
||||
return shown_names + obfuscated_names
|
||||
|
||||
@api.model
|
||||
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
groupby = [groupby] if isinstance(groupby, str) else groupby
|
||||
@@ -820,7 +839,7 @@ class Meeting(models.Model):
|
||||
if 'name' in fields:
|
||||
activity_values['summary'] = event.name
|
||||
if 'description' in fields:
|
||||
activity_values['note'] = tools.plaintext2html(event.description)
|
||||
activity_values['note'] = event.description and tools.plaintext2html(event.description)
|
||||
if 'start' in fields:
|
||||
# self.start is a datetime UTC *only when the event is not allday*
|
||||
# activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is)
|
||||
|
||||
@@ -36,6 +36,28 @@ class TestEventNotifications(SavepointCase):
|
||||
self.assertEqual(event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
|
||||
self.assertIn(self.partner, event.message_follower_ids.partner_id, "He should be follower of the event")
|
||||
|
||||
def test_attendee_added_create_with_specific_states(self):
|
||||
"""
|
||||
When an event is created from an external calendar account (such as Google) which is not linked to an
|
||||
Flectra account, attendee info such as email and state are given at sync.
|
||||
In this case, attendee_ids should be created accordingly.
|
||||
"""
|
||||
organizer_partner = self.env['res.partner'].create({'name': "orga", "email": "orga@google.com"})
|
||||
event = self.env['calendar.event'].with_user(self.user).create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
'attendee_ids': [
|
||||
(0, 0, {'partner_id': self.partner.id, 'state': 'needsAction'}),
|
||||
(0, 0, {'partner_id': organizer_partner.id, 'state': 'accepted'})
|
||||
],
|
||||
'partner_ids': [(4, self.partner.id), (4, organizer_partner.id)],
|
||||
})
|
||||
attendees_info = [(a.email, a.state) for a in event.attendee_ids]
|
||||
self.assertEqual(len(event.attendee_ids), 2)
|
||||
self.assertIn((self.partner.email, "needsAction"), attendees_info)
|
||||
self.assertIn((organizer_partner.email, "accepted"), attendees_info)
|
||||
|
||||
def test_attendee_added_multi(self):
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "Doom's day",
|
||||
@@ -68,3 +90,44 @@ class TestEventNotifications(SavepointCase):
|
||||
self.assertNotIn(self.partner, self.event.attendee_ids.partner_id, "It should have removed the attendee")
|
||||
self.assertNotIn(self.partner, self.event.message_follower_ids.partner_id, "It should have unsubscribed the partner")
|
||||
self.assertIn(partner_bis, self.event.attendee_ids.partner_id, "It should have left the attendee")
|
||||
|
||||
def test_default_attendee(self):
|
||||
"""
|
||||
Check if priority list id correctly followed
|
||||
1) vals_list[0]['attendee_ids']
|
||||
2) vals_list[0]['partner_ids']
|
||||
3) context.get('default_attendee_ids')
|
||||
"""
|
||||
partner_bis = self.env['res.partner'].create({'name': "Xavier"})
|
||||
event = self.env['calendar.event'].with_user(
|
||||
self.user
|
||||
).with_context(
|
||||
default_attendee_ids=[(0, 0, {'partner_id': partner_bis.id})]
|
||||
).create({
|
||||
'name': "Doom's day",
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
})
|
||||
self.assertIn(self.partner, event.attendee_ids.partner_id, "Partner should be in attendee")
|
||||
self.assertNotIn(partner_bis, event.attendee_ids.partner_id, "Partner bis should not be in attendee")
|
||||
|
||||
def test_default_attendee_2(self):
|
||||
"""
|
||||
Check if priority list id correctly followed
|
||||
1) vals_list[0]['attendee_ids']
|
||||
2) vals_list[0]['partner_ids']
|
||||
3) context.get('default_attendee_ids')
|
||||
"""
|
||||
partner_bis = self.env['res.partner'].create({'name': "Xavier"})
|
||||
event = self.env['calendar.event'].with_user(
|
||||
self.user
|
||||
).with_context(
|
||||
default_attendee_ids=[(0, 0, {'partner_id': partner_bis.id})]
|
||||
).create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
})
|
||||
self.assertNotIn(self.partner, event.attendee_ids.partner_id, "Partner should not be in attendee")
|
||||
self.assertIn(partner_bis, event.attendee_ids.partner_id, "Partner bis should be in attendee")
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
|
||||
'stop': '2011-04-30 18:30:00',
|
||||
'description': 'The Technical Presentation will cover following topics:\n* Creating Flectra class\n* Views\n* Wizards\n* Workflows',
|
||||
'duration': 2.5,
|
||||
'location': 'FlectraHQ Inc., Odoo S.A.',
|
||||
'location': 'FlectraHQ, Inc., Odoo S.A.',
|
||||
'name': 'Technical Presentation'
|
||||
})
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
|
||||
<page name="page_invitations" string="Invitations" groups="base.group_no_one">
|
||||
<button name="action_sendmail" type="object" string="Send mail" icon="fa-envelope" class="oe_link"/>
|
||||
<field name="attendee_ids" widget="one2many" mode="tree,kanban">
|
||||
<field name="attendee_ids" widget="one2many" mode="tree,kanban" readonly="1">
|
||||
<tree string="Invitation details" editable="top" create="false" delete="false">
|
||||
<field name="partner_id" />
|
||||
<field name="state" />
|
||||
|
||||
@@ -36,7 +36,7 @@ class StockMoveLine(models.Model):
|
||||
for move_line in self:
|
||||
if move_line.move_id.sale_line_id:
|
||||
unit_price = move_line.move_id.sale_line_id.price_reduce_taxinc
|
||||
qty = move_line.product_uom_id._compute_quantity(move_line.move_id.sale_line_id.product_qty, move_line.move_id.sale_line_id.product_uom)
|
||||
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.move_id.sale_line_id.product_uom)
|
||||
else:
|
||||
unit_price = move_line.product_id.list_price
|
||||
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id)
|
||||
|
||||
@@ -40,10 +40,6 @@ class StockMoveInvoice(AccountTestInvoicingCommon):
|
||||
|
||||
def test_01_delivery_stock_move(self):
|
||||
# Test if the stored fields of stock moves are computed with invoice before delivery flow
|
||||
self.product_11.write({
|
||||
'weight': 0.25,
|
||||
})
|
||||
|
||||
self.sale_prepaid = self.SaleOrder.create({
|
||||
'partner_id': self.partner_18.id,
|
||||
'partner_invoice_id': self.partner_18.id,
|
||||
@@ -97,4 +93,50 @@ class StockMoveInvoice(AccountTestInvoicingCommon):
|
||||
self.assertEqual(moves[0].weight, 2.0, 'wrong move weight')
|
||||
|
||||
# Ship
|
||||
moves.move_line_ids.write({'qty_done': 2})
|
||||
self.picking = self.sale_prepaid.picking_ids._action_done()
|
||||
self.assertEqual(moves[0].move_line_ids.sale_price, 1725.0, 'wrong shipping value')
|
||||
|
||||
def test_02_delivery_stock_move(self):
|
||||
# Test if SN product shipment line has the correct amount
|
||||
self.product_cable_management_box.write({
|
||||
'tracking': 'serial'
|
||||
})
|
||||
|
||||
serial_numbers = self.env['stock.production.lot'].create([{
|
||||
'name': str(x),
|
||||
'product_id': self.product_cable_management_box.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for x in range(5)])
|
||||
|
||||
|
||||
self.sale_prepaid = self.SaleOrder.create({
|
||||
'partner_id': self.partner_18.id,
|
||||
'partner_invoice_id': self.partner_18.id,
|
||||
'partner_shipping_id': self.partner_18.id,
|
||||
'pricelist_id': self.pricelist_id.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': 'Cable Management Box',
|
||||
'product_id': self.product_cable_management_box.id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom': self.product_uom_unit.id,
|
||||
'price_unit': 750.00,
|
||||
})],
|
||||
})
|
||||
|
||||
# I add delivery cost in Sales order
|
||||
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
|
||||
'default_order_id': self.sale_prepaid.id,
|
||||
'default_carrier_id': self.normal_delivery.id,
|
||||
}))
|
||||
choose_delivery_carrier = delivery_wizard.save()
|
||||
choose_delivery_carrier.button_confirm()
|
||||
|
||||
# I confirm the SO.
|
||||
self.sale_prepaid.action_confirm()
|
||||
moves = self.sale_prepaid.picking_ids.move_lines
|
||||
# Ship
|
||||
for ml, lot in zip(moves.move_line_ids, serial_numbers):
|
||||
ml.write({'qty_done': 1, 'lot_id': lot.id})
|
||||
self.picking = self.sale_prepaid.picking_ids._action_done()
|
||||
self.assertEqual(moves[0].move_line_ids[0].sale_price, 862.5, 'wrong shipping value')
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
font-family: Arial, Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
#header_background {
|
||||
background-color: #2496f6;
|
||||
background-color: #009EFB;
|
||||
}
|
||||
.global_layout {
|
||||
max-width: 588px;
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
.button {
|
||||
float: right;
|
||||
background-color: #2496f6;
|
||||
background-color: #009EFB;
|
||||
color: #ffffff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -142,7 +142,7 @@
|
||||
}
|
||||
.flectra_link_text {
|
||||
font-weight: bold;
|
||||
color: #2496f6;
|
||||
color: #009EFB;
|
||||
}
|
||||
.run_business {
|
||||
color: #2d2a26;
|
||||
@@ -238,8 +238,8 @@
|
||||
}
|
||||
#header {
|
||||
padding: 20px 30px 25px 30px;
|
||||
border-left: 1px solid #2496f6;
|
||||
border-right: 1px solid #2496f6;
|
||||
border-left: 1px solid #009EFB;
|
||||
border-right: 1px solid #009EFB;
|
||||
}
|
||||
.global_layout {
|
||||
padding: 25px 30px 30px 30px;
|
||||
@@ -406,10 +406,10 @@
|
||||
<div style="width: 50%; float: left;">
|
||||
<p class="run_business">Run your business from anywhere with <b>Flectra Mobile</b>.</p>
|
||||
<div>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.flectrahq" target="_blank"><img class="download_app" src="https://www.flectrahq.com/digest/static/src/img/google_play.png" /></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.mobile" target="_blank"><img class="download_app" src="https://www.flectrahq.com/digest/static/src/img/google_play.png" /></a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://itunes.apple.com/us/app/flectra/id1561830563" target="_blank"><img class="download_app" src="https://www.flectrahq.com/digest/static/src/img/app_store.png" /></a>
|
||||
<a href="https://itunes.apple.com/us/app/flectra/id1272543640" target="_blank"><img class="download_app" src="https://www.flectrahq.com/digest/static/src/img/app_store.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,10 @@ class SaleOrder(models.Model):
|
||||
.with_context(default_sale_order_id=so.id) \
|
||||
._for_xml_id('event_sale.action_sale_order_event_registration')
|
||||
return res
|
||||
|
||||
def action_cancel(self):
|
||||
|
||||
def _action_cancel(self):
|
||||
self.order_line._cancel_associated_registrations()
|
||||
return super(SaleOrder, self).action_cancel()
|
||||
return super()._action_cancel()
|
||||
|
||||
def action_view_attendee_list(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("event.event_registration_action_tree")
|
||||
|
||||
@@ -191,6 +191,8 @@ flectra_mailgate: "|/path/to/flectra-mailgate.py --host=localhost -u %(uid)d -p
|
||||
elif server.server_type == 'pop':
|
||||
try:
|
||||
while True:
|
||||
failed_in_loop = 0
|
||||
num = 0
|
||||
pop_server = server.connect()
|
||||
(num_messages, total_size) = pop_server.stat()
|
||||
pop_server.list()
|
||||
@@ -204,11 +206,13 @@ flectra_mailgate: "|/path/to/flectra-mailgate.py --host=localhost -u %(uid)d -p
|
||||
except Exception:
|
||||
_logger.info('Failed to process mail from %s server %s.', server.server_type, server.name, exc_info=True)
|
||||
failed += 1
|
||||
failed_in_loop += 1
|
||||
self.env.cr.commit()
|
||||
if num_messages < MAX_POP_MESSAGES:
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", num, server.server_type, server.name, (num - failed_in_loop), failed_in_loop)
|
||||
# Stop if (1) no more message left or (2) all messages have failed
|
||||
if num_messages < MAX_POP_MESSAGES or failed_in_loop == num:
|
||||
break
|
||||
pop_server.quit()
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", num_messages, server.server_type, server.name, (num_messages - failed), failed)
|
||||
except Exception:
|
||||
_logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type, server.name, exc_info=True)
|
||||
finally:
|
||||
|
||||
@@ -105,9 +105,17 @@ class FleetVehicleLogContract(models.Model):
|
||||
delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
|
||||
date_today = fields.Date.from_string(fields.Date.today())
|
||||
outdated_days = fields.Date.to_string(date_today + relativedelta(days=+delay_alert_contract))
|
||||
nearly_expired_contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', outdated_days)])
|
||||
reminder_activity_type = self.env.ref('fleet.mail_act_fleet_contract_to_renew', raise_if_not_found=False) or self.env['mail.activity.type']
|
||||
nearly_expired_contracts = self.search([
|
||||
('state', '=', 'open'),
|
||||
('expiration_date', '<', outdated_days),
|
||||
('user_id', '!=', False)
|
||||
]
|
||||
).filtered(
|
||||
lambda nec: reminder_activity_type not in nec.activity_ids.activity_type_id
|
||||
)
|
||||
|
||||
for contract in nearly_expired_contracts.filtered(lambda contract: contract.user_id):
|
||||
for contract in nearly_expired_contracts:
|
||||
contract.activity_schedule(
|
||||
'fleet.mail_act_fleet_contract_to_renew', contract.expiration_date,
|
||||
user_id=contract.user_id.id)
|
||||
|
||||
@@ -69,7 +69,7 @@ class GoogleCalendarService():
|
||||
def patch(self, event_id, values, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
|
||||
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
|
||||
self.google_service._do_request(url, json.dumps(values), headers, method='PUT', timeout=timeout)
|
||||
self.google_service._do_request(url, json.dumps(values), headers, method='PATCH', timeout=timeout)
|
||||
|
||||
@requires_auth_token
|
||||
def delete(self, event_id, token=None, timeout=TIMEOUT):
|
||||
|
||||
@@ -75,7 +75,7 @@ class GoogleDrive(models.Model):
|
||||
try:
|
||||
req = requests.post(
|
||||
'https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/batch?%s' % (spreadsheet_key, werkzeug.urls.url_encode({'v': 3, 'access_token': access_token})),
|
||||
data=request,
|
||||
data=request.encode('utf-8'),
|
||||
headers={'content-type': 'application/atom+xml', 'If-Match': '*'},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -105,7 +105,7 @@ class HrEmployeePrivate(models.Model):
|
||||
string='Tags')
|
||||
# misc
|
||||
notes = fields.Text('Notes', groups="hr.group_hr_user")
|
||||
color = fields.Integer('Color Index', default=0, groups="hr.group_hr_user")
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", groups="hr.group_hr_user", copy=False)
|
||||
pin = fields.Char(string="PIN", groups="hr.group_hr_user", copy=False,
|
||||
help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).")
|
||||
|
||||
@@ -39,6 +39,12 @@ var KioskMode = AbstractAction.extend({
|
||||
return Promise.all([def, this._super.apply(this, arguments)]);
|
||||
},
|
||||
|
||||
on_attach_callback: function () {
|
||||
// Stop polling to avoid notifications in kiosk mode
|
||||
this.call('bus_service', 'stopPolling');
|
||||
$('body').find('.o_ChatWindowHeader_commandClose').click();
|
||||
},
|
||||
|
||||
_onBarcodeScanned: function(barcode) {
|
||||
var self = this;
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
@@ -69,6 +75,7 @@ var KioskMode = AbstractAction.extend({
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
clearInterval(this.clock_start);
|
||||
clearInterval(this._interval);
|
||||
this.call('bus_service', 'startPolling');
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class HrExpense(models.Model):
|
||||
# product_id not required to allow create an expense without product via mail alias, but should be required on the view.
|
||||
product_id = fields.Many2one('product.product', string='Product', readonly=True, tracking=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='restrict')
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', compute='_compute_from_product_id_company_id',
|
||||
store=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]},
|
||||
store=True, copy=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]},
|
||||
default=_default_product_uom_id, domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
|
||||
unit_amount = fields.Float("Unit Price", compute='_compute_from_product_id_company_id', store=True, required=True, copy=True,
|
||||
|
||||
@@ -28,13 +28,13 @@ flectra.define('hr_expense.expenses.tree', function (require) {
|
||||
async _renderView() {
|
||||
const self = this;
|
||||
await this._super(...arguments);
|
||||
const google_url = "https://play.google.com/store/apps/details?id=com.flectra.flectrahq";
|
||||
const apple_url = "https://apps.apple.com/be/app/flectra/id1561830563";
|
||||
const google_url = "https://play.google.com/store/apps/details?id=com.flectra.mobile";
|
||||
const apple_url = "https://apps.apple.com/be/app/flectra/id1272543640";
|
||||
const action_desktop = {
|
||||
name: 'Download our App',
|
||||
type: 'ir.actions.client',
|
||||
tag: 'expense_qr_code_modal',
|
||||
params: {'url': "https://apps.apple.com/be/app/flectra/id1561830563"},
|
||||
params: {'url': "https://apps.apple.com/be/app/flectra/id1272543640"},
|
||||
target: 'new',
|
||||
};
|
||||
this.$el.find('img.o_expense_apple_store').on('click', function(event) {
|
||||
|
||||
@@ -290,5 +290,9 @@ class TestExpenses(TestExpenseCommon):
|
||||
sheet.action_sheet_move_create()
|
||||
action_data = sheet.action_register_payment()
|
||||
wizard = Form(self.env['account.payment.register'].with_context(action_data['context'])).save()
|
||||
wizard.action_create_payments()
|
||||
action = wizard.action_create_payments()
|
||||
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
|
||||
|
||||
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')
|
||||
|
||||
@@ -398,10 +398,10 @@
|
||||
</p>
|
||||
<p>Snap pictures of your receipts and let Flectra<br/> automatically create expenses for you.</p>
|
||||
<p>
|
||||
<a href="https://apps.apple.com/be/app/flectra/id1561830563" target="_blank">
|
||||
<a href="https://apps.apple.com/be/app/flectra/id1272543640" target="_blank">
|
||||
<img alt="Apple App Store" class="img img-fluid h-100 o_expense_apple_store" src="/hr_expense/static/img/app_store.png"/>
|
||||
</a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.flectrahq" target="_blank" class="o_expense_google_store">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.mobile" target="_blank" class="o_expense_google_store">
|
||||
<img alt="Google Play Store" class="img img-fluid h-100 o_expense_google_store" src="/hr_expense/static/img/play_store.png"/>
|
||||
</a>
|
||||
</p>
|
||||
@@ -433,10 +433,10 @@
|
||||
</p>
|
||||
<p>Snap pictures of your receipts and let Flectra<br/> automatically create expenses for you.</p>
|
||||
<p>
|
||||
<a href="https://apps.apple.com/be/app/flectra/id1561830563" target="_blank">
|
||||
<a href="https://apps.apple.com/be/app/flectra/id1272543640" target="_blank">
|
||||
<img alt="Apple App Store" class="img img-fluid h-100 o_expense_apple_store" src="/hr_expense/static/img/app_store.png"/>
|
||||
</a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.flectrahq" target="_blank" class="o_expense_google_store">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.flectra.mobile" target="_blank" class="o_expense_google_store">
|
||||
<img alt="Google Play Store" class="img img-fluid h-100 o_expense_google_store" src="/hr_expense/static/img/play_store.png"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -19,13 +19,21 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
res['partner_bank_id'] = expense_sheet.employee_id.bank_account_id.id or line.partner_id.bank_ids and line.partner_id.bank_ids.ids[0]
|
||||
return res
|
||||
|
||||
def _create_payments(self):
|
||||
# OVERRIDE to set the 'done' state on expense sheets.
|
||||
payments = super()._create_payments()
|
||||
|
||||
expense_sheets = self.env['hr.expense.sheet'].search([('account_move_id', 'in', self.line_ids.move_id.ids)])
|
||||
for expense_sheet in expense_sheets:
|
||||
if expense_sheet.currency_id.is_zero(expense_sheet.amount_residual):
|
||||
expense_sheet.state = 'done'
|
||||
|
||||
def _init_payments(self, to_process, edit_mode=False):
|
||||
# OVERRIDE
|
||||
payments = super()._init_payments(to_process, edit_mode=edit_mode)
|
||||
for payment, vals in zip(payments, to_process):
|
||||
expenses = vals['batch']['lines'].expense_id
|
||||
if expenses:
|
||||
payment.line_ids.write({'expense_id': expenses[0].id})
|
||||
return payments
|
||||
|
||||
def _reconcile_payments(self, to_process, edit_mode=False):
|
||||
# OVERRIDE
|
||||
res = super()._reconcile_payments(to_process, edit_mode=edit_mode)
|
||||
for vals in to_process:
|
||||
expense_sheets = vals['batch']['lines'].expense_id.sheet_id
|
||||
for expense_sheet in expense_sheets:
|
||||
if expense_sheet.currency_id.is_zero(expense_sheet.amount_residual):
|
||||
expense_sheet.state = 'done'
|
||||
return res
|
||||
|
||||
@@ -67,6 +67,9 @@ class HrFleet(Controller):
|
||||
page.mergePage(header_pdf.getPage(0))
|
||||
writer.addPage(page)
|
||||
|
||||
if not writer.getNumPages():
|
||||
request.not_found(_('There is no pdf attached to generate a claim report.'))
|
||||
|
||||
_buffer = io.BytesIO()
|
||||
writer.write(_buffer)
|
||||
merged_pdf = _buffer.getvalue()
|
||||
|
||||
@@ -8,7 +8,7 @@ access_hr_holidays_employee_allocation,hr.holidays.employee.allocation,model_hr_
|
||||
access_hr_holidays_status_manager,hr.holidays.status manager,model_hr_leave_type,hr_holidays.group_hr_holidays_manager,1,1,1,1
|
||||
access_hr_holidays_status_user,hr.holidays.status user,model_hr_leave_type,hr_holidays.group_hr_holidays_user,1,0,0,0
|
||||
access_hr_holidays_status_employee,hr.holidays.status employee,model_hr_leave_type,base.group_user,1,0,0,0
|
||||
access_hr_leave_report,access_hr_leave_report,model_hr_leave_report,,1,0,0,0
|
||||
access_hr_leave_report,access_hr_leave_report,model_hr_leave_report,base.group_user,1,0,0,0
|
||||
access_resource_calendar_leaves_user,resource_calendar_leaves_user,resource.model_resource_calendar_leaves,hr_holidays.group_hr_holidays_user,1,1,1,1
|
||||
access_calendar_event_hr_user,calendar.event.hr.user,calendar.model_calendar_event,hr_holidays.group_hr_holidays_user,1,1,1,1
|
||||
access_calendar_event_type_manager,calendar.event.type.manager,calendar.model_calendar_event_type,hr_holidays.group_hr_holidays_manager,1,1,1,1
|
||||
|
||||
|
@@ -295,7 +295,9 @@ class TestCompanyLeave(SavepointCase):
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': self.paid_time_off.id,
|
||||
'request_date_from': date(2020, 3, 29),
|
||||
'date_from': datetime(2020, 3, 29, 7, 0, 0),
|
||||
'request_date_to': date(2020, 4, 1),
|
||||
'date_to': datetime(2020, 4, 1, 19, 0, 0),
|
||||
'number_of_days': 3,
|
||||
} for employee in employees[0:15]])
|
||||
leaves._compute_date_from_to()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_employee_deletion
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.tests import Form, tagged, TransactionCase
|
||||
from flectra.exceptions import MissingError
|
||||
|
||||
@tagged('post_install')
|
||||
class TestEmployeeDeletion(TransactionCase):
|
||||
|
||||
def test_employee_deletion(self):
|
||||
# Tests an issue with the form view where the employee could be deleted
|
||||
employee_a, employee_b = self.env['hr.employee'].create([
|
||||
{
|
||||
'name': 'A',
|
||||
},
|
||||
{
|
||||
'name': 'B',
|
||||
},
|
||||
])
|
||||
department_a, department_b = self.env['hr.department'].create([
|
||||
{
|
||||
'name': 'DEP A',
|
||||
'manager_id': employee_a.id,
|
||||
},
|
||||
{
|
||||
'name': 'DEP B',
|
||||
'manager_id': employee_b.id,
|
||||
},
|
||||
])
|
||||
employee_a.write({
|
||||
'parent_id': employee_a.id,
|
||||
'coach_id': employee_a.id,
|
||||
'department_id': department_a.id,
|
||||
})
|
||||
try:
|
||||
with Form(employee_a) as form:
|
||||
form.department_id = department_b
|
||||
except MissingError:
|
||||
self.fail('The employee should not have been deleted')
|
||||
@@ -8,7 +8,7 @@
|
||||
<div id="o_work_employee_main" position="after">
|
||||
<div id="o_employee_right">
|
||||
<h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="child_ids" widget="hr_org_chart" readonly="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
@@ -22,7 +22,7 @@
|
||||
<xpath expr="//div[@id='o_work_employee_main']" position="after">
|
||||
<div id="o_employee_right">
|
||||
<h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="child_ids" widget="hr_org_chart" readonly="1"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
By setting an alias to a job position, emails sent to this address create applications automatically. You can even use multiple trackers to get statistics according to the source of the application: LinkedIn, Monster, Indeed, etc.
|
||||
% set record = object.env['hr.job'].search([('alias_name', '!=', False)], limit=1)
|
||||
% if record and record.alias_domain
|
||||
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #2496f6; text-decoration: none;">Try sending an email</a>
|
||||
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #009EFB; text-decoration: none;">Try sending an email</a>
|
||||
% endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
% if 'website_url' in object.job_id and object.job_id.website_url:
|
||||
<div style="margin: 16px 8px 16px 8px;">
|
||||
<a href="${object.job_id.website_url}"
|
||||
style="background-color: #2496f6; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
style="background-color: #009EFB; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
% if 'website_url' in object.job_id and object.job_id.website_url:
|
||||
<div style="margin: 16px 8px 16px 8px;">
|
||||
<a href="${object.job_id.website_url}"
|
||||
style="background-color: #2496f6; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
style="background-color: #009EFB; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ class Applicant(models.Model):
|
||||
'default_name': applicant.partner_name or contact_name,
|
||||
'default_job_id': applicant.job_id.id,
|
||||
'default_job_title': applicant.job_id.name,
|
||||
'address_home_id': address_id,
|
||||
'default_address_home_id': address_id,
|
||||
'default_department_id': applicant.department_id.id or False,
|
||||
'default_address_id': applicant.company_id and applicant.company_id.partner_id
|
||||
and applicant.company_id.partner_id.id or False,
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
|
||||
<record id="employee_resume_line_admin_4" model="hr.resume.line">
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="name">FlectraHQ Inc.</field>
|
||||
<field name="name">FlectraHQ, Inc.</field>
|
||||
<field name="date_start" eval="(datetime.now()+relativedelta(years=-3)).strftime('%Y-11-01')"/>
|
||||
<field name="line_type_id" ref="resume_type_experience"/>
|
||||
<field name="description">
|
||||
|
||||
@@ -16,7 +16,7 @@ except ImportError:
|
||||
slugify_lib = None
|
||||
|
||||
import flectra
|
||||
from flectra import api, models, registry, exceptions, tools
|
||||
from flectra import api, models, registry, exceptions, tools, http
|
||||
from flectra.addons.base.models.ir_http import RequestUID, ModelConverter
|
||||
from flectra.addons.base.models.qweb import QWebException
|
||||
from flectra.http import request
|
||||
@@ -653,14 +653,14 @@ class IrHttp(models.AbstractModel):
|
||||
@tools.ormcache('path')
|
||||
def url_rewrite(self, path):
|
||||
new_url = False
|
||||
req = request.httprequest
|
||||
router = req.app.get_db_router(request.db).bind('')
|
||||
router = http.root.get_db_router(request.db).bind('')
|
||||
try:
|
||||
_ = router.match(path, method='POST')
|
||||
except werkzeug.exceptions.MethodNotAllowed:
|
||||
_ = router.match(path, method='GET')
|
||||
except werkzeug.routing.RequestRedirect as e:
|
||||
new_url = e.new_url[7:] # remove scheme
|
||||
# get path from http://{path}?{current query string}
|
||||
new_url = e.new_url.split('?')[0][7:]
|
||||
except werkzeug.exceptions.NotFound:
|
||||
new_url = path
|
||||
except Exception as e:
|
||||
@@ -672,7 +672,7 @@ class IrHttp(models.AbstractModel):
|
||||
@api.model
|
||||
@tools.cache('path', 'query_args')
|
||||
def _get_endpoint_qargs(self, path, query_args=None):
|
||||
router = request.httprequest.app.get_db_router(request.db).bind('')
|
||||
router = http.root.get_db_router(request.db).bind('')
|
||||
endpoint = False
|
||||
try:
|
||||
endpoint = router.match(path, method='POST', query_args=query_args)
|
||||
|
||||
@@ -20,18 +20,29 @@ class PrinterInterface(Interface):
|
||||
with cups_lock:
|
||||
printers = conn.getPrinters()
|
||||
devices = conn.getDevices()
|
||||
for printer in printers:
|
||||
path = printers.get(printer).get('device-uri', False)
|
||||
if path and path in devices:
|
||||
devices.get(path).update({'supported': True}) # these printers are automatically supported
|
||||
for path in devices:
|
||||
if 'uuid=' in path:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path.split('uuid=')[1])
|
||||
elif 'serial=' in path:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path.split('serial=')[1])
|
||||
else:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path)
|
||||
devices[path]['identifier'] = identifier
|
||||
devices[path]['url'] = path
|
||||
printer_devices[identifier] = devices[path]
|
||||
for printer_name, printer in printers.items():
|
||||
path = printer.get('device-uri', False)
|
||||
if printer_name != self.get_identifier(path):
|
||||
printer.update({'supported': True}) # these printers are automatically supported
|
||||
device_class = 'network'
|
||||
if 'usb' in printer.get('device-uri'):
|
||||
device_class = 'direct'
|
||||
printer.update({'device-class': device_class})
|
||||
printer.update({'device-make-and-model': printer}) # give name setted in Cups
|
||||
printer.update({'device-id': ''})
|
||||
devices.update({printer_name: printer})
|
||||
for path, device in devices.items():
|
||||
identifier = self.get_identifier(path)
|
||||
device.update({'identifier': identifier})
|
||||
device.update({'url': path})
|
||||
printer_devices.update({identifier: device})
|
||||
return printer_devices
|
||||
|
||||
def get_identifier(self, path):
|
||||
if 'uuid=' in path:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path.split('uuid=')[1])
|
||||
elif 'serial=' in path:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path.split('serial=')[1])
|
||||
else:
|
||||
identifier = sub('[^a-zA-Z0-9_]', '', path)
|
||||
return identifier
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<td style="padding: 0 50px;">
|
||||
<div style="font-size: 13px; padding: 10px 0;">
|
||||
<span>Hello,</span><br />Here's a copy of your conversation with
|
||||
<span t-field="channel.livechat_operator_id.name"/>, on the
|
||||
<span t-esc="channel.livechat_operator_id.user_livechat_username or channel.livechat_operator_id.name"/>, on the
|
||||
<span t-field="channel.livechat_channel_id.create_date"/>
|
||||
</div>
|
||||
<table cellspacing="0" cellpadding="0" style="width:100%; border-collapse: collapse;">
|
||||
|
||||
@@ -3,5 +3,6 @@ from . import res_users
|
||||
from . import res_partner
|
||||
from . import im_livechat_channel
|
||||
from . import mail_channel
|
||||
from . import mail_message
|
||||
from . import rating
|
||||
from . import digest
|
||||
|
||||
@@ -52,10 +52,7 @@ class MailChannel(models.Model):
|
||||
clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header
|
||||
should be added to the notification, since the user cannot be listining to the channel.
|
||||
"""
|
||||
livechat_channels = self.filtered(lambda x: x.channel_type == 'livechat')
|
||||
other_channels = self.filtered(lambda x: x.channel_type != 'livechat')
|
||||
notifications = super(MailChannel, livechat_channels)._channel_message_notifications(message.with_context(im_livechat_use_username=True)) + \
|
||||
super(MailChannel, other_channels)._channel_message_notifications(message, message_format)
|
||||
notifications = super()._channel_message_notifications(message=message, message_format=message_format)
|
||||
for channel in self:
|
||||
# add uuid for private livechat channels to allow anonymous to listen
|
||||
if channel.channel_type == 'livechat' and channel.public == 'private':
|
||||
@@ -67,11 +64,6 @@ class MailChannel(models.Model):
|
||||
notifications = self._channel_channel_notifications(unpinned_channel_partner.mapped('partner_id').ids) + notifications
|
||||
return notifications
|
||||
|
||||
def channel_fetch_message(self, last_id=False, limit=20):
|
||||
""" Override to add the context of the livechat username."""
|
||||
channel = self.with_context(im_livechat_use_username=True) if self.channel_type == 'livechat' else self
|
||||
return super(MailChannel, channel).channel_fetch_message(last_id=last_id, limit=limit)
|
||||
|
||||
def channel_info(self, extra_info=False):
|
||||
""" Extends the channel header by adding the livechat operator and the 'anonymous' profile
|
||||
:rtype : list(dict)
|
||||
@@ -83,12 +75,32 @@ class MailChannel(models.Model):
|
||||
if channel.channel_type == 'livechat':
|
||||
# add the operator id
|
||||
if channel.livechat_operator_id:
|
||||
res = channel.livechat_operator_id.with_context(im_livechat_use_username=True).name_get()[0]
|
||||
channel_infos_dict[channel.id]['operator_pid'] = (res[0], res[1].replace(',', ''))
|
||||
display_name = channel.livechat_operator_id.user_livechat_username or channel.livechat_operator_id.display_name
|
||||
channel_infos_dict[channel.id]['operator_pid'] = (channel.livechat_operator_id.id, display_name.replace(',', ''))
|
||||
# add the anonymous or partner name
|
||||
channel_infos_dict[channel.id]['livechat_visitor'] = channel._channel_get_livechat_visitor_info()
|
||||
return list(channel_infos_dict.values())
|
||||
|
||||
def _channel_info_format_member(self, partner, partner_info):
|
||||
"""Override to remove sensitive information in livechat."""
|
||||
if self.channel_type == 'livechat':
|
||||
return {
|
||||
'id': partner.id,
|
||||
'name': partner.user_livechat_username or partner.name, # for API compatibility in stable
|
||||
'email': False, # for API compatibility in stable
|
||||
'im_status': False, # for API compatibility in stable
|
||||
'livechat_username': partner.user_livechat_username,
|
||||
}
|
||||
return super()._channel_info_format_member(partner=partner, partner_info=partner_info)
|
||||
|
||||
def _notify_typing_partner_data(self):
|
||||
"""Override to remove name and return livechat username if applicable."""
|
||||
data = super()._notify_typing_partner_data()
|
||||
if self.channel_type == 'livechat' and self.env.user.partner_id.user_livechat_username:
|
||||
data['partner_name'] = self.env.user.partner_id.user_livechat_username # for API compatibility in stable
|
||||
data['livechat_username'] = self.env.user.partner_id.user_livechat_username
|
||||
return data
|
||||
|
||||
@api.model
|
||||
def channel_fetch_slot(self):
|
||||
values = super(MailChannel, self).channel_fetch_slot()
|
||||
@@ -206,7 +218,7 @@ class MailChannel(models.Model):
|
||||
mail_body = template._render(render_context, engine='ir.qweb', minimal_qcontext=True)
|
||||
mail_body = self.env['mail.render.mixin']._replace_local_links(mail_body)
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'subject': _('Conversation with %s', self.livechat_operator_id.name),
|
||||
'subject': _('Conversation with %s', self.livechat_operator_id.user_livechat_username or self.livechat_operator_id.name),
|
||||
'email_from': company.catchall_formatted or company.email_formatted,
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_to': email,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
def _message_format(self, fnames):
|
||||
"""Override to remove email_from and to return the livechat username if applicable.
|
||||
A third param is added to the author_id tuple in this case to be able to differentiate it
|
||||
from the normal name in client code."""
|
||||
vals_list = super()._message_format(fnames=fnames)
|
||||
for vals in vals_list:
|
||||
message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids)
|
||||
if message_sudo.model == 'mail.channel' and self.env['mail.channel'].browse(message_sudo.res_id).channel_type == 'livechat':
|
||||
vals.pop('email_from')
|
||||
if message_sudo.author_id.user_livechat_username:
|
||||
vals['author_id'] = (message_sudo.author_id.id, message_sudo.author_id.user_livechat_username, message_sudo.author_id.user_livechat_username)
|
||||
return vals_list
|
||||
@@ -1,35 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models, api
|
||||
from flectra import api, models, fields
|
||||
|
||||
|
||||
class Partners(models.Model):
|
||||
""" Update of res.partners class
|
||||
- override name_get to take into account the livechat username
|
||||
"""
|
||||
"""Update of res.partner class to take into account the livechat username."""
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def name_get(self):
|
||||
if self.env.context.get('im_livechat_use_username'):
|
||||
# process the ones with livechat username
|
||||
users_with_livechatname = self.env['res.users'].search([('partner_id', 'in', self.ids), ('livechat_username', '!=', False)])
|
||||
map_with_livechatname = {}
|
||||
for user in users_with_livechatname:
|
||||
map_with_livechatname[user.partner_id.id] = user.livechat_username
|
||||
user_livechat_username = fields.Char(compute='_compute_user_livechat_username')
|
||||
|
||||
# process the ones without livecaht username
|
||||
partner_without_livechatname = self - users_with_livechatname.mapped('partner_id')
|
||||
no_livechatname_name_get = super(Partners, partner_without_livechatname).name_get()
|
||||
map_without_livechatname = dict(no_livechatname_name_get)
|
||||
|
||||
# restore order
|
||||
result = []
|
||||
for partner in self:
|
||||
name = map_with_livechatname.get(partner.id)
|
||||
if not name:
|
||||
name = map_without_livechatname.get(partner.id)
|
||||
result.append((partner.id, name))
|
||||
else:
|
||||
result = super(Partners, self).name_get()
|
||||
return result
|
||||
@api.depends('user_ids.livechat_username')
|
||||
def _compute_user_livechat_username(self):
|
||||
for partner in self:
|
||||
partner.user_livechat_username = next(iter(partner.user_ids.mapped('livechat_username')), False)
|
||||
|
||||
@@ -32,3 +32,35 @@ registerInstancePatchModel('mail.chat_window', 'im_livechat/static/src/models/ch
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
flectra.define('im_livechat/static/src/models/message/message.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
registerClassPatchModel,
|
||||
} = require('mail/static/src/model/model_core.js');
|
||||
|
||||
registerClassPatchModel('mail.message', 'im_livechat/static/src/models/message/message.js', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
convertData(data) {
|
||||
const data2 = this._super(data);
|
||||
if ('author_id' in data) {
|
||||
if (data.author_id[2]) {
|
||||
// flux specific for livechat, a 3rd param is livechat_username
|
||||
// and means 2nd param (display_name) should be ignored
|
||||
data2.author = [
|
||||
['insert-and-replace', {
|
||||
id: data.author_id[0],
|
||||
livechat_username: data.author_id[2],
|
||||
}],
|
||||
];
|
||||
}
|
||||
}
|
||||
return data2;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+11
-2
@@ -32,10 +32,19 @@ registerInstancePatchModel('mail.messaging_notification_handler', 'im_livechat/s
|
||||
partnerId = partner_id;
|
||||
partnerName = partner_name;
|
||||
}
|
||||
this._super(channelId, Object.assign(data, {
|
||||
Object.assign(data, {
|
||||
partner_id: partnerId,
|
||||
partner_name: partnerName,
|
||||
}));
|
||||
});
|
||||
if ('livechat_username' in data) {
|
||||
// flux specific, livechat_username is returned instead of name for livechat channels
|
||||
delete data.partner_name; // value still present for API compatibility in stable
|
||||
this.env.models['mail.partner'].insert({
|
||||
id: partnerId,
|
||||
livechat_username: data.livechat_username,
|
||||
});
|
||||
}
|
||||
this._super(channelId, data);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ flectra.define('im_livechat/static/src/models/partner/partner.js', function (req
|
||||
|
||||
const {
|
||||
registerClassPatchModel,
|
||||
registerFieldPatchModel,
|
||||
} = require('mail/static/src/model/model_core.js');
|
||||
const { attr } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
let nextPublicId = -1;
|
||||
|
||||
@@ -13,6 +15,20 @@ registerClassPatchModel('mail.partner', 'im_livechat/static/src/models/partner/p
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
convertData(data) {
|
||||
const data2 = this._super(data);
|
||||
if ('livechat_username' in data) {
|
||||
// flux specific, if livechat username is present it means `name`,
|
||||
// `email` and `im_status` contain `false` even though their value
|
||||
// might actually exist. Remove them from data2 to avoid overwriting
|
||||
// existing value (that could be known through other means).
|
||||
delete data2.name;
|
||||
delete data2.email;
|
||||
delete data2.im_status;
|
||||
data2.livechat_username = data.livechat_username;
|
||||
}
|
||||
return data2;
|
||||
},
|
||||
getNextPublicId() {
|
||||
const id = nextPublicId;
|
||||
nextPublicId -= 1;
|
||||
@@ -20,4 +36,12 @@ registerClassPatchModel('mail.partner', 'im_livechat/static/src/models/partner/p
|
||||
},
|
||||
});
|
||||
|
||||
registerFieldPatchModel('mail.partner', 'im_livechat/static/src/models/partner/partner.js', {
|
||||
/**
|
||||
* States the specific name of this partner in the context of livechat.
|
||||
* Either a string or undefined.
|
||||
*/
|
||||
livechat_username: attr(),
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -59,6 +59,19 @@ registerClassPatchModel('mail.thread', 'im_livechat/static/src/models/thread/thr
|
||||
});
|
||||
|
||||
registerInstancePatchModel('mail.thread', 'im_livechat/static/src/models/thread/thread.js', {
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getMemberName(partner) {
|
||||
if (this.channel_type === 'livechat' && partner.livechat_username) {
|
||||
return partner.livechat_username;
|
||||
}
|
||||
return this._super(partner);
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
|
||||
@@ -9,7 +9,8 @@ class TestGetMailChannel(TransactionCase):
|
||||
super(TestGetMailChannel, self).setUp()
|
||||
self.operators = self.env['res.users'].create([{
|
||||
'name': 'Michel',
|
||||
'login': 'michel'
|
||||
'login': 'michel',
|
||||
'livechat_username': "Michel Operator",
|
||||
}, {
|
||||
'name': 'Paul',
|
||||
'login': 'paul'
|
||||
@@ -58,12 +59,28 @@ class TestGetMailChannel(TransactionCase):
|
||||
test_user = self.env['res.users'].create({'name': 'Roger', 'login': 'roger', 'country_id': belgium.id})
|
||||
|
||||
# ensure visitor info are correct with anonymous
|
||||
channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='Visitor 22', country_id=belgium.id)
|
||||
operator = self.operators[0]
|
||||
channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='Visitor 22', previous_operator_id=operator.partner_id.id, country_id=belgium.id)
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
self.assertFalse(visitor_info['id'])
|
||||
self.assertEqual(visitor_info['name'], "Visitor 22")
|
||||
self.assertEqual(visitor_info['country'], (20, "Belgium"))
|
||||
|
||||
# ensure member info are hidden (in particular email and real name when livechat username is present)
|
||||
self.assertEqual(sorted(channel_info['members'], key=lambda m: m['id']), sorted([{
|
||||
'email': False,
|
||||
'id': operator.partner_id.id,
|
||||
'im_status': False,
|
||||
'livechat_username': 'Michel Operator',
|
||||
'name': 'Michel Operator',
|
||||
}, {
|
||||
'email': False,
|
||||
'id': public_user.partner_id.id,
|
||||
'im_status': False,
|
||||
'livechat_username': False,
|
||||
'name': 'Public user',
|
||||
}], key=lambda m: m['id']))
|
||||
|
||||
# ensure visitor info are correct with real user
|
||||
channel_info = self.livechat_channel.with_user(test_user)._open_livechat_mail_channel(anonymous_name='whatever', user_id=test_user.id)
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
@@ -74,7 +91,7 @@ class TestGetMailChannel(TransactionCase):
|
||||
# ensure visitor info are correct when operator is testing himself
|
||||
operator = self.operators[0]
|
||||
channel_info = self.livechat_channel.with_user(operator)._open_livechat_mail_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=operator.id)
|
||||
self.assertEqual(channel_info['operator_pid'], (operator.partner_id.id, "Michel"))
|
||||
self.assertEqual(channel_info['operator_pid'], (operator.partner_id.id, "Michel Operator"))
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
self.assertEqual(visitor_info['id'], operator.partner_id.id)
|
||||
self.assertEqual(visitor_info['name'], "Michel")
|
||||
@@ -92,3 +109,22 @@ class TestGetMailChannel(TransactionCase):
|
||||
})
|
||||
|
||||
return mail_channels
|
||||
|
||||
def test_operator_livechat_username(self):
|
||||
"""Ensures the operator livechat_username is returned by `channel_fetch_message`, which is
|
||||
the method called by the public route displaying chat history."""
|
||||
public_user = self.env.ref('base.public_user')
|
||||
operator = self.operators[0]
|
||||
operator.write({
|
||||
'email': 'michel@example.com',
|
||||
'livechat_username': 'Michel at your service',
|
||||
})
|
||||
channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='whatever')
|
||||
channel = self.env['mail.channel'].browse(channel_info['id'])
|
||||
channel.with_user(operator).message_post(body='Hello', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
message_formats = channel.with_user(public_user).channel_fetch_message()
|
||||
self.assertEqual(len(message_formats), 1)
|
||||
self.assertEqual(message_formats[0]['author_id'][0], operator.partner_id.id)
|
||||
self.assertEqual(message_formats[0]['author_id'][1], operator.livechat_username)
|
||||
self.assertEqual(message_formats[0]['author_id'][2], operator.livechat_username)
|
||||
self.assertFalse(message_formats[0].get('email_from'), "should not send email_from to livechat user")
|
||||
|
||||
@@ -19,35 +19,35 @@ dc_e_f,170,19,FACTURAS DE EXPORTACION,E,FACTURA,invoice,FA-E,base.ar,not_zero
|
||||
dc_e_nd,180,20,NOTAS DE DEBITO POR OPERACIONES CON EL EXTERIOR,E,NOTA DE DEBITO,debit_note,ND-E,base.ar,not_zero
|
||||
dc_e_nc,190,21,NOTAS DE CREDITO POR OPERACIONES CON EL EXTERIOR,E,NOTA DE CREDITO,credit_note,NC-E,base.ar,not_zero
|
||||
dc_e_fs,200,22,FACTURAS - PERMISO EXPORTACION SIMPLIFICADO - DTO. 855/97,E,,,,base.ar,not_zero
|
||||
dc_usados,210,30,COMPROBANTES DE COMPRA DE BIENES USADOS,,,,,base.ar,
|
||||
dc_usados,210,30,COMPROBANTES DE COMPRA DE BIENES USADOS,,,invoice,CBU,base.ar,not_zero
|
||||
dc_mandato,220,31,MANDATO - CONSIGNACION,,,,,base.ar,
|
||||
dc_reciclado,230,32,COMPROBANTES PARA RECICLAR MATERIALES,,,,,base.ar,
|
||||
dc_a_rg1415,240,34,"COMPROBANTES A DEL APARTADO A, INC. F), R.G. Nº 1415",A,,invoice,,base.ar,not_zero
|
||||
dc_b_rg1415,250,35,"COMPROBANTES B DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",B,,invoice,,base.ar,zero
|
||||
dc_c_rg1415,260,36,"COMPROBANTES C DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",C,,invoice,,base.ar,zero
|
||||
dc_nd_rg1415,270,37,NOTAS DE DEBITO O DOCUMENTO EQUIVALENTE QUE CUMPLAN CON LA R.G. Nº 1415,,,debit_note,,base.ar,not_zero
|
||||
dc_nc_rg1415,280,38,NOTAS DE CREDITO O DOCUMENTO EQUIVALENTE QUE CUMPLAN CON LA R.G. Nº 1415,,,credit_note,,base.ar,not_zero
|
||||
dc_reciclado,230,32,COMPROBANTES PARA RECICLAR MATERIALES,,,invoice,CRM,base.ar,not_zero
|
||||
dc_a_rg1415,240,34,"COMPROBANTES A DEL APARTADO A, INC. F), R.G. Nº 1415",A,,invoice,CA-A,base.ar,not_zero
|
||||
dc_b_rg1415,250,35,"COMPROBANTES B DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",B,,invoice,CA-B,base.ar,zero
|
||||
dc_c_rg1415,260,36,"COMPROBANTES C DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",C,,invoice,CA-C,base.ar,zero
|
||||
dc_nd_rg1415,270,37,NOTAS DE DEBITO O DOCUMENTO EQUIVALENTE QUE CUMPLAN CON LA R.G. Nº 1415,,,debit_note,ND1415,base.ar,not_zero
|
||||
dc_nc_rg1415,280,38,NOTAS DE CREDITO O DOCUMENTO EQUIVALENTE QUE CUMPLAN CON LA R.G. Nº 1415,,,credit_note,NC1415,base.ar,not_zero
|
||||
dc_a_o_rg1415,290,39,OTROS COMPROBANTES A QUE CUMPLEN CON LA R.G. Nº 1415,A,,invoice,OC-A,base.ar,not_zero
|
||||
dc_b_o_rg1415,300,40,OTROS COMPROBANTES B QUE CUMPLAN CON LA R.G. Nº 1415,B,,invoice,OC-B,base.ar,zero
|
||||
dc_c_o_rg1415,310,41,OTROS COMPROBANTES C QUE CUMPLAN CON LA R.G. Nº 1415,C,,invoice,OC-C,base.ar,zero
|
||||
dc_a_rf,320,50,RECIBO FACTURA A REGIMEN DE FACTURA DE CREDITO,A,,,,base.ar,not_zero
|
||||
dc_m_f,330,51,FACTURAS M,M,FACTURA,invoice,FA-M,base.ar,not_zero
|
||||
dc_m_nd,340,52,NOTAS DE DEBITO M,M,NOTA DE DEBITO,debit_note,ND-C,base.ar,not_zero
|
||||
dc_m_nc,350,53,NOTAS DE CREDITO M,M,NOTA DE CREDITO,credit_note,NC-C,base.ar,not_zero
|
||||
dc_m_nd,340,52,NOTAS DE DEBITO M,M,NOTA DE DEBITO,debit_note,ND-M,base.ar,not_zero
|
||||
dc_m_nc,350,53,NOTAS DE CREDITO M,M,NOTA DE CREDITO,credit_note,NC-M,base.ar,not_zero
|
||||
dc_m_r,360,54,RECIBOS M,M,RECIBO,invoice,RE-M,base.ar,not_zero
|
||||
dc_m_nvc,370,55,NOTAS DE VENTA AL CONTADO M,M,,invoice,NVC-M,base.ar,not_zero
|
||||
dc_m_rg1415,380,56,"COMPROBANTES M DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",M,,invoice,,base.ar,not_zero
|
||||
dc_m_o_rg1415,390,57,OTROS COMPROBANTES M QUE CUMPLAN CON LA R.G. Nº 1415,M,,invoice,,base.ar,not_zero
|
||||
dc_m_cvl,400,58,CUENTAS DE VENTA Y LIQUIDO PRODUCTO M,M,,invoice,,base.ar,not_zero
|
||||
dc_m_rg1415,380,56,"COMPROBANTES M DEL ANEXO I, APARTADO A, INC. F), R.G. Nº 1415",M,,invoice,CA-M,base.ar,not_zero
|
||||
dc_m_o_rg1415,390,57,OTROS COMPROBANTES M QUE CUMPLAN CON LA R.G. Nº 1415,M,,invoice,OC-M,base.ar,not_zero
|
||||
dc_m_cvl,400,58,CUENTAS DE VENTA Y LIQUIDO PRODUCTO M,M,,invoice,LP-M,base.ar,not_zero
|
||||
dc_m_l,410,59,LIQUIDACIONES M,M,LIQUIDACION,invoice,LI-M,base.ar,not_zero
|
||||
dc_a_cvl,420,60,CUENTAS DE VENTA Y LIQUIDO PRODUCTO A,A,CTA VTA LIQUIDO PRODUCTO,invoice,LP-A,base.ar,not_zero
|
||||
dc_b_cvl,430,61,CUENTAS DE VENTA Y LIQUIDO PRODUCTO B,B,CTA VTA LIQUIDO PRODUCTO,invoice,LP-B,base.ar,zero
|
||||
dc_a_l,440,63,LIQUIDACIONES A,A,LIQUIDACION,invoice,LI-A,base.ar,not_zero
|
||||
dc_b_l,450,64,LIQUIDACIONES B,B,LIQUIDACION,invoice,LI-B,base.ar,zero
|
||||
dc_nc,460,65,"NOTAS DE CREDITO DE COMPROBANTES CON COD. 34, 39, 58, 59, 60, 63, 96, 97,",,,credit_note,,base.ar,
|
||||
dc_nc,460,65,"NOTAS DE CREDITO DE COMPROBANTES CON COD. 34, 39, 58, 59, 60, 63, 96, 97,",,,,,base.ar,
|
||||
dc_desp_imp,470,66,DESPACHO DE IMPORTACION,,,invoice,DI,base.ar,not_zero
|
||||
dc_imp_serv,480,67,IMPORTACION DE SERVICIOS,,,invoice,IS,base.ar,
|
||||
dc_c_l,490,68,LIQUIDACION C,C,LIQUIDACION,invoice,,base.ar,not_zero
|
||||
dc_imp_serv,480,67,IMPORTACION DE SERVICIOS,,,,,base.ar,
|
||||
dc_c_l,490,68,LIQUIDACION C,C,LIQUIDACION,invoice,LI-C,base.ar,not_zero
|
||||
dc_rfc,500,70,RECIBOS FACTURA DE CREDITO,,,,,base.ar,not_zero
|
||||
dc_cfcp,510,71,CREDITO FISCAL POR CONTRIBUCIONES PATRONALES,,,,,base.ar,
|
||||
dc_f1116,520,73,FORMULARIO 1116 RT,,,,,base.ar,
|
||||
@@ -57,13 +57,13 @@ dc_zeta,550,80,INFORME DIARIO DE CIERRE (ZETA) - CONTROLADORES FISCALES,,,,,base
|
||||
dc_a_t,560,81,TIQUE FACTURA A,A,TIQUE FACTURA,invoice,TF-A,base.ar,not_zero
|
||||
dc_b_t,570,82,TIQUE - FACTURA B,B,TIQUE FACTURA,invoice,TF-B,base.ar,zero
|
||||
dc_t,580,83,TIQUE,,TIQUE,invoice,TI-X,base.ar,zero
|
||||
dc_sp_c,590,84,COMPROBANTE FACTURA DE SERVICIOS PUBLICOS INTERESES FINANCIEROS,,,invoice,,base.ar,
|
||||
dc_sp_nc,600,85,NOTA DE CREDITO SERVICIOS PUBLICOS NOTA DE CREDITO CONTROLADORES FISCALES,,,credit_note,,base.ar,
|
||||
dc_sp_nd,610,86,NOTA DE DEBITO SERVICIOS PUBLICOS,,,debit_note,,base.ar,
|
||||
dc_oc_se,620,87,OTROS COMPROBANTES - SERVICIOS DEL EXTERIOR,,,invoice,,base.ar,
|
||||
dc_sp_c,590,84,COMPROBANTE FACTURA DE SERVICIOS PUBLICOS INTERESES FINANCIEROS,,,,,base.ar,
|
||||
dc_sp_nc,600,85,NOTA DE CREDITO SERVICIOS PUBLICOS NOTA DE CREDITO CONTROLADORES FISCALES,,,,,base.ar,
|
||||
dc_sp_nd,610,86,NOTA DE DEBITO SERVICIOS PUBLICOS,,,,,base.ar,
|
||||
dc_oc_se,620,87,OTROS COMPROBANTES - SERVICIOS DEL EXTERIOR,,,,,base.ar,
|
||||
dc_oc_c,630,88,REMITO ELECTRONICO,,,,,base.ar,
|
||||
dc_oc_nd,640,89,RESUMEN DE DATOS,,,debit_note,,base.ar,
|
||||
dc_oc_nc,650,90,OTROS COMPROBANTES - DOCUMENTOS EXCEPTUADOS - NOTAS DE CREDITO,,,credit_note,,base.ar,not_zero
|
||||
dc_oc_nd,640,89,RESUMEN DE DATOS,,,,,base.ar,
|
||||
dc_oc_nc,650,90,OTROS COMPROBANTES - DOCUMENTOS EXCEPTUADOS - NOTAS DE CREDITO,,,credit_note,OC,base.ar,not_zero
|
||||
dc_r_r,660,91,REMITOS R,R,,,,base.ar,
|
||||
dc_ac_inc_df,670,92,AJUSTES CONTABLES QUE INCREMENTAN EL DEBITO FISCAL,,,,,base.ar,
|
||||
dc_ac_dis_df,680,93,AJUSTES CONTABLES QUE DISMINUYEN EL DEBITO FISCAL,,,,,base.ar,
|
||||
@@ -74,8 +74,8 @@ dc_f1116c,720,97,FORMULARIO 1116 C,,,,,base.ar,
|
||||
dc_oc_ncrg3419,730,99,OTROS COMPROBANTES QUE NO CUMPLEN O ESTAN EXCEPTUADOS DE LA R.G. Nº 1415 Y SUS MODIF,,,invoice,OC-X,base.ar,not_zero
|
||||
dc_aa_dj_pos,740,101,AJUSTE ANUAL PROVENIENTE DE LA D J DEL IVA POSITIVO,,,,,base.ar,
|
||||
dc_aa_dj_neg,750,102,AJUSTE ANUAL PROVENIENTE DE LA D J DEL IVA NEGATIVO,,,,,base.ar,
|
||||
dc_na,760,103,NOTA DE ASIGNACION,,,invoice,,base.ar,
|
||||
dc_nca,770,104,NOTA DE CREDITO DE ASIGNACION,,,credit_note,,base.ar,
|
||||
dc_na,760,103,NOTA DE ASIGNACION,,,,,base.ar,
|
||||
dc_nca,770,104,NOTA DE CREDITO DE ASIGNACION,,,,,base.ar,
|
||||
dc_remito_x,790,94,REMITOS X,X,REMITO,,RM-X,base.ar,
|
||||
dc_liq_s_a,800,17,LIQUIDACION DE SERVICIOS PUBLICOS CLASE A,A,,invoice,LS-A,base.ar,not_zero
|
||||
dc_liq_s_b,810,18,LIQUIDACION DE SERVICIOS PUBLICOS CLASE B,B,,invoice,LS-B,base.ar,zero
|
||||
@@ -86,14 +86,14 @@ dc_con_b_m,850,26,"COMPROBANTES ""B"" DE CONSIGNACION PRIMARIA PARA EL SECTOR PE
|
||||
dc_liq_uci_a,860,27,LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A,A,,invoice,LU-A,base.ar,not_zero
|
||||
dc_liq_uci_b,870,28,LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B,B,,invoice,LU-B,base.ar,zero
|
||||
dc_liq_uci_c,880,29,LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C,C,,invoice,LU-C,base.ar,zero
|
||||
dc_liq_prim_gr,890,33,LIQUIDACION PRIMARIA DE GRANOS,,,,,base.ar,
|
||||
dc_liq_prim_gr,890,33,LIQUIDACION PRIMARIA DE GRANOS,,,invoice,LPG,base.ar,not_zero
|
||||
dc_nc_liq_uci_a,900,43,NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B,B,,credit_note,NCLU-B,base.ar,zero
|
||||
dc_nc_liq_uci_b,910,44,NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C,C,,credit_note,NCLU-C,base.ar,zero
|
||||
dc_nd_liq_uci_a,920,45,NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A,A,,debit_note,NDLU-A,base.ar,not_zero
|
||||
dc_nd_liq_uci_b,930,46,NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B,B,,debit_note,NDLU-B,base.ar,zero
|
||||
dc_nd_liq_uci_c,940,47,NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C,C,,debit_note,NDLU-C,base.ar,zero
|
||||
dc_nc_liq_uci_c,950,48,NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A,A,,credit_note,NCLU-A,base.ar,not_zero
|
||||
dc_bs_no_reg,960,49,COMPROBANTES DE COMPRA DE BIENES NO REGISTRABLES A CONSUMIDORES FINALES,,,,BNR,base.ar,not_zero
|
||||
dc_bs_no_reg,960,49,COMPROBANTES DE COMPRA DE BIENES NO REGISTRABLES A CONSUMIDORES FINALES,,,invoice,BNR,base.ar,not_zero
|
||||
dc_t_nc,970,110,TIQUE NOTA DE CREDITO,,TIQUE NOTA DE CREDITO,credit_note,TC-X,base.ar,not_zero
|
||||
dc_t_c,980,111,TIQUE FACTURA C,C,TIQUE FACTURA,invoice,TF-C,base.ar,zero
|
||||
dc_t_nc_a,990,112,TIQUE NOTA DE CREDITO A,A,TIQUE NOTA DE CREDITO,credit_note,TN-A,base.ar,not_zero
|
||||
@@ -105,7 +105,7 @@ dc_t_nd_c,1040,117,TIQUE NOTA DE DEBITO C,C,TIQUE NOTA DE DEBITO,debit_note,TD-C
|
||||
dc_t_m,1050,118,TIQUE FACTURA M,M,FACTURA,invoice,TF-M,base.ar,not_zero
|
||||
dc_t_nc_m,1060,119,TIQUE NOTA DE CREDITO M,M,NOTA DE CREDITO,credit_note,TC-M,base.ar,not_zero
|
||||
dc_t_nd_m,1070,120,TIQUE NOTA DE DEBITO M,M,NOTA DE DEBITO,debit_note,TD-M,base.ar,not_zero
|
||||
dc_liq_sec_gr,1080,331,LIQUIDACION SECUNDARIA DE GRANOS,,,,,base.ar,
|
||||
dc_liq_sec_gr,1080,331,LIQUIDACION SECUNDARIA DE GRANOS,,,invoice,LSG,base.ar,not_zero
|
||||
dc_cert_ele_gr,1090,332,CERTIFICACION ELECTRONICA (GRANOS),,,,,base.ar,
|
||||
dc_fce_a_f,1100,201,FACTURA DE CREDITO ELECTRONICA MiPyMEs (FCE) A,A,FACTURA DE CREDITO ELECTRONICA,invoice,FCE-A,base.ar,not_zero
|
||||
dc_fce_a_nd,1110,202,NOTA DE DEBITO ELECTRONICA MiPyMEs (FCE) A,A,NOTA DE DEBITO ELECTRONICA,debit_note,NDE-A,base.ar,not_zero
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<flectra>
|
||||
<data noupdate="1">
|
||||
<function model="l10n_latam.document.type" name="write">
|
||||
<value model="l10n_latam.document.type" eval="obj().search([('code', 'in', ['5','10','14','16','22','30','31','32','34','35','36','37','38','50','55','56','57','58','59','60','61','65','67','68','70','71','73','74','75','80','84','85','86','87','88','89','90','91','92','93','94','95','96','97','101','102','103','104','94','23','24','25','26','33','331','332','150','151','157','158','159','160','161','162','163','164','165','166','167','168','169','170','171','172','180','182','183','185','186','188','189','190','191'])]).ids"/>
|
||||
<value model="l10n_latam.document.type" eval="obj().search([('country_id.code', '=', 'AR'), ('code', 'in', ['5','10','14','16','22','30','31','32','34','35','36','37','38','50','55','56','57','58','59','60','61','65','67','68','70','71','73','74','75','80','84','85','86','87','88','89','90','91','92','93','94','95','96','97','101','102','103','104','94','23','24','25','26','33','331','332','150','151','157','158','159','160','161','162','163','164','165','166','167','168','169','170','171','172','180','182','183','185','186','188','189','190','191'])]).ids"/>
|
||||
<value eval="{'active': False}"/>
|
||||
</function>
|
||||
</data>
|
||||
|
||||
@@ -34,7 +34,7 @@ Wizards provided by this module:
|
||||
**Path to access :** Invoicing/Reporting/Legal Reports/Belgium Statements/Annual Listing Of VAT-Subjected Customers
|
||||
|
||||
""",
|
||||
'author': 'Noviat, FlectraHQ Inc.',
|
||||
'author': 'Noviat, FlectraHQ, Inc.',
|
||||
'depends': [
|
||||
'account',
|
||||
'base_iban',
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"a493","Deferred income","493","account.data_account_type_current_liabilities","l10n_be.l10nbe_chart_template","","False"
|
||||
"a496","Foreign currency translation differences - Assets","496","account.data_account_type_current_assets","l10n_be.l10nbe_chart_template","","False"
|
||||
"a497","Foreign currency translation differences - Liabilities","497","account.data_account_type_current_liabilities","l10n_be.l10nbe_chart_template","","False"
|
||||
"a499","Suspense account","499","account.data_account_type_current_liabilities","l10n_be.l10nbe_chart_template","","False"
|
||||
"a499","Suspense account","499","account.data_account_type_current_assets","l10n_be.l10nbe_chart_template","","False"
|
||||
"a500","Current investments other than shares, fixed income securities and term accounts - Cost","500","account.data_account_type_current_assets","l10n_be.l10nbe_chart_template","","False"
|
||||
"a509","Current investments other than shares, fixed income securities and term accounts - Amounts written down","509","account.data_account_type_current_assets","l10n_be.l10nbe_chart_template","","False"
|
||||
"a510","Shares and current investments other than fixed income investments - Acquisition value","510","account.data_account_type_current_assets","l10n_be.l10nbe_chart_template","","False"
|
||||
|
||||
|
@@ -21,9 +21,9 @@ class IrActionsReport(models.Model):
|
||||
if edi_attachment:
|
||||
old_xml = base64.b64decode(edi_attachment.with_context(bin_size=False).datas, validate=True)
|
||||
tree = etree.fromstring(old_xml)
|
||||
document_currency_code_elements = tree.xpath("//*[local-name()='DocumentCurrencyCode']")
|
||||
anchor_elements = tree.xpath("//*[local-name()='AccountingSupplierParty']")
|
||||
additional_document_elements = tree.xpath("//*[local-name()='AdditionalDocumentReference']")
|
||||
if document_currency_code_elements and not additional_document_elements:
|
||||
if anchor_elements and not additional_document_elements:
|
||||
pdf = base64.b64encode(buffer.getvalue()).decode()
|
||||
pdf_name = '%s.pdf' % record._get_efff_name()
|
||||
to_inject = '''
|
||||
@@ -40,7 +40,8 @@ class IrActionsReport(models.Model):
|
||||
</cac:AdditionalDocumentReference>
|
||||
''' % (escape(pdf_name), quoteattr(pdf_name), pdf)
|
||||
|
||||
document_currency_code_elements[0].addnext(etree.fromstring(to_inject))
|
||||
anchor_index = tree.index(anchor_elements[0])
|
||||
tree.insert(anchor_index, etree.fromstring(to_inject))
|
||||
new_xml = etree.tostring(tree, pretty_print=True)
|
||||
edi_attachment.write({
|
||||
'res_model': 'account.move',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_ubl
|
||||
from . import test_ubl
|
||||
from . import test_efff_export
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import io
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
|
||||
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
|
||||
from flectra.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestUBLBE(AccountEdiTestCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref='l10n_be.l10nbe_chart_template', edi_format_ref='l10n_be_edi.edi_efff_1'):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
|
||||
|
||||
cls.partner_a.write({
|
||||
'street': "Chaussée de Namur 40",
|
||||
'zip': "1367",
|
||||
'city': "Ramillies",
|
||||
'vat': 'BE0202239951',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
})
|
||||
|
||||
cls.env.company.write({
|
||||
'street': "Rue des Bourlottes 9",
|
||||
'zip': "1367",
|
||||
'city': "Ramillies",
|
||||
'vat': 'BE0477472701',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
})
|
||||
|
||||
cls.tax_21 = cls.env['account.tax'].create({
|
||||
'name': 'tax_21',
|
||||
'amount_type': 'percent',
|
||||
'amount': 21,
|
||||
'type_tax_use': 'sale',
|
||||
})
|
||||
|
||||
def test_out_invoice_efff(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_payment_term_id': self.pay_terms_b.id,
|
||||
'invoice_date': '2017-01-01',
|
||||
'date': '2017-01-01',
|
||||
'invoice_origin': 'test invoice origin',
|
||||
'narration': 'test narration',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'price_unit': 1000.0,
|
||||
'product_id': self.product_a.id,
|
||||
'tax_ids': [(6, 0, self.tax_21.ids)],
|
||||
})],
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Print the invoice to append AdditionalDocumentReference.
|
||||
pdf_buffer = io.BytesIO()
|
||||
pdf_writer = PdfFileWriter()
|
||||
pdf_writer.addBlankPage(42, 42)
|
||||
pdf_writer.write(pdf_buffer)
|
||||
self.env.ref('account.account_invoices_without_payment')._postprocess_pdf_report(invoice, pdf_buffer)
|
||||
pdf_buffer.close()
|
||||
|
||||
attachment = invoice._get_edi_attachment(self.edi_format)
|
||||
self.assertTrue(attachment)
|
||||
xml_content = base64.b64decode(attachment.datas)
|
||||
|
||||
current_etree = self.get_xml_tree_from_string(xml_content)
|
||||
expected_etree = self.get_xml_tree_from_string(f'''
|
||||
<Invoice>
|
||||
<UBLVersionID>2.0</UBLVersionID>
|
||||
<ID>{invoice.name}</ID>
|
||||
<IssueDate>2017-01-01</IssueDate>
|
||||
<InvoiceTypeCode>380</InvoiceTypeCode>
|
||||
<Note>test narration</Note>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
<AdditionalDocumentReference>
|
||||
<ID>efff_BE0477472701_INV2017010001.pdf</ID>
|
||||
<Attachment>
|
||||
<EmbeddedDocumentBinaryObject
|
||||
mimeCode="application/pdf"
|
||||
filename="efff_BE0477472701_INV2017010001.pdf">___ignore___</EmbeddedDocumentBinaryObject>
|
||||
</Attachment>
|
||||
</AdditionalDocumentReference>
|
||||
<AccountingSupplierParty>
|
||||
<Party>
|
||||
<PartyName>
|
||||
<Name>company_1_data</Name>
|
||||
</PartyName>
|
||||
<Language>
|
||||
<LocaleCode>en_US</LocaleCode>
|
||||
</Language>
|
||||
<PostalAddress>
|
||||
<StreetName>Rue des Bourlottes 9</StreetName>
|
||||
<CityName>Ramillies</CityName>
|
||||
<PostalZone>1367</PostalZone>
|
||||
<Country>
|
||||
<IdentificationCode>BE</IdentificationCode>
|
||||
<Name>Belgium</Name>
|
||||
</Country>
|
||||
</PostalAddress>
|
||||
<PartyTaxScheme>
|
||||
<RegistrationName>company_1_data</RegistrationName>
|
||||
<CompanyID>BE0477472701</CompanyID>
|
||||
<TaxScheme>
|
||||
<ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</ID>
|
||||
</TaxScheme>
|
||||
</PartyTaxScheme>
|
||||
<Contact>
|
||||
<Name>company_1_data</Name>
|
||||
</Contact>
|
||||
</Party>
|
||||
</AccountingSupplierParty>
|
||||
<AccountingCustomerParty>
|
||||
<Party>
|
||||
<PartyName>
|
||||
<Name>partner_a</Name>
|
||||
</PartyName>
|
||||
<Language>
|
||||
<LocaleCode>en_US</LocaleCode>
|
||||
</Language>
|
||||
<PostalAddress>
|
||||
<StreetName>Chaussée de Namur 40</StreetName>
|
||||
<CityName>Ramillies</CityName>
|
||||
<PostalZone>1367</PostalZone>
|
||||
<Country>
|
||||
<IdentificationCode>BE</IdentificationCode>
|
||||
<Name>Belgium</Name>
|
||||
</Country>
|
||||
</PostalAddress>
|
||||
<PartyTaxScheme>
|
||||
<RegistrationName>partner_a</RegistrationName>
|
||||
<CompanyID>BE0202239951</CompanyID>
|
||||
<TaxScheme>
|
||||
<ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</ID>
|
||||
</TaxScheme>
|
||||
</PartyTaxScheme>
|
||||
<Contact>
|
||||
<Name>partner_a</Name>
|
||||
</Contact>
|
||||
</Party>
|
||||
</AccountingCustomerParty>
|
||||
<PaymentMeans>
|
||||
<PaymentMeansCode listID="UN/ECE 4461">31</PaymentMeansCode>
|
||||
<PaymentDueDate>2017-02-28</PaymentDueDate>
|
||||
<InstructionID>{invoice.name}</InstructionID>
|
||||
</PaymentMeans>
|
||||
<PaymentTerms>
|
||||
<Note>30% Advance End of Following Month</Note>
|
||||
</PaymentTerms>
|
||||
<TaxTotal>
|
||||
<TaxAmount currencyID="EUR">210.00</TaxAmount>
|
||||
</TaxTotal>
|
||||
<LegalMonetaryTotal>
|
||||
<LineExtensionAmount currencyID="EUR">1000.00</LineExtensionAmount>
|
||||
<TaxExclusiveAmount currencyID="EUR">1000.00</TaxExclusiveAmount>
|
||||
<TaxInclusiveAmount currencyID="EUR">1210.00</TaxInclusiveAmount>
|
||||
<PrepaidAmount currencyID="EUR">0.00</PrepaidAmount>
|
||||
<PayableAmount currencyID="EUR">1210.00</PayableAmount>
|
||||
</LegalMonetaryTotal>
|
||||
<InvoiceLine>
|
||||
<ID>___ignore___</ID>
|
||||
<InvoicedQuantity>1.0</InvoicedQuantity>
|
||||
<LineExtensionAmount currencyID="EUR">1000.00</LineExtensionAmount>
|
||||
<TaxTotal>
|
||||
<TaxAmount currencyID="EUR">210.00</TaxAmount>
|
||||
</TaxTotal>
|
||||
<Item>
|
||||
<Description>product_a</Description>
|
||||
<Name>product_a</Name>
|
||||
</Item>
|
||||
<Price>
|
||||
<PriceAmount currencyID="EUR">1000.00</PriceAmount>
|
||||
</Price>
|
||||
</InvoiceLine>
|
||||
</Invoice>
|
||||
''')
|
||||
self.assertXmlTreeEqual(current_etree, expected_etree)
|
||||
@@ -242,6 +242,8 @@ class ResPartnerBank(models.Model):
|
||||
'quiet': 1,
|
||||
'mask': 'ch_cross',
|
||||
'value': '\n'.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
|
||||
# Swiss QR code requires Error Correction Level = 'M' by specification
|
||||
'barLevel': 'M',
|
||||
}
|
||||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ However, if the new QR-IBAN field is filled, the value will be used as the QR-IB
|
||||
This should help for reconciliation on the bank statements where the old IBAN code is still used.
|
||||
""",
|
||||
'version': '1.0',
|
||||
'author': 'FlectraHQ Inc., Odoo S.A',
|
||||
'author': 'FlectraHQ, Inc., Odoo S.A',
|
||||
'category': 'Hidden',
|
||||
'depends': ['l10n_ch'],
|
||||
'data': [
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"chart_cz_256000","Dluhové cenné papíry se splatností do jednoho roku držené do splatnosti","256000","l10n_cz.cz_chart_template","account.data_account_type_liquidity","False"
|
||||
"chart_cz_257000","Ostatní cenné papíry","257000","l10n_cz.cz_chart_template","account.data_account_type_liquidity","False"
|
||||
"chart_cz_259000","Pořízování krátkodobého finančního majetku","259000","l10n_cz.cz_chart_template","account.data_account_type_liquidity","False"
|
||||
"chart_cz_261000","Peníze na cestě","261000","l10n_cz.cz_chart_template","account.data_account_type_liquidity","False"
|
||||
"chart_cz_261000","Peníze na cestě","261000","l10n_cz.cz_chart_template","account.data_account_type_current_assets","False"
|
||||
"chart_cz_291000","Opravná položka ke krátkodobému finančnímu majetku","291000","l10n_cz.cz_chart_template","account.data_account_type_liquidity","False"
|
||||
"chart_cz_311000","Odběratelé","311000","l10n_cz.cz_chart_template","account.data_account_type_receivable","True"
|
||||
"chart_cz_313000","Pohledávky za eskontované cenné papíry","313000","l10n_cz.cz_chart_template","account.data_account_type_receivable","True"
|
||||
|
||||
|
@@ -6,3 +6,4 @@ from . import base_document_layout
|
||||
from . import chart_template
|
||||
from . import ir_actions_report
|
||||
from . import account_move
|
||||
from . import hr_timesheet
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from flectra import models, fields, api, _
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
l10n_de_template_data = fields.Binary(compute='_compute_l10n_de_template_data')
|
||||
l10n_de_document_title = fields.Char(compute='_compute_l10n_de_document_title')
|
||||
|
||||
def _compute_l10n_de_template_data(self):
|
||||
for record in self:
|
||||
record.l10n_de_template_data = []
|
||||
|
||||
def _compute_l10n_de_document_title(self):
|
||||
for record in self:
|
||||
record.l10n_de_document_title = ''
|
||||
|
||||
@@ -106,9 +106,12 @@
|
||||
</tr>
|
||||
</table>
|
||||
<h2>
|
||||
<span t-if="not o"><t t-esc="company.l10n_de_document_title"/></span>
|
||||
<span t-elif="'l10n_de_document_title' in o"><t t-esc="o.l10n_de_document_title"/></span>
|
||||
<span t-else="" t-field="o.name"/>
|
||||
<span t-if="not o and not docs"><t t-esc="company.l10n_de_document_title"/></span>
|
||||
<span t-else="">
|
||||
<t t-set="o" t-value="docs[0]" t-if="not o" />
|
||||
<span t-if="'l10n_de_document_title' in o"><t t-esc="o.l10n_de_document_title"/></span>
|
||||
<span t-else="" t-field="o.name"/>
|
||||
</span>
|
||||
</h2>
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
|
||||
@@ -37,13 +37,12 @@ class PatchedHTTPAdapter(requests.adapters.HTTPAdapter):
|
||||
# still made without checking temporary files exist.
|
||||
super().cert_verify(conn, url, verify, None)
|
||||
conn.cert_file = cert
|
||||
conn.key_file = cert
|
||||
conn.key_file = None
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
# OVERRIDE
|
||||
# Patch the OpenSSLContext to decode the certificate in-memory.
|
||||
conn = super().get_connection(url, proxies=proxies)
|
||||
|
||||
context = conn.conn_kw['ssl_context']
|
||||
|
||||
def patched_load_cert_chain(l10n_es_flectra_certificate, keyfile=None, password=None):
|
||||
@@ -229,7 +228,7 @@ 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
|
||||
elif partner.country_id.code in eu_country_codes:
|
||||
elif partner.country_id.code in eu_country_codes and partner.vat:
|
||||
# European partner.
|
||||
partner_info['IDOtro'] = {'IDType': '02', 'ID': IDOtro_ID}
|
||||
else:
|
||||
@@ -600,8 +599,6 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
if not move.company_id.vat:
|
||||
res.append(_("VAT number is missing on company %s", move.company_id.display_name))
|
||||
if not move.partner_id.vat:
|
||||
res.append(_("VAT number needs to be configured on the partner %s", move.partner_id.display_name))
|
||||
for line in move.invoice_line_ids.filtered(lambda line: not line.display_type):
|
||||
taxes = line.tax_ids.flatten_taxes_hierarchy()
|
||||
recargo_count = taxes.mapped('l10n_es_type').count('recargo')
|
||||
@@ -673,5 +670,5 @@ class AccountEdiFormat(models.Model):
|
||||
'res_model': inv._name,
|
||||
'res_id': inv.id,
|
||||
})
|
||||
res[inv] = {'attachment': attachment}
|
||||
res[inv]['attachment'] = attachment
|
||||
return res
|
||||
|
||||
@@ -7,7 +7,7 @@ import base64
|
||||
import io
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.exceptions import UserError, AccessDenied
|
||||
from flectra.tools import float_is_zero, pycompat
|
||||
from flectra.tools.misc import get_lang
|
||||
|
||||
@@ -31,7 +31,7 @@ class AccountFrFec(models.TransientModel):
|
||||
if not self.test_file:
|
||||
self.export_type = 'official'
|
||||
|
||||
def do_query_unaffected_earnings(self):
|
||||
def _do_query_unaffected_earnings(self):
|
||||
''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years.
|
||||
This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC.
|
||||
'''
|
||||
@@ -65,7 +65,6 @@ class AccountFrFec(models.TransientModel):
|
||||
am.date < %s
|
||||
AND am.company_id = %s
|
||||
AND aat.include_initial_balance IS NOT TRUE
|
||||
AND (aml.debit != 0 OR aml.credit != 0)
|
||||
'''
|
||||
# For official report: only use posted entries
|
||||
if self.export_type == "official":
|
||||
@@ -93,17 +92,15 @@ class AccountFrFec(models.TransientModel):
|
||||
"""
|
||||
dom_tom_group = self.env.ref('l10n_fr.dom-tom')
|
||||
is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped('code')
|
||||
if not is_dom_tom and not company.vat:
|
||||
raise UserError(_("Missing VAT number for company %s", company.name))
|
||||
if not is_dom_tom and company.vat[0:2] != 'FR':
|
||||
raise UserError(_("FEC is for French companies only !"))
|
||||
|
||||
return {
|
||||
'siren': company.vat[4:13] if not is_dom_tom else '',
|
||||
}
|
||||
if not company.vat or is_dom_tom:
|
||||
return {'siren': ''}
|
||||
else:
|
||||
return {'siren': company.vat[4:13]}
|
||||
|
||||
def generate_fec(self):
|
||||
self.ensure_one()
|
||||
if not (self.env.is_admin() or self.env.user.has_group('account.group_account_user')):
|
||||
raise AccessDenied()
|
||||
# We choose to implement the flat file instead of the XML
|
||||
# file for 2 reasons :
|
||||
# 1) the XSD file impose to have the label on the account.move
|
||||
@@ -148,7 +145,7 @@ class AccountFrFec(models.TransientModel):
|
||||
unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once
|
||||
if unaffected_earnings_xml_ref:
|
||||
#compute the benefit/loss of last year to add in the initial balance of the current year earnings account
|
||||
unaffected_earnings_results = self.do_query_unaffected_earnings()
|
||||
unaffected_earnings_results = self._do_query_unaffected_earnings()
|
||||
unaffected_earnings_line = False
|
||||
|
||||
sql_query = '''
|
||||
@@ -181,7 +178,6 @@ class AccountFrFec(models.TransientModel):
|
||||
am.date < %s
|
||||
AND am.company_id = %s
|
||||
AND aat.include_initial_balance = 't'
|
||||
AND (aml.debit != 0 OR aml.credit != 0)
|
||||
'''
|
||||
|
||||
# For official report: only use posted entries
|
||||
@@ -192,8 +188,7 @@ class AccountFrFec(models.TransientModel):
|
||||
|
||||
sql_query += '''
|
||||
GROUP BY aml.account_id, aat.type
|
||||
HAVING round(sum(aml.balance), %s) != 0
|
||||
AND aat.type not in ('receivable', 'payable')
|
||||
HAVING aat.type not in ('receivable', 'payable')
|
||||
'''
|
||||
formatted_date_from = fields.Date.to_string(self.date_from).replace('-', '')
|
||||
date_from = self.date_from
|
||||
@@ -201,7 +196,7 @@ class AccountFrFec(models.TransientModel):
|
||||
currency_digits = 2
|
||||
|
||||
self._cr.execute(
|
||||
sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits))
|
||||
sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id))
|
||||
|
||||
for row in self._cr.fetchall():
|
||||
listrow = list(row)
|
||||
@@ -279,7 +274,6 @@ class AccountFrFec(models.TransientModel):
|
||||
am.date < %s
|
||||
AND am.company_id = %s
|
||||
AND aat.include_initial_balance = 't'
|
||||
AND (aml.debit != 0 OR aml.credit != 0)
|
||||
'''
|
||||
|
||||
# For official report: only use posted entries
|
||||
@@ -290,11 +284,10 @@ class AccountFrFec(models.TransientModel):
|
||||
|
||||
sql_query += '''
|
||||
GROUP BY aml.account_id, aat.type, rp.ref, rp.id
|
||||
HAVING round(sum(aml.balance), %s) != 0
|
||||
AND aat.type in ('receivable', 'payable')
|
||||
HAVING aat.type in ('receivable', 'payable')
|
||||
'''
|
||||
self._cr.execute(
|
||||
sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits))
|
||||
sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id))
|
||||
|
||||
for row in self._cr.fetchall():
|
||||
listrow = list(row)
|
||||
@@ -360,7 +353,6 @@ class AccountFrFec(models.TransientModel):
|
||||
am.date >= %s
|
||||
AND am.date <= %s
|
||||
AND am.company_id = %s
|
||||
AND (aml.debit != 0 OR aml.credit != 0)
|
||||
'''
|
||||
|
||||
# For official report: only use posted entries
|
||||
|
||||
@@ -19,7 +19,7 @@ The module adds following features:
|
||||
|
||||
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
|
||||
|
||||
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ Inc. (only for Flectra Enterprise users)
|
||||
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ, Inc. (only for Flectra Enterprise users)
|
||||
""",
|
||||
'depends': ['l10n_fr', 'point_of_sale'],
|
||||
'installable': True,
|
||||
|
||||
@@ -254,11 +254,18 @@
|
||||
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''">
|
||||
<t t-if="not line.display_type" name="account_invoice_line_accountable">
|
||||
<td name="account_invoice_line_name">
|
||||
<span t-field="line.product_id.name" t-options="{'widget': 'text'}"/>
|
||||
<t t-if="line.product_id.name != line.with_context(lang='ar_001').product_id.name">
|
||||
<br/>
|
||||
<span t-field="line.with_context(lang='ar_001').product_id.name"
|
||||
t-options="{'widget': 'text'}"/>
|
||||
<t t-set="translation_name" t-value="line.with_context(lang='ar_001').product_id.name"/>
|
||||
<t t-if="line.product_id">
|
||||
<span t-field="line.product_id.name" t-options="{'widget': 'text'}"/>
|
||||
<t t-if="line.product_id.name != translation_name">
|
||||
<br/>
|
||||
<span t-field="line.with_context(lang='ar_001').product_id.name"
|
||||
t-options="{'widget': 'text'}"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="line.name and line.name != line.product_id.name and line.name != translation_name">
|
||||
<t t-if="line.product_id"><br/></t>
|
||||
<span t-field="line.name" t-options="{'widget': 'text'}"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@@ -416,10 +423,10 @@
|
||||
</b>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<p>رقم إشارة الدفعة:
|
||||
<p>
|
||||
<b>
|
||||
<span t-field="o.payment_reference"/>
|
||||
</b>
|
||||
<span t-field="o.payment_reference"/> :
|
||||
</b>رقم إشارة الدفعة
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Gulf Cooperation Council - Point of Sale',
|
||||
'author': 'FlectraHQ Inc., Odoo S.A',
|
||||
'author': 'FlectraHQ, Inc., Odoo S.A',
|
||||
'category': 'Accounting/Localizations/Point of Sale',
|
||||
'description': """
|
||||
GCC POS Localization
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'version': '1.0',
|
||||
'category': 'Accounting/Localizations/Account Charts',
|
||||
'description': """ This is the base module to manage chart of accounting and localization for Hong Kong """,
|
||||
'author': 'FlectraHQ Inc.',
|
||||
'author': 'FlectraHQ, Inc.',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'data/account_chart_template_data.xml',
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
<IdCodice t-esc="'OO99999999999'"/>
|
||||
</IdFiscaleIVA>
|
||||
<IdFiscaleIVA t-if="not record.commercial_partner_id.vat and record.commercial_partner_id.country_id.code != 'IT'">
|
||||
<IdPaese t-esc="record.commercial_partner_id.country_id.code"/>
|
||||
<IdCodice t-esc="'0000000'"/>
|
||||
</IdFiscaleIVA>
|
||||
<CodiceFiscale t-if="not record.commercial_partner_id.vat" t-esc="record.commercial_partner_id.l10n_it_codice_fiscale"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from flectra import models, _, _lt
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError, SERVER_URL
|
||||
from flectra.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError, DEFAULT_SERVER_URL
|
||||
|
||||
from lxml import etree
|
||||
import base64
|
||||
@@ -25,11 +25,12 @@ class AccountEdiFormat(models.Model):
|
||||
if self.env['ir.config_parameter'].get_param('account_edi_proxy_client.demo', False):
|
||||
return
|
||||
|
||||
server_url = self.env['ir.config_parameter'].get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
proxy_users = self.env['account_edi_proxy_client.user'].search([('edi_format_id', '=', self.env.ref('l10n_it_edi.edi_fatturaPA').id)])
|
||||
for proxy_user in proxy_users:
|
||||
company = proxy_user.company_id
|
||||
try:
|
||||
res = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/in/RicezioneInvoice',
|
||||
res = proxy_user._make_request(server_url + '/api/l10n_it_edi/1/in/RicezioneInvoice',
|
||||
params={'recipient_codice_fiscale': company.l10n_it_codice_fiscale})
|
||||
except AccountEdiProxyError as e:
|
||||
_logger.error('Error while receiving file from SdiCoop: %s', e)
|
||||
@@ -64,7 +65,7 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
if proxy_acks:
|
||||
try:
|
||||
proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/ack',
|
||||
proxy_user._make_request(server_url + '/api/l10n_it_edi/1/ack',
|
||||
params={'transaction_ids': proxy_acks})
|
||||
except AccountEdiProxyError as e:
|
||||
_logger.error('Error while receiving file from SdiCoop: %s', e)
|
||||
@@ -109,6 +110,13 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
return super()._support_batching(move=move, state=state, company=company)
|
||||
|
||||
def _get_batch_key(self, move, state):
|
||||
# OVERRIDE
|
||||
if self.code != 'fattura_pa':
|
||||
return super()._get_batch_key(move, state)
|
||||
|
||||
return move.move_type, bool(move.l10n_it_edi_transaction)
|
||||
|
||||
def _l10n_it_post_invoices_step_1(self, invoices):
|
||||
''' Send the invoices to the proxy.
|
||||
'''
|
||||
@@ -136,7 +144,7 @@ class AccountEdiFormat(models.Model):
|
||||
else:
|
||||
to_send[filename] = {
|
||||
'invoice': invoice,
|
||||
'data': {'filename': filename, 'xml': base64.b64encode(xml)}}
|
||||
'data': {'filename': filename, 'xml': base64.b64encode(xml).decode()}}
|
||||
|
||||
company = invoices.company_id
|
||||
proxy_user = self._get_proxy_user(company)
|
||||
@@ -167,6 +175,7 @@ class AccountEdiFormat(models.Model):
|
||||
def _l10n_it_post_invoices_step_2(self, invoices):
|
||||
''' Check if the sent invoices have been processed by FatturaPA.
|
||||
'''
|
||||
server_url = self.env['ir.config_parameter'].get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
to_check = {i.l10n_it_edi_transaction: i for i in invoices}
|
||||
to_return = {}
|
||||
company = invoices.company_id
|
||||
@@ -181,7 +190,7 @@ class AccountEdiFormat(models.Model):
|
||||
return {invoice: {'attachment': invoice.l10n_it_edi_attachment_id} for invoice in invoices}
|
||||
else:
|
||||
try:
|
||||
responses = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/in/TrasmissioneFatture',
|
||||
responses = proxy_user._make_request(server_url + '/api/l10n_it_edi/1/in/TrasmissioneFatture',
|
||||
params={'ids_transaction': list(to_check.keys())})
|
||||
except AccountEdiProxyError as e:
|
||||
return {invoice: {'error': e.message, 'blocking_level': 'error'} for invoice in invoices}
|
||||
@@ -207,6 +216,10 @@ class AccountEdiFormat(models.Model):
|
||||
to_return[invoice] = {'error': _('You are not allowed to check the status of this invoice.'), 'blocking_level': 'error'}
|
||||
continue
|
||||
|
||||
if not response.get('file'): # It means there is no status update, so we can skip it
|
||||
document = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'fattura_pa')
|
||||
to_return[invoice] = {'error': document.error, 'blocking_level': document.blocking_level}
|
||||
continue
|
||||
xml = proxy_user._decrypt_data(response['file'], response['key'])
|
||||
response_tree = etree.fromstring(xml)
|
||||
if state == 'ricevutaConsegna':
|
||||
@@ -225,25 +238,26 @@ class AccountEdiFormat(models.Model):
|
||||
elif state == 'notificaEsito':
|
||||
outcome = response_tree.find('Esito').text
|
||||
if outcome == 'EC01':
|
||||
to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id}
|
||||
to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
|
||||
else: # ECO2
|
||||
to_return[invoice] = {'error': _('The invoice was refused by the addressee.'), 'blocking_level': 'error'}
|
||||
elif state == 'NotificaDecorrenzaTermini':
|
||||
to_return[invoice] = {'error': _('Expiration of the maximum term for communication of acceptance/refusal'), 'blocking_level': 'error'}
|
||||
to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
|
||||
proxy_acks.append(id_transaction)
|
||||
|
||||
try:
|
||||
proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/ack',
|
||||
params={'transaction_ids': proxy_acks})
|
||||
except AccountEdiProxyError as e:
|
||||
# Will be ignored and acked again next time.
|
||||
_logger.error('Error while acking file to SdiCoop: %s', e)
|
||||
if proxy_acks:
|
||||
try:
|
||||
proxy_user._make_request(server_url + '/api/l10n_it_edi/1/ack',
|
||||
params={'transaction_ids': proxy_acks})
|
||||
except AccountEdiProxyError as e:
|
||||
# Will be ignored and acked again next time.
|
||||
_logger.error('Error while acking file to SdiCoop: %s', e)
|
||||
|
||||
return to_return
|
||||
|
||||
def _post_fattura_pa(self, invoices):
|
||||
# OVERRIDE
|
||||
if not invoices.l10n_it_edi_transaction:
|
||||
if not invoices[0].l10n_it_edi_transaction:
|
||||
return self._l10n_it_post_invoices_step_1(invoices)
|
||||
else:
|
||||
return self._l10n_it_post_invoices_step_2(invoices)
|
||||
@@ -277,7 +291,8 @@ class AccountEdiFormat(models.Model):
|
||||
'EI03': {'error': _lt('Unauthorized user'), 'blocking_level': 'error'},
|
||||
}
|
||||
|
||||
result = proxy_user._make_request(SERVER_URL + '/api/l10n_it_edi/1/out/SdiRiceviFile', params={'files': files})
|
||||
server_url = self.env['ir.config_parameter'].get_param('account_edi_proxy_client.edi_server_url', DEFAULT_SERVER_URL)
|
||||
result = proxy_user._make_request(server_url + '/api/l10n_it_edi/1/out/SdiRiceviFile', params={'files': files})
|
||||
|
||||
# Translate the errors.
|
||||
for filename in result.keys():
|
||||
|
||||
@@ -46,7 +46,7 @@ class StockPickingType(models.Model):
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = self.env['res.company'].browse(vals['company_id'])
|
||||
company = self.env['res.company'].browse(vals.get('company_id', False)) or self.env.company
|
||||
if 'l10n_it_ddt_sequence_id' not in vals or not vals['l10n_it_ddt_sequence_id'] and vals['code'] == 'outgoing' \
|
||||
and company.country_id.code == 'IT':
|
||||
ir_seq_name, ir_seq_prefix = self._get_dtt_ir_seq_vals(vals.get('warehouse_id'), vals['sequence_code'])
|
||||
@@ -54,7 +54,7 @@ class StockPickingType(models.Model):
|
||||
'name': ir_seq_name,
|
||||
'prefix': ir_seq_prefix,
|
||||
'padding': 5,
|
||||
'company_id': vals['company_id'],
|
||||
'company_id': company.id,
|
||||
'implementation': 'no_gap',
|
||||
}).id
|
||||
return super(StockPickingType, self).create(vals)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user