[PATCH] Upstream patch - 18012022

This commit is contained in:
Parthiv Patel
2022-01-18 15:56:27 +00:00
parent a6d4ddf2ea
commit 02e4797361
290 changed files with 4422 additions and 1070 deletions
+26 -18
View File
@@ -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)
+27 -13
View File
@@ -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)
+45 -17
View File
@@ -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))
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
})
+10 -4
View File
@@ -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',
+47 -7
View File
@@ -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, {
+58
View File
@@ -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
)
+8 -8
View File
@@ -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"/>
+167 -65
View File
@@ -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):
+1 -2
View File
@@ -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):
+82 -14
View File
@@ -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.
+1
View File
@@ -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.
+1 -1
View File
@@ -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 -2
View File
@@ -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">
+2 -2
View File
@@ -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
+3 -1
View File
@@ -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(':')
+21 -2
View File
@@ -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)
+63
View File
@@ -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")
+1 -1
View File
@@ -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'
})
+1 -1
View File
@@ -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" />
+1 -1
View File
@@ -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')
+7 -7
View File
@@ -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>
+3 -3
View File
@@ -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")
+6 -2
View File
@@ -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:
+10 -2
View File
@@ -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,
)
+1 -1
View File
@@ -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);
},
+1 -1
View File
@@ -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) {
+5 -1
View File
@@ -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')
+4 -4
View File
@@ -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
+3
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_hr_holidays_status_manager hr.holidays.status manager model_hr_leave_type hr_holidays.group_hr_holidays_manager 1 1 1 1
9 access_hr_holidays_status_user hr.holidays.status user model_hr_leave_type hr_holidays.group_hr_holidays_user 1 0 0 0
10 access_hr_holidays_status_employee hr.holidays.status employee model_hr_leave_type base.group_user 1 0 0 0
11 access_hr_leave_report access_hr_leave_report model_hr_leave_report base.group_user 1 0 0 0
12 access_resource_calendar_leaves_user resource_calendar_leaves_user resource.model_resource_calendar_leaves hr_holidays.group_hr_holidays_user 1 1 1 1
13 access_calendar_event_hr_user calendar.event.hr.user calendar.model_calendar_event hr_holidays.group_hr_holidays_user 1 1 1 1
14 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()
+4
View File
@@ -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')
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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">
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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;">
+1
View File
@@ -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
+24 -12
View File
@@ -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,
+21
View File
@@ -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
+7 -26
View File
@@ -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;
},
});
});
@@ -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
1 id sequence code name l10n_ar_letter report_name internal_type doc_code_prefix country_id/id purchase_aliquots
19 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
20 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
21 dc_e_fs 200 22 FACTURAS - PERMISO EXPORTACION SIMPLIFICADO - DTO. 855/97 E base.ar not_zero
22 dc_usados 210 30 COMPROBANTES DE COMPRA DE BIENES USADOS invoice CBU base.ar not_zero
23 dc_mandato 220 31 MANDATO - CONSIGNACION base.ar
24 dc_reciclado 230 32 COMPROBANTES PARA RECICLAR MATERIALES invoice CRM base.ar not_zero
25 dc_a_rg1415 240 34 COMPROBANTES A DEL APARTADO A, INC. F), R.G. Nº 1415 A invoice CA-A base.ar not_zero
26 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
27 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
28 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
29 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
30 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
31 dc_b_o_rg1415 300 40 OTROS COMPROBANTES B QUE CUMPLAN CON LA R.G. Nº 1415 B invoice OC-B base.ar zero
32 dc_c_o_rg1415 310 41 OTROS COMPROBANTES C QUE CUMPLAN CON LA R.G. Nº 1415 C invoice OC-C base.ar zero
33 dc_a_rf 320 50 RECIBO FACTURA A REGIMEN DE FACTURA DE CREDITO A base.ar not_zero
34 dc_m_f 330 51 FACTURAS M M FACTURA invoice FA-M base.ar not_zero
35 dc_m_nd 340 52 NOTAS DE DEBITO M M NOTA DE DEBITO debit_note ND-C ND-M base.ar not_zero
36 dc_m_nc 350 53 NOTAS DE CREDITO M M NOTA DE CREDITO credit_note NC-C NC-M base.ar not_zero
37 dc_m_r 360 54 RECIBOS M M RECIBO invoice RE-M base.ar not_zero
38 dc_m_nvc 370 55 NOTAS DE VENTA AL CONTADO M M invoice NVC-M base.ar not_zero
39 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
40 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
41 dc_m_cvl 400 58 CUENTAS DE VENTA Y LIQUIDO PRODUCTO M M invoice LP-M base.ar not_zero
42 dc_m_l 410 59 LIQUIDACIONES M M LIQUIDACION invoice LI-M base.ar not_zero
43 dc_a_cvl 420 60 CUENTAS DE VENTA Y LIQUIDO PRODUCTO A A CTA VTA LIQUIDO PRODUCTO invoice LP-A base.ar not_zero
44 dc_b_cvl 430 61 CUENTAS DE VENTA Y LIQUIDO PRODUCTO B B CTA VTA LIQUIDO PRODUCTO invoice LP-B base.ar zero
45 dc_a_l 440 63 LIQUIDACIONES A A LIQUIDACION invoice LI-A base.ar not_zero
46 dc_b_l 450 64 LIQUIDACIONES B B LIQUIDACION invoice LI-B base.ar zero
47 dc_nc 460 65 NOTAS DE CREDITO DE COMPROBANTES CON COD. 34, 39, 58, 59, 60, 63, 96, 97, credit_note base.ar
48 dc_desp_imp 470 66 DESPACHO DE IMPORTACION invoice DI base.ar not_zero
49 dc_imp_serv 480 67 IMPORTACION DE SERVICIOS invoice IS base.ar
50 dc_c_l 490 68 LIQUIDACION C C LIQUIDACION invoice LI-C base.ar not_zero
51 dc_rfc 500 70 RECIBOS FACTURA DE CREDITO base.ar not_zero
52 dc_cfcp 510 71 CREDITO FISCAL POR CONTRIBUCIONES PATRONALES base.ar
53 dc_f1116 520 73 FORMULARIO 1116 RT base.ar
57 dc_a_t 560 81 TIQUE FACTURA A A TIQUE FACTURA invoice TF-A base.ar not_zero
58 dc_b_t 570 82 TIQUE - FACTURA B B TIQUE FACTURA invoice TF-B base.ar zero
59 dc_t 580 83 TIQUE TIQUE invoice TI-X base.ar zero
60 dc_sp_c 590 84 COMPROBANTE FACTURA DE SERVICIOS PUBLICOS INTERESES FINANCIEROS invoice base.ar
61 dc_sp_nc 600 85 NOTA DE CREDITO SERVICIOS PUBLICOS NOTA DE CREDITO CONTROLADORES FISCALES credit_note base.ar
62 dc_sp_nd 610 86 NOTA DE DEBITO SERVICIOS PUBLICOS debit_note base.ar
63 dc_oc_se 620 87 OTROS COMPROBANTES - SERVICIOS DEL EXTERIOR invoice base.ar
64 dc_oc_c 630 88 REMITO ELECTRONICO base.ar
65 dc_oc_nd 640 89 RESUMEN DE DATOS debit_note base.ar
66 dc_oc_nc 650 90 OTROS COMPROBANTES - DOCUMENTOS EXCEPTUADOS - NOTAS DE CREDITO credit_note OC base.ar not_zero
67 dc_r_r 660 91 REMITOS R R base.ar
68 dc_ac_inc_df 670 92 AJUSTES CONTABLES QUE INCREMENTAN EL DEBITO FISCAL base.ar
69 dc_ac_dis_df 680 93 AJUSTES CONTABLES QUE DISMINUYEN EL DEBITO FISCAL base.ar
74 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
75 dc_aa_dj_pos 740 101 AJUSTE ANUAL PROVENIENTE DE LA D J DEL IVA POSITIVO base.ar
76 dc_aa_dj_neg 750 102 AJUSTE ANUAL PROVENIENTE DE LA D J DEL IVA NEGATIVO base.ar
77 dc_na 760 103 NOTA DE ASIGNACION invoice base.ar
78 dc_nca 770 104 NOTA DE CREDITO DE ASIGNACION credit_note base.ar
79 dc_remito_x 790 94 REMITOS X X REMITO RM-X base.ar
80 dc_liq_s_a 800 17 LIQUIDACION DE SERVICIOS PUBLICOS CLASE A A invoice LS-A base.ar not_zero
81 dc_liq_s_b 810 18 LIQUIDACION DE SERVICIOS PUBLICOS CLASE B B invoice LS-B base.ar zero
86 dc_liq_uci_a 860 27 LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A A invoice LU-A base.ar not_zero
87 dc_liq_uci_b 870 28 LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B B invoice LU-B base.ar zero
88 dc_liq_uci_c 880 29 LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C C invoice LU-C base.ar zero
89 dc_liq_prim_gr 890 33 LIQUIDACION PRIMARIA DE GRANOS invoice LPG base.ar not_zero
90 dc_nc_liq_uci_a 900 43 NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B B credit_note NCLU-B base.ar zero
91 dc_nc_liq_uci_b 910 44 NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C C credit_note NCLU-C base.ar zero
92 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
93 dc_nd_liq_uci_b 930 46 NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B B debit_note NDLU-B base.ar zero
94 dc_nd_liq_uci_c 940 47 NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C C debit_note NDLU-C base.ar zero
95 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
96 dc_bs_no_reg 960 49 COMPROBANTES DE COMPRA DE BIENES NO REGISTRABLES A CONSUMIDORES FINALES invoice BNR base.ar not_zero
97 dc_t_nc 970 110 TIQUE NOTA DE CREDITO TIQUE NOTA DE CREDITO credit_note TC-X base.ar not_zero
98 dc_t_c 980 111 TIQUE FACTURA C C TIQUE FACTURA invoice TF-C base.ar zero
99 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 dc_t_m 1050 118 TIQUE FACTURA M M FACTURA invoice TF-M base.ar not_zero
106 dc_t_nc_m 1060 119 TIQUE NOTA DE CREDITO M M NOTA DE CREDITO credit_note TC-M base.ar not_zero
107 dc_t_nd_m 1070 120 TIQUE NOTA DE DEBITO M M NOTA DE DEBITO debit_note TD-M base.ar not_zero
108 dc_liq_sec_gr 1080 331 LIQUIDACION SECUNDARIA DE GRANOS invoice LSG base.ar not_zero
109 dc_cert_ele_gr 1090 332 CERTIFICACION ELECTRONICA (GRANOS) base.ar
110 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
111 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>
+1 -1
View File
@@ -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"
1 id name code user_type_id/id chart_template_id/id tag_ids/id reconcile
253 a493 Deferred income 493 account.data_account_type_current_liabilities l10n_be.l10nbe_chart_template False
254 a496 Foreign currency translation differences - Assets 496 account.data_account_type_current_assets l10n_be.l10nbe_chart_template False
255 a497 Foreign currency translation differences - Liabilities 497 account.data_account_type_current_liabilities l10n_be.l10nbe_chart_template False
256 a499 Suspense account 499 account.data_account_type_current_liabilities account.data_account_type_current_assets l10n_be.l10nbe_chart_template False
257 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
258 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
259 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',
+2 -1
View File
@@ -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)
+2
View File
@@ -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)
+1 -1
View File
@@ -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"
1 id name code chart_template_id/id user_type_id/id reconcile
82 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
83 chart_cz_257000 Ostatní cenné papíry 257000 l10n_cz.cz_chart_template account.data_account_type_liquidity False
84 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
85 chart_cz_261000 Peníze na cestě 261000 l10n_cz.cz_chart_template account.data_account_type_liquidity account.data_account_type_current_assets False
86 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
87 chart_cz_311000 Odběratelé 311000 l10n_cz.cz_chart_template account.data_account_type_receivable True
88 chart_cz_313000 Pohledávky za eskontované cenné papíry 313000 l10n_cz.cz_chart_template account.data_account_type_receivable True
+1
View File
@@ -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
+16
View File
@@ -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 = ''
+6 -3
View File
@@ -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
+13 -21
View File
@@ -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
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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