mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
Master upstream patch
This commit is contained in:
@@ -49,9 +49,9 @@ class PortalAccount(CustomerPortal):
|
||||
order = searchbar_sortings[sortby]['order']
|
||||
|
||||
searchbar_filters = {
|
||||
'all': {'label': _('All'), 'domain': [('move_type', 'in', ['in_invoice', 'out_invoice'])]},
|
||||
'invoices': {'label': _('Invoices'), 'domain': [('move_type', '=', 'out_invoice')]},
|
||||
'bills': {'label': _('Bills'), 'domain': [('move_type', '=', 'in_invoice')]},
|
||||
'all': {'label': _('All'), 'domain': []},
|
||||
'invoices': {'label': _('Invoices'), 'domain': [('move_type', '=', ('out_invoice', 'out_refund'))]},
|
||||
'bills': {'label': _('Bills'), 'domain': [('move_type', '=', ('in_invoice', 'in_refund'))]},
|
||||
}
|
||||
# default filter by value
|
||||
if not filterby:
|
||||
|
||||
@@ -617,16 +617,23 @@ class AccountGroup(models.Model):
|
||||
"""
|
||||
if not self and not account_ids:
|
||||
return
|
||||
self.env['account.group'].flush()
|
||||
self.env['account.account'].flush()
|
||||
self.env['account.group'].flush(self.env['account.group']._fields)
|
||||
self.env['account.account'].flush(self.env['account.account']._fields)
|
||||
query = """
|
||||
UPDATE account_account account SET group_id = (
|
||||
SELECT agroup.id FROM account_group agroup
|
||||
WHERE 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
|
||||
ORDER BY char_length(agroup.code_prefix_start) DESC LIMIT 1
|
||||
) WHERE account.company_id in %(company_ids)s {where_account};
|
||||
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
|
||||
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}
|
||||
)
|
||||
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 ''
|
||||
)
|
||||
@@ -642,21 +649,28 @@ class AccountGroup(models.Model):
|
||||
"""
|
||||
if not self:
|
||||
return
|
||||
self.env['account.group'].flush()
|
||||
self.env['account.group'].flush(self.env['account.group']._fields)
|
||||
query = """
|
||||
UPDATE account_group agroup SET parent_id = (
|
||||
SELECT parent.id FROM account_group parent
|
||||
WHERE char_length(parent.code_prefix_start) < char_length(agroup.code_prefix_start)
|
||||
AND parent.code_prefix_start <= LEFT(agroup.code_prefix_start, char_length(parent.code_prefix_start))
|
||||
AND parent.code_prefix_end >= LEFT(agroup.code_prefix_end, char_length(parent.code_prefix_end))
|
||||
AND parent.id != agroup.id
|
||||
AND parent.company_id = %(company_id)s
|
||||
ORDER BY char_length(parent.code_prefix_start) DESC LIMIT 1
|
||||
) WHERE agroup.company_id = %(company_id)s;
|
||||
WITH relation AS (
|
||||
SELECT DISTINCT FIRST_VALUE(parent.id) OVER (PARTITION BY child.id ORDER BY child.id, char_length(parent.code_prefix_start) DESC) AS parent_id,
|
||||
child.id AS child_id
|
||||
FROM account_group parent
|
||||
JOIN account_group child
|
||||
ON char_length(parent.code_prefix_start) < char_length(child.code_prefix_start)
|
||||
AND parent.code_prefix_start <= LEFT(child.code_prefix_start, char_length(parent.code_prefix_start))
|
||||
AND parent.code_prefix_end >= LEFT(child.code_prefix_end, char_length(parent.code_prefix_end))
|
||||
AND parent.id != child.id
|
||||
AND parent.company_id = child.company_id
|
||||
WHERE child.company_id IN %(company_ids)s
|
||||
)
|
||||
UPDATE account_group child
|
||||
SET parent_id = relation.parent_id
|
||||
FROM relation
|
||||
WHERE child.id = relation.child_id;
|
||||
"""
|
||||
self.env.cr.execute(query, {'company_id': self.company_id.id})
|
||||
self.env.cr.execute(query, {'company_ids': tuple(self.company_id.ids)})
|
||||
self.env['account.group'].invalidate_cache(fnames=['parent_id'])
|
||||
self.env['account.group'].search([('company_id', '=', self.company_id.id)])._parent_store_update()
|
||||
self.env['account.group'].search([('company_id', 'in', self.company_id.ids)])._parent_store_update()
|
||||
|
||||
|
||||
class AccountRoot(models.Model):
|
||||
|
||||
@@ -697,9 +697,6 @@ class AccountJournal(models.Model):
|
||||
''' Get the outstanding payments balance of the current journal by filtering the journal items using the
|
||||
journal's accounts.
|
||||
|
||||
/!\ The current journal is not part of the applied domain. This is the expected behavior since we only want
|
||||
a logic based on accounts.
|
||||
|
||||
:param domain: An additional domain to be applied on the account.move.line model.
|
||||
:param date: The date to be used when performing the currency conversions.
|
||||
:return: The balance expressed in the journal's currency.
|
||||
@@ -722,6 +719,7 @@ class AccountJournal(models.Model):
|
||||
('display_type', 'not in', ('line_section', 'line_note')),
|
||||
('move_id.state', '!=', 'cancel'),
|
||||
('reconciled', '=', False),
|
||||
('journal_id', '=', self.id),
|
||||
]
|
||||
query = self.env['account.move.line']._where_calc(domain)
|
||||
tables, where_clause, where_params = query.get_sql()
|
||||
|
||||
@@ -194,6 +194,7 @@ class AccountMove(models.Model):
|
||||
payment_reference = fields.Char(string='Payment Reference', index=True, copy=False,
|
||||
help="The payment reference to set on journal items.")
|
||||
payment_id = fields.Many2one(
|
||||
index=True,
|
||||
comodel_name='account.payment',
|
||||
string="Payment", copy=False, check_company=True)
|
||||
statement_line_id = fields.Many2one(
|
||||
@@ -580,7 +581,7 @@ class AccountMove(models.Model):
|
||||
quantity = 1.0
|
||||
tax_type = base_line.tax_ids[0].type_tax_use if base_line.tax_ids else None
|
||||
is_refund = (tax_type == 'sale' and base_line.debit) or (tax_type == 'purchase' and base_line.credit)
|
||||
price_unit_wo_discount = base_line.balance
|
||||
price_unit_wo_discount = base_line.amount_currency
|
||||
|
||||
balance_taxes_res = base_line.tax_ids._origin.with_context(force_sign=move._get_tax_force_sign()).compute_all(
|
||||
price_unit_wo_discount,
|
||||
@@ -1114,13 +1115,15 @@ class AccountMove(models.Model):
|
||||
# the same counter for multiple groups that might be spread in multiple months.
|
||||
final_batches = []
|
||||
for journal_group in grouped.values():
|
||||
journal_group_changed = True
|
||||
for date_group in journal_group.values():
|
||||
if (
|
||||
not final_batches
|
||||
journal_group_changed
|
||||
or final_batches[-1]['format'] != date_group['format']
|
||||
or dict(final_batches[-1]['format_values'], seq=0) != dict(date_group['format_values'], seq=0)
|
||||
):
|
||||
final_batches += [date_group]
|
||||
journal_group_changed = False
|
||||
elif date_group['reset'] == 'never':
|
||||
final_batches[-1]['records'] += date_group['records']
|
||||
elif (
|
||||
@@ -2806,7 +2809,10 @@ class AccountMove(models.Model):
|
||||
('date', '<=', fields.Date.context_today(self)),
|
||||
('auto_post', '=', True),
|
||||
])
|
||||
records._post()
|
||||
for ids in self._cr.split_for_in_conditions(records.ids, size=1000):
|
||||
self.browse(ids)._post()
|
||||
if not self.env.registry.in_test_mode():
|
||||
self._cr.commit()
|
||||
|
||||
# offer the possibility to duplicate thanks to a button instead of a hidden menu, which is more visible
|
||||
def action_duplicate(self):
|
||||
@@ -2827,7 +2833,8 @@ class AccountMove(models.Model):
|
||||
}
|
||||
for line in preview_vals['items_vals']:
|
||||
if 'partner_id' in line[2]:
|
||||
line[2]['partner_id'] = self.env['res.partner'].browse(line[2]['partner_id']).display_name
|
||||
# sudo is needed to compute display_name in a multi companies environment
|
||||
line[2]['partner_id'] = self.env['res.partner'].browse(line[2]['partner_id']).sudo().display_name
|
||||
line[2]['account_id'] = self.env['account.account'].browse(line[2]['account_id']).display_name or _('Destination Account')
|
||||
line[2]['debit'] = currency_id and formatLang(self.env, line[2]['debit'], currency_obj=currency_id) or line[2]['debit']
|
||||
line[2]['credit'] = currency_id and formatLang(self.env, line[2]['credit'], currency_obj=currency_id) or line[2]['debit']
|
||||
@@ -3154,23 +3161,75 @@ class AccountMoveLine(models.Model):
|
||||
return '\n'.join(values)
|
||||
|
||||
def _get_computed_price_unit(self):
|
||||
''' Helper to get the default price unit based on the product by taking care of the taxes
|
||||
set on the product and the fiscal position.
|
||||
:return: The price unit.
|
||||
'''
|
||||
self.ensure_one()
|
||||
|
||||
if not self.product_id:
|
||||
return self.price_unit
|
||||
elif self.move_id.is_sale_document(include_receipts=True):
|
||||
# Out invoice.
|
||||
price_unit = self.product_id.lst_price
|
||||
return 0.0
|
||||
|
||||
company = self.move_id.company_id
|
||||
currency = self.move_id.currency_id
|
||||
company_currency = company.currency_id
|
||||
product_uom = self.product_id.uom_id
|
||||
fiscal_position = self.move_id.fiscal_position_id
|
||||
is_refund_document = self.move_id.move_type in ('out_refund', 'in_refund')
|
||||
move_date = self.move_id.date or fields.Date.context_today(self)
|
||||
|
||||
if self.move_id.is_sale_document(include_receipts=True):
|
||||
product_price_unit = self.product_id.lst_price
|
||||
product_taxes = self.product_id.taxes_id
|
||||
elif self.move_id.is_purchase_document(include_receipts=True):
|
||||
# In invoice.
|
||||
price_unit = self.product_id.standard_price
|
||||
product_price_unit = self.product_id.standard_price
|
||||
product_taxes = self.product_id.supplier_taxes_id
|
||||
else:
|
||||
return self.price_unit
|
||||
return 0.0
|
||||
product_taxes = product_taxes.filtered(lambda tax: tax.company_id == company)
|
||||
|
||||
if self.product_uom_id != self.product_id.uom_id:
|
||||
price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom_id)
|
||||
# Apply unit of measure.
|
||||
if self.product_uom_id and self.product_uom_id != product_uom:
|
||||
product_price_unit = product_uom._compute_price(product_price_unit, self.product_uom_id)
|
||||
|
||||
return price_unit
|
||||
# Apply fiscal position.
|
||||
if product_taxes and fiscal_position:
|
||||
product_taxes_after_fp = fiscal_position.map_tax(product_taxes, partner=self.partner_id)
|
||||
|
||||
if set(product_taxes.ids) != set(product_taxes_after_fp.ids):
|
||||
flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy()
|
||||
if any(tax.price_include for tax in flattened_taxes_before_fp):
|
||||
taxes_res = flattened_taxes_before_fp.compute_all(
|
||||
product_price_unit,
|
||||
quantity=1.0,
|
||||
currency=company_currency,
|
||||
product=self.product_id,
|
||||
partner=self.partner_id,
|
||||
is_refund=is_refund_document,
|
||||
)
|
||||
product_price_unit = company_currency.round(taxes_res['total_excluded'])
|
||||
|
||||
flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy()
|
||||
if any(tax.price_include for tax in flattened_taxes_after_fp):
|
||||
taxes_res = flattened_taxes_after_fp.compute_all(
|
||||
product_price_unit,
|
||||
quantity=1.0,
|
||||
currency=company_currency,
|
||||
product=self.product_id,
|
||||
partner=self.partner_id,
|
||||
is_refund=is_refund_document,
|
||||
handle_price_include=False,
|
||||
)
|
||||
for tax_res in taxes_res['taxes']:
|
||||
tax = self.env['account.tax'].browse(tax_res['id'])
|
||||
if tax.price_include:
|
||||
product_price_unit += tax_res['amount']
|
||||
|
||||
# Apply currency rate.
|
||||
if currency and currency != company_currency:
|
||||
product_price_unit = company_currency._convert(product_price_unit, currency, company, move_date)
|
||||
|
||||
return product_price_unit
|
||||
|
||||
def _get_computed_account(self):
|
||||
self.ensure_one()
|
||||
@@ -3471,33 +3530,21 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
line.name = line._get_computed_name()
|
||||
line.account_id = line._get_computed_account()
|
||||
line.tax_ids = line._get_computed_taxes()
|
||||
taxes = line._get_computed_taxes()
|
||||
if taxes and line.move_id.fiscal_position_id:
|
||||
taxes = line.move_id.fiscal_position_id.map_tax(taxes, partner=line.partner_id)
|
||||
line.tax_ids = taxes
|
||||
line.product_uom_id = line._get_computed_uom()
|
||||
line.price_unit = line._get_computed_price_unit()
|
||||
|
||||
# price_unit and taxes may need to be adapted following Fiscal Position
|
||||
line._set_price_and_tax_after_fpos()
|
||||
|
||||
# Convert the unit price to the invoice's currency.
|
||||
company = line.move_id.company_id
|
||||
line.price_unit = company.currency_id._convert(line.price_unit, line.move_id.currency_id, company, line.move_id.date, round=False)
|
||||
|
||||
@api.onchange('product_uom_id')
|
||||
def _onchange_uom_id(self):
|
||||
''' Recompute the 'price_unit' depending of the unit of measure. '''
|
||||
price_unit = self._get_computed_price_unit()
|
||||
|
||||
# See '_onchange_product_id' for details.
|
||||
taxes = self._get_computed_taxes()
|
||||
if taxes and self.move_id.fiscal_position_id:
|
||||
price_subtotal = self._get_price_total_and_subtotal(price_unit=price_unit, taxes=taxes)['price_subtotal']
|
||||
accounting_vals = self._get_fields_onchange_subtotal(price_subtotal=price_subtotal, currency=self.move_id.company_currency_id)
|
||||
amount_currency = accounting_vals['amount_currency']
|
||||
price_unit = self._get_fields_onchange_balance(amount_currency=amount_currency, force_computation=True).get('price_unit', price_unit)
|
||||
|
||||
# Convert the unit price to the invoice's currency.
|
||||
company = self.move_id.company_id
|
||||
self.price_unit = company.currency_id._convert(price_unit, self.move_id.currency_id, company, self.move_id.date, round=False)
|
||||
taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
|
||||
self.tax_ids = taxes
|
||||
self.price_unit = self._get_computed_price_unit()
|
||||
|
||||
@api.onchange('account_id')
|
||||
def _onchange_account_id(self):
|
||||
@@ -4384,10 +4431,17 @@ class AccountMoveLine(models.Model):
|
||||
continue
|
||||
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix)
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': account_to_fix.id,
|
||||
}
|
||||
|
||||
if grouping_key not in account_vals_to_fix:
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': account_to_fix.id,
|
||||
}
|
||||
else:
|
||||
# Multiple base lines could share the same key, if the same
|
||||
# cash basis tax is used alone on several lines of the invoices
|
||||
account_vals_to_fix[grouping_key]['debit'] += vals['debit']
|
||||
account_vals_to_fix[grouping_key]['credit'] += vals['credit']
|
||||
|
||||
# ==========================================================================
|
||||
# Subtract the balance of all previously generated cash basis journal entries
|
||||
|
||||
@@ -187,23 +187,32 @@ class AccountPayment(models.Model):
|
||||
self.journal_id.display_name))
|
||||
|
||||
# Compute amounts.
|
||||
write_off_amount = write_off_line_vals.get('amount', 0.0)
|
||||
write_off_amount_currency = write_off_line_vals.get('amount', 0.0)
|
||||
|
||||
if self.payment_type == 'inbound':
|
||||
# Receive money.
|
||||
counterpart_amount = -self.amount
|
||||
write_off_amount *= -1
|
||||
liquidity_amount_currency = self.amount
|
||||
elif self.payment_type == 'outbound':
|
||||
# Send money.
|
||||
counterpart_amount = self.amount
|
||||
liquidity_amount_currency = -self.amount
|
||||
write_off_amount_currency *= -1
|
||||
else:
|
||||
counterpart_amount = 0.0
|
||||
write_off_amount = 0.0
|
||||
liquidity_amount_currency = write_off_amount_currency = 0.0
|
||||
|
||||
balance = self.currency_id._convert(counterpart_amount, self.company_id.currency_id, self.company_id, self.date)
|
||||
counterpart_amount_currency = counterpart_amount
|
||||
write_off_balance = self.currency_id._convert(write_off_amount, self.company_id.currency_id, self.company_id, self.date)
|
||||
write_off_amount_currency = write_off_amount
|
||||
write_off_balance = self.currency_id._convert(
|
||||
write_off_amount_currency,
|
||||
self.company_id.currency_id,
|
||||
self.company_id,
|
||||
self.date,
|
||||
)
|
||||
liquidity_balance = self.currency_id._convert(
|
||||
liquidity_amount_currency,
|
||||
self.company_id.currency_id,
|
||||
self.company_id,
|
||||
self.date,
|
||||
)
|
||||
counterpart_amount_currency = -liquidity_amount_currency - write_off_amount_currency
|
||||
counterpart_balance = -liquidity_balance - write_off_balance
|
||||
currency_id = self.currency_id.id
|
||||
|
||||
if self.is_internal_transfer:
|
||||
@@ -236,33 +245,33 @@ class AccountPayment(models.Model):
|
||||
{
|
||||
'name': liquidity_line_name or default_line_name,
|
||||
'date_maturity': self.date,
|
||||
'amount_currency': -counterpart_amount_currency,
|
||||
'amount_currency': liquidity_amount_currency,
|
||||
'currency_id': currency_id,
|
||||
'debit': balance < 0.0 and -balance or 0.0,
|
||||
'credit': balance > 0.0 and balance or 0.0,
|
||||
'debit': liquidity_balance if liquidity_balance > 0.0 else 0.0,
|
||||
'credit': -liquidity_balance if liquidity_balance < 0.0 else 0.0,
|
||||
'partner_id': self.partner_id.id,
|
||||
'account_id': self.journal_id.payment_debit_account_id.id if balance < 0.0 else self.journal_id.payment_credit_account_id.id,
|
||||
'account_id': self.journal_id.payment_credit_account_id.id if liquidity_balance < 0.0 else self.journal_id.payment_debit_account_id.id,
|
||||
},
|
||||
# Receivable / Payable.
|
||||
{
|
||||
'name': self.payment_reference or default_line_name,
|
||||
'date_maturity': self.date,
|
||||
'amount_currency': counterpart_amount_currency + write_off_amount_currency if currency_id else 0.0,
|
||||
'amount_currency': counterpart_amount_currency,
|
||||
'currency_id': currency_id,
|
||||
'debit': balance + write_off_balance > 0.0 and balance + write_off_balance or 0.0,
|
||||
'credit': balance + write_off_balance < 0.0 and -balance - write_off_balance or 0.0,
|
||||
'debit': counterpart_balance if counterpart_balance > 0.0 else 0.0,
|
||||
'credit': -counterpart_balance if counterpart_balance < 0.0 else 0.0,
|
||||
'partner_id': self.partner_id.id,
|
||||
'account_id': self.destination_account_id.id,
|
||||
},
|
||||
]
|
||||
if write_off_balance:
|
||||
if not self.currency_id.is_zero(write_off_amount_currency):
|
||||
# Write-off line.
|
||||
line_vals_list.append({
|
||||
'name': write_off_line_vals.get('name') or default_line_name,
|
||||
'amount_currency': -write_off_amount_currency,
|
||||
'amount_currency': write_off_amount_currency,
|
||||
'currency_id': currency_id,
|
||||
'debit': write_off_balance < 0.0 and -write_off_balance or 0.0,
|
||||
'credit': write_off_balance > 0.0 and write_off_balance or 0.0,
|
||||
'debit': write_off_balance if write_off_balance > 0.0 else 0.0,
|
||||
'credit': -write_off_balance if write_off_balance < 0.0 else 0.0,
|
||||
'partner_id': self.partner_id.id,
|
||||
'account_id': write_off_line_vals.get('account_id'),
|
||||
})
|
||||
@@ -344,7 +353,9 @@ class AccountPayment(models.Model):
|
||||
available_payment_methods = pay.journal_id.outbound_payment_method_ids
|
||||
|
||||
# Select the first available one by default.
|
||||
if available_payment_methods:
|
||||
if pay.payment_method_id in available_payment_methods:
|
||||
pay.payment_method_id = pay.payment_method_id
|
||||
elif available_payment_methods:
|
||||
pay.payment_method_id = available_payment_methods[0]._origin
|
||||
else:
|
||||
pay.payment_method_id = False
|
||||
@@ -678,12 +689,15 @@ class AccountPayment(models.Model):
|
||||
})
|
||||
payment_vals_to_write.update({
|
||||
'amount': abs(liquidity_amount),
|
||||
'payment_type': 'inbound' if liquidity_amount > 0.0 else 'outbound',
|
||||
'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,
|
||||
})
|
||||
if liquidity_amount > 0.0:
|
||||
payment_vals_to_write.update({'payment_type': 'inbound'})
|
||||
elif liquidity_amount < 0.0:
|
||||
payment_vals_to_write.update({'payment_type': 'outbound'})
|
||||
|
||||
move.write(move._cleanup_write_orm_values(move, move_vals_to_write))
|
||||
pay.write(move._cleanup_write_orm_values(pay, payment_vals_to_write))
|
||||
@@ -708,16 +722,21 @@ class AccountPayment(models.Model):
|
||||
# This allows to create a new payment with custom 'line_ids'.
|
||||
|
||||
if writeoff_lines:
|
||||
counterpart_amount = sum(counterpart_lines.mapped('amount_currency'))
|
||||
writeoff_amount = sum(writeoff_lines.mapped('amount_currency'))
|
||||
counterpart_amount = counterpart_lines['amount_currency']
|
||||
if writeoff_amount > 0.0 and counterpart_amount > 0.0:
|
||||
sign = 1
|
||||
else:
|
||||
|
||||
# To be consistent with the payment_difference made in account.payment.register,
|
||||
# 'writeoff_amount' needs to be signed regarding the 'amount' field before the write.
|
||||
# Since the write is already done at this point, we need to base the computation on accounting values.
|
||||
if (counterpart_amount > 0.0) == (writeoff_amount > 0.0):
|
||||
sign = -1
|
||||
else:
|
||||
sign = 1
|
||||
writeoff_amount = abs(writeoff_amount) * sign
|
||||
|
||||
write_off_line_vals = {
|
||||
'name': writeoff_lines[0].name,
|
||||
'amount': writeoff_amount * sign,
|
||||
'amount': writeoff_amount,
|
||||
'account_id': writeoff_lines[0].account_id.id,
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -30,7 +30,7 @@ class AccountReconcileModelPartnerMapping(models.Model):
|
||||
re.compile(record.payment_ref_regex)
|
||||
if record.narration_regex:
|
||||
current_regex = record.narration_regex
|
||||
re.compile(record.narration_regex)
|
||||
re.compile(record.narration_regex)
|
||||
except re.error:
|
||||
raise ValidationError(_("The following regular expression is invalid to create a partner mapping: %s") % current_regex)
|
||||
|
||||
@@ -612,6 +612,7 @@ class AccountReconcileModel(models.Model):
|
||||
LEFT JOIN account_move move ON move.id = aml.move_id AND move.state = 'posted'
|
||||
LEFT JOIN account_account account ON account.id = aml.account_id
|
||||
LEFT JOIN res_partner aml_partner ON aml.partner_id = aml_partner.id
|
||||
LEFT JOIN account_payment payment ON payment.move_id = move.id
|
||||
WHERE
|
||||
aml.company_id = st_line_move.company_id
|
||||
AND move.state = 'posted'
|
||||
@@ -725,9 +726,11 @@ class AccountReconcileModel(models.Model):
|
||||
st_ref_list += ['st_line_move.ref']
|
||||
if not st_ref_list:
|
||||
return "FALSE"
|
||||
return r'''(move.payment_reference IS NOT NULL AND ({}))'''.format(
|
||||
|
||||
# payment_reference is not used on account.move for payments; ref is used instead
|
||||
return r'''((move.payment_reference IS NOT NULL OR (payment.id IS NOT NULL AND move.ref IS NOT NULL)) AND ({}))'''.format(
|
||||
' OR '.join(
|
||||
rf"regexp_replace(move.payment_reference, '\s+', '', 'g') = regexp_replace({st_ref}, '\s+', '', 'g')"
|
||||
rf"regexp_replace(CASE WHEN payment.id IS NULL THEN move.payment_reference ELSE move.ref END, '\s+', '', 'g') = regexp_replace({st_ref}, '\s+', '', 'g')"
|
||||
for st_ref in st_ref_list
|
||||
)
|
||||
)
|
||||
@@ -881,8 +884,7 @@ class AccountReconcileModel(models.Model):
|
||||
# Statement line amount is equal to the total residual.
|
||||
if line_currency.is_zero(line_residual_after_reconciliation):
|
||||
return True
|
||||
|
||||
reconciled_percentage = (abs(line_residual) - abs(line_residual_after_reconciliation)) / abs(line_residual) * 100
|
||||
reconciled_percentage = 100 - abs(line_residual_after_reconciliation) / abs(line_residual - line_residual_after_reconciliation) * 100
|
||||
return reconciled_percentage >= self.match_total_amount_param
|
||||
|
||||
def _filter_candidates(self, candidates, aml_ids_to_exclude, reconciled_amls_ids):
|
||||
|
||||
@@ -17,12 +17,13 @@ class Digest(models.Model):
|
||||
for record in self:
|
||||
start, end, company = record._get_kpi_compute_parameters()
|
||||
self._cr.execute('''
|
||||
SELECT SUM(line.debit)
|
||||
SELECT -SUM(line.balance)
|
||||
FROM account_move_line line
|
||||
JOIN account_move move ON move.id = line.move_id
|
||||
JOIN account_journal journal ON journal.id = move.journal_id
|
||||
JOIN account_account account ON account.id = line.account_id
|
||||
WHERE line.company_id = %s AND line.date >= %s AND line.date < %s
|
||||
AND journal.type = 'sale'
|
||||
AND account.internal_group = 'income'
|
||||
AND move.state = 'posted'
|
||||
''', [company.id, start, end])
|
||||
query_res = self._cr.fetchone()
|
||||
record.kpi_account_total_revenue_value = query_res and query_res[0] or 0.0
|
||||
|
||||
@@ -56,7 +56,7 @@ class AccountFiscalPosition(models.Model):
|
||||
return taxes
|
||||
result = self.env['account.tax']
|
||||
for tax in taxes:
|
||||
taxes_correspondance = self.tax_ids.filtered(lambda t: t.tax_src_id == tax)
|
||||
taxes_correspondance = self.tax_ids.filtered(lambda t: t.tax_src_id == tax._origin)
|
||||
result |= taxes_correspondance.tax_dest_id if taxes_correspondance else tax
|
||||
return result
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ tour.register('account_tour', {
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] textarea[name=name]",
|
||||
extra_trigger: "[name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Fill in the details of the line.<br><i>Tip: all the details can be set automatically if you configure your <b>products</b>.</i>"),
|
||||
content: _t("Fill in the details of the line."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] input[name=price_unit]",
|
||||
|
||||
@@ -19,6 +19,7 @@ from . import test_account_invoice_report
|
||||
from . import test_account_journal_dashboard
|
||||
from . import test_fiscal_position
|
||||
from . import test_reconciliation
|
||||
from . import test_sequence_mixin
|
||||
from . import test_settings
|
||||
from . import test_tax
|
||||
from . import test_invoice_taxes
|
||||
|
||||
@@ -342,325 +342,6 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
# You can remove journal items if the related journal entry is still balanced.
|
||||
self.test_move.line_ids.unlink()
|
||||
|
||||
def test_sequence_change_date(self):
|
||||
# Check setup
|
||||
self.assertEqual(self.test_move.state, 'draft')
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.assertEqual(fields.Date.to_string(self.test_move.date), '2016-01-01')
|
||||
|
||||
# Never posetd, the number must change if we change the date
|
||||
self.test_move.date = '2020-02-02'
|
||||
self.assertEqual(self.test_move.name, 'MISC/2020/02/0001')
|
||||
|
||||
# We don't recompute user's input when posting
|
||||
self.test_move.name = 'MyMISC/2020/0000001'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
|
||||
# Has been posted, and it doesn't change anymore
|
||||
self.test_move.button_draft()
|
||||
self.test_move.date = '2020-01-02'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
|
||||
def test_journal_sequence(self):
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
|
||||
copy1 = self.test_move.copy({'date': self.test_move.date})
|
||||
self.assertEqual(copy1.name, '/')
|
||||
copy1.action_post()
|
||||
self.assertEqual(copy1.name, 'MISC/2016/01/0002')
|
||||
|
||||
copy2 = self.test_move.copy({'date': self.test_move.date})
|
||||
new_journal = self.test_move.journal_id.copy()
|
||||
new_journal.code = "MISC2"
|
||||
copy2.journal_id = new_journal
|
||||
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
|
||||
with Form(copy2) as move_form: # It is editable in the form
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
move_form.journal_id = self.test_move.journal_id
|
||||
self.assertEqual(move_form.name, '/')
|
||||
move_form.journal_id = new_journal
|
||||
self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
copy2.action_post()
|
||||
self.assertEqual(copy2.name, 'MyMISC/2016/0001')
|
||||
|
||||
copy3 = copy2.copy({'date': copy2.date})
|
||||
self.assertEqual(copy3.name, '/')
|
||||
with self.assertRaises(AssertionError):
|
||||
with Form(copy2) as move_form: # It is not editable in the form
|
||||
move_form.name = 'MyMISC/2016/0002'
|
||||
copy3.action_post()
|
||||
self.assertEqual(copy3.name, 'MyMISC/2016/0002')
|
||||
copy3.name = 'MISC2/2016/00002'
|
||||
|
||||
copy4 = copy2.copy({'date': copy2.date})
|
||||
copy4.action_post()
|
||||
self.assertEqual(copy4.name, 'MISC2/2016/00003')
|
||||
|
||||
copy5 = copy2.copy({'date': copy2.date})
|
||||
copy5.date = '2021-02-02'
|
||||
copy5.action_post()
|
||||
self.assertEqual(copy5.name, 'MISC2/2021/00001')
|
||||
copy5.name = 'N\'importe quoi?'
|
||||
|
||||
copy6 = copy5.copy({'date': copy5.date})
|
||||
copy6.action_post()
|
||||
self.assertEqual(copy6.name, 'N\'importe quoi?1')
|
||||
|
||||
def test_journal_sequence_format(self):
|
||||
"""Test different format of sequences and what it becomes on another period"""
|
||||
sequences = [
|
||||
('JRNL/2016/00001', 'JRNL/2016/00002', 'JRNL/2016/00003', 'JRNL/2017/00001'),
|
||||
('1234567', '1234568', '1234569', '1234570'),
|
||||
('20190910', '20190911', '20190912', '20190913'),
|
||||
('2016-0910', '2016-0911', '2016-0912', '2017-0001'),
|
||||
('201603-10', '201603-11', '201604-01', '201703-01'),
|
||||
('16-03-10', '16-03-11', '16-04-01', '17-03-01'),
|
||||
('2016-10', '2016-11', '2016-12', '2017-01'),
|
||||
('045-001-000002', '045-001-000003', '045-001-000004', '045-001-000005'),
|
||||
('JRNL/2016/00001suffix', 'JRNL/2016/00002suffix', 'JRNL/2016/00003suffix', 'JRNL/2017/00001suffix'),
|
||||
]
|
||||
other_moves = self.env['account.move'].search([('journal_id', '=', self.test_move.journal_id.id)]) - self.test_move
|
||||
other_moves.unlink() # Do not interfere when trying to get the highest name for new periods
|
||||
|
||||
init_move = self.test_move
|
||||
next_move = init_move.copy()
|
||||
next_move_month = init_move.copy()
|
||||
next_move_year = init_move.copy()
|
||||
init_move.date = '2016-03-12'
|
||||
next_move.date = '2016-03-12'
|
||||
next_move_month.date = '2016-04-12'
|
||||
next_move_year.date = '2017-03-12'
|
||||
next_moves = (next_move + next_move_month + next_move_year)
|
||||
next_moves.action_post()
|
||||
|
||||
for sequence_init, sequence_next, sequence_next_month, sequence_next_year in sequences:
|
||||
init_move.name = sequence_init
|
||||
next_moves.name = False
|
||||
next_moves._compute_name()
|
||||
self.assertEqual(
|
||||
[next_move.name, next_move_month.name, next_move_year.name],
|
||||
[sequence_next, sequence_next_month, sequence_next_year],
|
||||
)
|
||||
|
||||
def test_journal_next_sequence(self):
|
||||
prefix = "TEST_ORDER/2016/"
|
||||
self.test_move.name = f"{prefix}1"
|
||||
for c in range(2, 25):
|
||||
copy = self.test_move.copy({'date': self.test_move.date})
|
||||
copy.name = "/"
|
||||
copy.action_post()
|
||||
self.assertEqual(copy.name, f"{prefix}{c}")
|
||||
|
||||
def test_journal_sequence_multiple_type(self):
|
||||
entry, entry2, invoice, invoice2, refund, refund2 = (self.test_move.copy({'date': self.test_move.date}) for i in range(6))
|
||||
(invoice + invoice2 + refund + refund2).write({
|
||||
'journal_id': self.company_data['default_journal_sale'],
|
||||
'partner_id': 1,
|
||||
'invoice_date': '2016-01-01',
|
||||
})
|
||||
(invoice + invoice2).move_type = 'out_invoice'
|
||||
(refund + refund2).move_type = 'out_refund'
|
||||
all = (entry + entry2 + invoice + invoice2 + refund + refund2)
|
||||
all.name = False
|
||||
all.action_post()
|
||||
self.assertEqual(entry.name, 'MISC/2016/01/0002')
|
||||
self.assertEqual(entry2.name, 'MISC/2016/01/0003')
|
||||
self.assertEqual(invoice.name, 'INV/2016/01/0001')
|
||||
self.assertEqual(invoice2.name, 'INV/2016/01/0002')
|
||||
self.assertEqual(refund.name, 'RINV/2016/01/0001')
|
||||
self.assertEqual(refund2.name, 'RINV/2016/01/0002')
|
||||
|
||||
def test_journal_sequence_groupby_compute(self):
|
||||
# Setup two journals with a sequence that resets yearly
|
||||
journals = self.env['account.journal'].create([{
|
||||
'name': f'Journal{i}',
|
||||
'code': f'J{i}',
|
||||
'type': 'general',
|
||||
} for i in range(2)])
|
||||
account = self.env['account.account'].search([], limit=1)
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[i].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': '2010-01-01',
|
||||
} for i in range(2)])._post()
|
||||
for i in range(2):
|
||||
moves[i].name = f'J{i}/2010/00001'
|
||||
|
||||
# Check that the moves are correctly batched
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[journal_index].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': f'2010-{month}-01',
|
||||
} for journal_index, month in [(1, 1), (0, 1), (1, 2), (1, 1)]])._post()
|
||||
self.assertEqual(
|
||||
moves.mapped('name'),
|
||||
['J1/2010/00002', 'J0/2010/00002', 'J1/2010/00004', 'J1/2010/00003'],
|
||||
)
|
||||
|
||||
def test_journal_override_sequence_regex(self):
|
||||
other_moves = self.env['account.move'].search([('journal_id', '=', self.test_move.journal_id.id)]) - self.test_move
|
||||
other_moves.unlink() # Do not interfere when trying to get the highest name for new periods
|
||||
self.test_move.date = '2020-01-01'
|
||||
self.test_move.name = '00000876-G 0002/2020'
|
||||
next = self.test_move.copy({'date': self.test_move.date})
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000876-G 0002/2021') # Wait, I didn't want this!
|
||||
|
||||
next.button_draft()
|
||||
next.name = False
|
||||
next.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000877-G 0002/2020') # Pfew, better!
|
||||
next = self.test_move.copy({'date': self.test_move.date})
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000878-G 0002/2020')
|
||||
|
||||
next = self.test_move.copy({'date': self.test_move.date})
|
||||
next.date = "2017-05-02"
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000001-G 0002/2017')
|
||||
|
||||
def test_journal_sequence_ordering(self):
|
||||
self.test_move.name = 'XMISC/2016/00001'
|
||||
copies = reduce((lambda x, y: x+y), [self.test_move.copy({'date': self.test_move.date}) for i in range(6)])
|
||||
|
||||
copies[0].date = '2019-03-05'
|
||||
copies[1].date = '2019-03-06'
|
||||
copies[2].date = '2019-03-07'
|
||||
copies[3].date = '2019-03-04'
|
||||
copies[4].date = '2019-03-05'
|
||||
copies[5].date = '2019-03-05'
|
||||
# that entry is actualy the first one of the period, so it already has a name
|
||||
# set it to '/' so that it is recomputed at post to be ordered correctly.
|
||||
copies[0].name = '/'
|
||||
copies.action_post()
|
||||
|
||||
# Ordered by date
|
||||
self.assertEqual(copies[0].name, 'XMISC/2019/00002')
|
||||
self.assertEqual(copies[1].name, 'XMISC/2019/00005')
|
||||
self.assertEqual(copies[2].name, 'XMISC/2019/00006')
|
||||
self.assertEqual(copies[3].name, 'XMISC/2019/00001')
|
||||
self.assertEqual(copies[4].name, 'XMISC/2019/00003')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/00004')
|
||||
|
||||
# Can't have twice the same name
|
||||
with self.assertRaises(ValidationError):
|
||||
copies[0].name = 'XMISC/2019/00001'
|
||||
|
||||
# Lets remove the order by date
|
||||
copies[0].name = 'XMISC/2019/10001'
|
||||
copies[1].name = 'XMISC/2019/10002'
|
||||
copies[2].name = 'XMISC/2019/10003'
|
||||
copies[3].name = 'XMISC/2019/10004'
|
||||
copies[4].name = 'XMISC/2019/10005'
|
||||
copies[5].name = 'XMISC/2019/10006'
|
||||
|
||||
copies[4].button_draft()
|
||||
copies[4].with_context(force_delete=True).unlink()
|
||||
copies[5].button_draft()
|
||||
|
||||
wizard = Form(self.env['account.resequence.wizard'].with_context(active_ids=set(copies.ids) - set(copies[4].ids), active_model='account.move'))
|
||||
|
||||
new_values = json.loads(wizard.new_values)
|
||||
self.assertEqual(new_values[str(copies[0].id)]['new_by_date'], 'XMISC/2019/10002')
|
||||
self.assertEqual(new_values[str(copies[0].id)]['new_by_name'], 'XMISC/2019/10001')
|
||||
|
||||
self.assertEqual(new_values[str(copies[1].id)]['new_by_date'], 'XMISC/2019/10004')
|
||||
self.assertEqual(new_values[str(copies[1].id)]['new_by_name'], 'XMISC/2019/10002')
|
||||
|
||||
self.assertEqual(new_values[str(copies[2].id)]['new_by_date'], 'XMISC/2019/10005')
|
||||
self.assertEqual(new_values[str(copies[2].id)]['new_by_name'], 'XMISC/2019/10003')
|
||||
|
||||
self.assertEqual(new_values[str(copies[3].id)]['new_by_date'], 'XMISC/2019/10001')
|
||||
self.assertEqual(new_values[str(copies[3].id)]['new_by_name'], 'XMISC/2019/10004')
|
||||
|
||||
self.assertEqual(new_values[str(copies[5].id)]['new_by_date'], 'XMISC/2019/10003')
|
||||
self.assertEqual(new_values[str(copies[5].id)]['new_by_name'], 'XMISC/2019/10005')
|
||||
|
||||
wizard.save().resequence()
|
||||
|
||||
self.assertEqual(copies[3].state, 'posted')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
|
||||
self.assertEqual(copies[5].state, 'draft')
|
||||
|
||||
def test_sequence_get_more_specific(self):
|
||||
def test_date(date, name):
|
||||
test = self.test_move.copy({'date': date})
|
||||
test.action_post()
|
||||
self.assertEqual(test.name, name)
|
||||
|
||||
def set_sequence(date, name):
|
||||
return self.test_move.copy({'date': date, 'name': name})._post()
|
||||
|
||||
# Start with a continuous sequence
|
||||
self.test_move.name = 'MISC/00001'
|
||||
|
||||
# Change the prefix to reset every year starting in 2017
|
||||
new_year = set_sequence(self.test_move.date + relativedelta(years=1), 'MISC/2017/00001')
|
||||
|
||||
# Change the prefix to reset every month starting in February 2017
|
||||
new_month = set_sequence(new_year.date + relativedelta(months=1), 'MISC/2017/02/00001')
|
||||
|
||||
test_date(self.test_move.date, 'MISC/00002') # Keep the old prefix in 2016
|
||||
test_date(new_year.date, 'MISC/2017/00002') # Keep the new prefix in 2017
|
||||
test_date(new_month.date, 'MISC/2017/02/00002') # Keep the new prefix in February 2017
|
||||
|
||||
# Change the prefix to never reset (again) year starting in 2018 (Please don't do that)
|
||||
reset_never = set_sequence(self.test_move.date + relativedelta(years=2), 'MISC/00100')
|
||||
test_date(reset_never.date, 'MISC/00101') # Keep the new prefix in 2018
|
||||
|
||||
def test_sequence_concurency(self):
|
||||
with self.env.registry.cursor() as cr0,\
|
||||
self.env.registry.cursor() as cr1,\
|
||||
self.env.registry.cursor() as cr2:
|
||||
env0 = api.Environment(cr0, SUPERUSER_ID, {})
|
||||
env1 = api.Environment(cr1, SUPERUSER_ID, {})
|
||||
env2 = api.Environment(cr2, SUPERUSER_ID, {})
|
||||
|
||||
journal = env0['account.journal'].create({
|
||||
'name': 'concurency_test',
|
||||
'code': 'CT',
|
||||
'type': 'general',
|
||||
})
|
||||
account = env0['account.account'].create({
|
||||
'code': 'CT',
|
||||
'name': 'CT',
|
||||
'user_type_id': env0.ref('account.data_account_type_fixed_assets').id,
|
||||
})
|
||||
moves = env0['account.move'].create([{
|
||||
'journal_id': journal.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})]
|
||||
}] * 3)
|
||||
moves.name = '/'
|
||||
moves[0].action_post()
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/'])
|
||||
env0.cr.commit()
|
||||
|
||||
# start the transactions here on cr2 to simulate concurency with cr1
|
||||
env2.cr.execute('SELECT 1')
|
||||
|
||||
move = env1['account.move'].browse(moves[1].id)
|
||||
move.action_post()
|
||||
env1.cr.commit()
|
||||
|
||||
move = env2['account.move'].browse(moves[2].id)
|
||||
with self.assertRaises(psycopg2.OperationalError), env2.cr.savepoint(), mute_logger('flectra.sql_db'):
|
||||
move.action_post()
|
||||
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', '/'])
|
||||
moves.button_draft()
|
||||
moves.posted_before = False
|
||||
moves.unlink()
|
||||
journal.unlink()
|
||||
account.unlink()
|
||||
env0.cr.commit()
|
||||
|
||||
def test_add_followers_on_post(self):
|
||||
# Add some existing partners, some from another company
|
||||
company = self.env['res.company'].create({'name': 'Oopo'})
|
||||
|
||||
@@ -830,7 +830,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
'amount_total': 1730.0,
|
||||
})
|
||||
|
||||
def test_out_invoice_line_onchange_rounding_price_subtotal(self):
|
||||
def test_out_invoice_line_onchange_rounding_price_subtotal_1(self):
|
||||
''' Seek for rounding issue on the price_subtotal when dealing with a price_unit having more digits than the
|
||||
foreign currency one.
|
||||
'''
|
||||
@@ -904,6 +904,108 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
|
||||
check_invoice_values(invoice_2)
|
||||
|
||||
def test_out_invoice_line_onchange_rounding_price_subtotal_2(self):
|
||||
""" Ensure the cyclic computations implemented using onchanges are not leading to rounding issues when using
|
||||
price-included taxes.
|
||||
For example:
|
||||
100 / 1.21 ~= 82.64 but 82.64 * 1.21 ~= 99.99 != 100.0.
|
||||
"""
|
||||
|
||||
def check_invoice_values(invoice):
|
||||
self.assertInvoiceValues(invoice, [
|
||||
{
|
||||
'price_unit': 100.0,
|
||||
'price_subtotal': 82.64,
|
||||
'debit': 0.0,
|
||||
'credit': 82.64,
|
||||
},
|
||||
{
|
||||
'price_unit': 17.36,
|
||||
'price_subtotal': 17.36,
|
||||
'debit': 0.0,
|
||||
'credit': 17.36,
|
||||
},
|
||||
{
|
||||
'price_unit': -100.0,
|
||||
'price_subtotal': -100.0,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
], {
|
||||
'amount_untaxed': 82.64,
|
||||
'amount_tax': 17.36,
|
||||
'amount_total': 100.0,
|
||||
})
|
||||
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': '21%',
|
||||
'amount': 21.0,
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
})
|
||||
|
||||
# == Test assigning tax directly ==
|
||||
|
||||
invoice_create = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': '2017-01-01',
|
||||
'date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'test line',
|
||||
'price_unit': 100.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [(6, 0, tax.ids)],
|
||||
})],
|
||||
})
|
||||
|
||||
check_invoice_values(invoice_create)
|
||||
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||
move_form.invoice_date = fields.Date.from_string('2017-01-01')
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = 'test line'
|
||||
line_form.price_unit = 100.0
|
||||
line_form.account_id = self.company_data['default_account_revenue']
|
||||
line_form.tax_ids.clear()
|
||||
line_form.tax_ids.add(tax)
|
||||
invoice_onchange = move_form.save()
|
||||
|
||||
check_invoice_values(invoice_onchange)
|
||||
|
||||
# == Test when the tax is set on a product ==
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'product',
|
||||
'lst_price': 100.0,
|
||||
'property_account_income_id': self.company_data['default_account_revenue'].id,
|
||||
'taxes_id': [(6, 0, tax.ids)],
|
||||
})
|
||||
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||
move_form.invoice_date = fields.Date.from_string('2017-01-01')
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = product
|
||||
invoice_onchange = move_form.save()
|
||||
|
||||
check_invoice_values(invoice_onchange)
|
||||
|
||||
# == Test with a fiscal position ==
|
||||
|
||||
fiscal_position = self.env['account.fiscal.position'].create({'name': 'fiscal_position'})
|
||||
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||
move_form.invoice_date = fields.Date.from_string('2017-01-01')
|
||||
move_form.partner_id = self.partner_a
|
||||
move_form.fiscal_position_id = fiscal_position
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = product
|
||||
invoice_onchange = move_form.save()
|
||||
|
||||
check_invoice_values(invoice_onchange)
|
||||
|
||||
def test_out_invoice_line_onchange_taxes_2_price_unit_tax_included(self):
|
||||
''' Seek for rounding issue in the price unit. Suppose a price_unit of 2300 with a 5.5% price-included tax
|
||||
applied on it.
|
||||
|
||||
@@ -212,8 +212,11 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
FROM account_account_tag_account_move_line_rel rel
|
||||
JOIN account_move_line line ON line.id = rel.account_move_line_id
|
||||
WHERE line.tax_exigible IS TRUE
|
||||
AND line.company_id IN %(company_ids)s
|
||||
GROUP BY rel.account_account_tag_id
|
||||
''')
|
||||
''', {
|
||||
'company_ids': tuple(self.env.companies.ids),
|
||||
})
|
||||
|
||||
for tag_id, total_balance in self.cr.fetchall():
|
||||
tag, expected_balance = expected_values[tag_id]
|
||||
|
||||
@@ -324,6 +324,206 @@ class TestAccountPayment(AccountTestInvoicingCommon):
|
||||
},
|
||||
])
|
||||
|
||||
def test_inbound_payment_sync_writeoff_debit_sign(self):
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 100.0,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
})
|
||||
|
||||
# ==== Edit the account.move.line ====
|
||||
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
payment.move_id.write({
|
||||
'line_ids': [
|
||||
(1, liquidity_lines.id, {'debit': 100.0}),
|
||||
(1, counterpart_lines.id, {'credit': 125.0}),
|
||||
(0, 0, {'debit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment, [{
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'amount': 100.0,
|
||||
}])
|
||||
|
||||
# ==== Edit the account.payment amount ====
|
||||
|
||||
payment.write({
|
||||
'partner_type': 'supplier',
|
||||
'amount': 100.1,
|
||||
'destination_account_id': self.company_data['default_account_payable'].id,
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment.line_ids.sorted('balance'), [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 125.1,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 25.0,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
},
|
||||
{
|
||||
'debit': 100.1,
|
||||
'credit': 0.0,
|
||||
'account_id': self.payment_debit_account_id.id,
|
||||
},
|
||||
])
|
||||
|
||||
def test_inbound_payment_sync_writeoff_credit_sign(self):
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 100.0,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
})
|
||||
|
||||
# ==== Edit the account.move.line ====
|
||||
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
payment.move_id.write({
|
||||
'line_ids': [
|
||||
(1, liquidity_lines.id, {'debit': 100.0}),
|
||||
(1, counterpart_lines.id, {'credit': 75.0}),
|
||||
(0, 0, {'credit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment, [{
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'amount': 100.0,
|
||||
}])
|
||||
|
||||
# ==== Edit the account.payment amount ====
|
||||
|
||||
payment.write({
|
||||
'partner_type': 'supplier',
|
||||
'amount': 100.1,
|
||||
'destination_account_id': self.company_data['default_account_payable'].id,
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment.line_ids.sorted('balance'), [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 75.1,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 25.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
},
|
||||
{
|
||||
'debit': 100.1,
|
||||
'credit': 0.0,
|
||||
'account_id': self.payment_debit_account_id.id,
|
||||
},
|
||||
])
|
||||
|
||||
def test_outbound_payment_sync_writeoff_debit_sign(self):
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 100.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
})
|
||||
|
||||
# ==== Edit the account.move.line ====
|
||||
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
payment.move_id.write({
|
||||
'line_ids': [
|
||||
(1, liquidity_lines.id, {'credit': 100.0}),
|
||||
(1, counterpart_lines.id, {'debit': 75.0}),
|
||||
(0, 0, {'debit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment, [{
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
'amount': 100.0,
|
||||
}])
|
||||
|
||||
# ==== Edit the account.payment amount ====
|
||||
|
||||
payment.write({
|
||||
'partner_type': 'customer',
|
||||
'amount': 100.1,
|
||||
'destination_account_id': self.company_data['default_account_receivable'].id,
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment.line_ids.sorted('balance'), [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 100.1,
|
||||
'account_id': self.payment_credit_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 25.0,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
},
|
||||
{
|
||||
'debit': 75.1,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
])
|
||||
|
||||
def test_outbound_payment_sync_writeoff_credit_sign(self):
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 100.0,
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
})
|
||||
|
||||
# ==== Edit the account.move.line ====
|
||||
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
payment.move_id.write({
|
||||
'line_ids': [
|
||||
(1, liquidity_lines.id, {'credit': 100.0}),
|
||||
(1, counterpart_lines.id, {'debit': 125.0}),
|
||||
(0, 0, {'credit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment, [{
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
'amount': 100.0,
|
||||
}])
|
||||
|
||||
# ==== Edit the account.payment amount ====
|
||||
|
||||
payment.write({
|
||||
'partner_type': 'customer',
|
||||
'amount': 100.1,
|
||||
'destination_account_id': self.company_data['default_account_receivable'].id,
|
||||
})
|
||||
|
||||
self.assertRecordValues(payment.line_ids.sorted('balance'), [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 100.1,
|
||||
'account_id': self.payment_credit_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 25.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
},
|
||||
{
|
||||
'debit': 125.1,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
])
|
||||
|
||||
def test_internal_transfer(self):
|
||||
copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
def test_matching_fields_match_total_amount(self):
|
||||
# Check match_total_amount: line amount >= total residual amount.
|
||||
self.rule_1.match_total_amount_param = 90.0
|
||||
self.bank_line_1.amount += 5
|
||||
self.bank_line_1.amount += 10
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': [
|
||||
@@ -361,11 +361,11 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
|
||||
})
|
||||
self.rule_1.match_total_amount_param = 100.0
|
||||
self.bank_line_1.amount -= 5
|
||||
self.bank_line_1.amount -= 10
|
||||
|
||||
# Check match_total_amount: line amount <= total residual amount.
|
||||
self.rule_1.match_total_amount_param = 90.0
|
||||
self.bank_line_1.amount -= 5
|
||||
self.bank_line_1.amount -= 10
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': [
|
||||
@@ -376,7 +376,37 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
|
||||
})
|
||||
self.rule_1.match_total_amount_param = 100.0
|
||||
self.bank_line_1.amount += 5
|
||||
self.bank_line_1.amount += 10
|
||||
|
||||
# Check match_total_amount: line amount >= total residual amount, match_total_amount_param just not matched.
|
||||
self.rule_1.match_total_amount_param = 90.0
|
||||
self.bank_line_1.amount += 10.01
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': []},
|
||||
self.bank_line_2.id: {'aml_ids': [
|
||||
self.invoice_line_1.id,
|
||||
self.invoice_line_2.id,
|
||||
self.invoice_line_3.id,
|
||||
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
|
||||
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
|
||||
})
|
||||
self.rule_1.match_total_amount_param = 100.0
|
||||
self.bank_line_1.amount -= 10.01
|
||||
|
||||
# Check match_total_amount: line amount <= total residual amount, match_total_amount_param just not matched.
|
||||
self.rule_1.match_total_amount_param = 90.0
|
||||
self.bank_line_1.amount -= 10.01
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': []},
|
||||
self.bank_line_2.id: {'aml_ids': [
|
||||
self.invoice_line_1.id,
|
||||
self.invoice_line_2.id,
|
||||
self.invoice_line_3.id,
|
||||
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
|
||||
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
|
||||
})
|
||||
self.rule_1.match_total_amount_param = 100.0
|
||||
self.bank_line_1.amount += 10.01
|
||||
|
||||
def test_matching_fields_match_partner_category_ids(self):
|
||||
test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'})
|
||||
@@ -919,3 +949,39 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': [self.invoice_line_2.id], 'model': second_inv_matching_rule, 'partner': self.bank_line_2.partner_id}
|
||||
}, statements=self.bank_st)
|
||||
|
||||
def test_payment_similar_communications(self):
|
||||
def create_payment_line(amount, memo, partner):
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': amount,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'partner_id': partner.id,
|
||||
'ref': memo,
|
||||
'destination_account_id': self.company_data['default_account_receivable'].id,
|
||||
})
|
||||
payment.action_post()
|
||||
|
||||
return payment.line_ids.filtered(lambda x: x.account_id.user_type_id.type not in {'receivable', 'payable'})
|
||||
|
||||
payment_partner = self.env['res.partner'].create({
|
||||
'name': "Bernard Gagnant",
|
||||
})
|
||||
|
||||
self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)]
|
||||
|
||||
pmt_line_1 = create_payment_line(500, 'a1b2c3', payment_partner)
|
||||
pmt_line_2 = create_payment_line(500, 'a1b2c3', payment_partner)
|
||||
pmt_line_3 = create_payment_line(500, 'd1e2f3', payment_partner)
|
||||
|
||||
self.bank_line_1.write({
|
||||
'amount': 1000,
|
||||
'payment_ref': 'a1b2c3',
|
||||
'partner_id': payment_partner.id,
|
||||
})
|
||||
self.bank_line_2.unlink()
|
||||
self.rule_1.match_total_amount = False
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': (pmt_line_1 + pmt_line_2).ids, 'model': self.rule_1, 'partner': payment_partner},
|
||||
}, statements=self.bank_line_1.statement_id)
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from flectra.tests import tagged
|
||||
from flectra.tests.common import Form
|
||||
from flectra import fields, api, SUPERUSER_ID
|
||||
from flectra.exceptions import ValidationError
|
||||
from flectra.tools import mute_logger
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from functools import reduce
|
||||
import json
|
||||
import psycopg2
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSequenceMixin(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
cls.test_move = cls.create_move()
|
||||
|
||||
@classmethod
|
||||
def create_move(cls, move_type=None, date=None, journal=None, name=None, post=False):
|
||||
move = cls.env['account.move'].create({
|
||||
'move_type': move_type or 'entry',
|
||||
'date': date or '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, None, {
|
||||
'name': 'line',
|
||||
'account_id': cls.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
if journal:
|
||||
move.name = False
|
||||
move.journal_id = journal
|
||||
if name:
|
||||
move.name = name
|
||||
if post:
|
||||
move.action_post()
|
||||
return move
|
||||
|
||||
def test_sequence_change_date(self):
|
||||
"""Change the sequence when we change the date iff it has never been posted."""
|
||||
# Check setup
|
||||
self.assertEqual(self.test_move.state, 'draft')
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.assertEqual(fields.Date.to_string(self.test_move.date), '2016-01-01')
|
||||
|
||||
# Never posetd, the number must change if we change the date
|
||||
self.test_move.date = '2020-02-02'
|
||||
self.assertEqual(self.test_move.name, 'MISC/2020/02/0001')
|
||||
|
||||
# We don't recompute user's input when posting
|
||||
self.test_move.name = 'MyMISC/2020/0000001'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
|
||||
# Has been posted, and it doesn't change anymore
|
||||
self.test_move.button_draft()
|
||||
self.test_move.date = '2020-01-02'
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
|
||||
|
||||
def test_journal_sequence(self):
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
self.test_move.action_post()
|
||||
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
|
||||
|
||||
copy1 = self.create_move(date=self.test_move.date)
|
||||
self.assertEqual(copy1.name, '/')
|
||||
copy1.action_post()
|
||||
self.assertEqual(copy1.name, 'MISC/2016/01/0002')
|
||||
|
||||
copy2 = self.create_move(date=self.test_move.date)
|
||||
new_journal = self.test_move.journal_id.copy()
|
||||
new_journal.code = "MISC2"
|
||||
copy2.journal_id = new_journal
|
||||
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
|
||||
with Form(copy2) as move_form: # It is editable in the form
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
move_form.journal_id = self.test_move.journal_id
|
||||
self.assertEqual(move_form.name, '/')
|
||||
move_form.journal_id = new_journal
|
||||
self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
copy2.action_post()
|
||||
self.assertEqual(copy2.name, 'MyMISC/2016/0001')
|
||||
|
||||
copy3 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
self.assertEqual(copy3.name, '/')
|
||||
with self.assertRaises(AssertionError):
|
||||
with Form(copy2) as move_form: # It is not editable in the form
|
||||
move_form.name = 'MyMISC/2016/0002'
|
||||
copy3.action_post()
|
||||
self.assertEqual(copy3.name, 'MyMISC/2016/0002')
|
||||
copy3.name = 'MISC2/2016/00002'
|
||||
|
||||
copy4 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
copy4.action_post()
|
||||
self.assertEqual(copy4.name, 'MISC2/2016/00003')
|
||||
|
||||
copy5 = self.create_move(date=copy2.date, journal=new_journal)
|
||||
copy5.date = '2021-02-02'
|
||||
copy5.action_post()
|
||||
self.assertEqual(copy5.name, 'MISC2/2021/00001')
|
||||
copy5.name = 'N\'importe quoi?'
|
||||
|
||||
copy6 = self.create_move(date=copy5.date, journal=new_journal)
|
||||
copy6.action_post()
|
||||
self.assertEqual(copy6.name, 'N\'importe quoi?1')
|
||||
|
||||
def test_journal_sequence_format(self):
|
||||
"""Test different format of sequences and what it becomes on another period"""
|
||||
sequences = [
|
||||
('JRNL/2016/00001', 'JRNL/2016/00002', 'JRNL/2016/00003', 'JRNL/2017/00001'),
|
||||
('1234567', '1234568', '1234569', '1234570'),
|
||||
('20190910', '20190911', '20190912', '20190913'),
|
||||
('2016-0910', '2016-0911', '2016-0912', '2017-0001'),
|
||||
('201603-10', '201603-11', '201604-01', '201703-01'),
|
||||
('16-03-10', '16-03-11', '16-04-01', '17-03-01'),
|
||||
('2016-10', '2016-11', '2016-12', '2017-01'),
|
||||
('045-001-000002', '045-001-000003', '045-001-000004', '045-001-000005'),
|
||||
('JRNL/2016/00001suffix', 'JRNL/2016/00002suffix', 'JRNL/2016/00003suffix', 'JRNL/2017/00001suffix'),
|
||||
]
|
||||
|
||||
init_move = self.create_move(date='2016-03-12')
|
||||
next_move = self.create_move(date='2016-03-12')
|
||||
next_move_month = self.create_move(date='2016-04-12')
|
||||
next_move_year = self.create_move(date='2017-03-12')
|
||||
next_moves = (next_move + next_move_month + next_move_year)
|
||||
next_moves.action_post()
|
||||
|
||||
for sequence_init, sequence_next, sequence_next_month, sequence_next_year in sequences:
|
||||
init_move.name = sequence_init
|
||||
next_moves.name = False
|
||||
next_moves._compute_name()
|
||||
self.assertEqual(
|
||||
[next_move.name, next_move_month.name, next_move_year.name],
|
||||
[sequence_next, sequence_next_month, sequence_next_year],
|
||||
)
|
||||
|
||||
def test_journal_next_sequence(self):
|
||||
"""Sequences behave correctly even when there is not enough padding."""
|
||||
prefix = "TEST_ORDER/2016/"
|
||||
self.test_move.name = f"{prefix}1"
|
||||
for c in range(2, 25):
|
||||
copy = self.create_move(date=self.test_move.date)
|
||||
copy.name = "/"
|
||||
copy.action_post()
|
||||
self.assertEqual(copy.name, f"{prefix}{c}")
|
||||
|
||||
def test_journal_sequence_multiple_type(self):
|
||||
"""Domain is computed accordingly to different types."""
|
||||
entry, entry2, invoice, invoice2, refund, refund2 = (
|
||||
self.create_move(date='2016-01-01')
|
||||
for i in range(6)
|
||||
)
|
||||
(invoice + invoice2 + refund + refund2).write({
|
||||
'journal_id': self.company_data['default_journal_sale'],
|
||||
'partner_id': 1,
|
||||
'invoice_date': '2016-01-01',
|
||||
})
|
||||
(invoice + invoice2).move_type = 'out_invoice'
|
||||
(refund + refund2).move_type = 'out_refund'
|
||||
all = (entry + entry2 + invoice + invoice2 + refund + refund2)
|
||||
all.name = False
|
||||
all.action_post()
|
||||
self.assertEqual(entry.name, 'MISC/2016/01/0002')
|
||||
self.assertEqual(entry2.name, 'MISC/2016/01/0003')
|
||||
self.assertEqual(invoice.name, 'INV/2016/01/0001')
|
||||
self.assertEqual(invoice2.name, 'INV/2016/01/0002')
|
||||
self.assertEqual(refund.name, 'RINV/2016/01/0001')
|
||||
self.assertEqual(refund2.name, 'RINV/2016/01/0002')
|
||||
|
||||
def test_journal_sequence_groupby_compute(self):
|
||||
"""The grouping optimization is correctly done."""
|
||||
# Setup two journals with a sequence that resets yearly
|
||||
journals = self.env['account.journal'].create([{
|
||||
'name': f'Journal{i}',
|
||||
'code': f'J{i}',
|
||||
'type': 'general',
|
||||
} for i in range(2)])
|
||||
account = self.env['account.account'].search([], limit=1)
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[i].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': '2010-01-01',
|
||||
} for i in range(2)])._post()
|
||||
for i in range(2):
|
||||
moves[i].name = f'J{i}/2010/00001'
|
||||
|
||||
# Check that the moves are correctly batched
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[journal_index].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': f'2010-{month}-01',
|
||||
} for journal_index, month in [(1, 1), (0, 1), (1, 2), (1, 1)]])._post()
|
||||
self.assertEqual(
|
||||
moves.mapped('name'),
|
||||
['J1/2010/00002', 'J0/2010/00002', 'J1/2010/00004', 'J1/2010/00003'],
|
||||
)
|
||||
|
||||
journals[0].code = 'OLD'
|
||||
journals.flush()
|
||||
journal_same_code = self.env['account.journal'].create([{
|
||||
'name': 'Journal0',
|
||||
'code': 'J0',
|
||||
'type': 'general',
|
||||
}])
|
||||
moves = (
|
||||
self.create_move(date='2010-01-01', journal=journal_same_code, name='J0/2010/00001')
|
||||
+ self.create_move(date='2010-01-01', journal=journal_same_code)
|
||||
+ self.create_move(date='2010-01-01', journal=journal_same_code)
|
||||
+ self.create_move(date='2010-01-01', journal=journals[0])
|
||||
)._post()
|
||||
self.assertEqual(
|
||||
moves.mapped('name'),
|
||||
['J0/2010/00001', 'J0/2010/00002', 'J0/2010/00003', 'J0/2010/00003'],
|
||||
)
|
||||
|
||||
def test_journal_override_sequence_regex(self):
|
||||
"""There is a possibility to override the regex and change the order of the paramters."""
|
||||
self.create_move(date='2020-01-01', name='00000876-G 0002/2020')
|
||||
next = self.create_move(date='2020-01-01')
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000876-G 0002/2021') # Wait, I didn't want this!
|
||||
|
||||
next.button_draft()
|
||||
next.name = False
|
||||
next.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000877-G 0002/2020') # Pfew, better!
|
||||
next = self.create_move(date='2020-01-01')
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000878-G 0002/2020')
|
||||
|
||||
next = self.create_move(date='2017-05-02')
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000001-G 0002/2017')
|
||||
|
||||
def test_journal_sequence_ordering(self):
|
||||
"""Entries are correctly sorted when posting multiple at once."""
|
||||
self.test_move.name = 'XMISC/2016/00001'
|
||||
copies = reduce((lambda x, y: x+y), [
|
||||
self.create_move(date=self.test_move.date)
|
||||
for i in range(6)
|
||||
])
|
||||
|
||||
copies[0].date = '2019-03-05'
|
||||
copies[1].date = '2019-03-06'
|
||||
copies[2].date = '2019-03-07'
|
||||
copies[3].date = '2019-03-04'
|
||||
copies[4].date = '2019-03-05'
|
||||
copies[5].date = '2019-03-05'
|
||||
# that entry is actualy the first one of the period, so it already has a name
|
||||
# set it to '/' so that it is recomputed at post to be ordered correctly.
|
||||
copies[0].name = '/'
|
||||
copies.action_post()
|
||||
|
||||
# Ordered by date
|
||||
self.assertEqual(copies[0].name, 'XMISC/2019/00002')
|
||||
self.assertEqual(copies[1].name, 'XMISC/2019/00005')
|
||||
self.assertEqual(copies[2].name, 'XMISC/2019/00006')
|
||||
self.assertEqual(copies[3].name, 'XMISC/2019/00001')
|
||||
self.assertEqual(copies[4].name, 'XMISC/2019/00003')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/00004')
|
||||
|
||||
# Can't have twice the same name
|
||||
with self.assertRaises(ValidationError):
|
||||
copies[0].name = 'XMISC/2019/00001'
|
||||
|
||||
# Lets remove the order by date
|
||||
copies[0].name = 'XMISC/2019/10001'
|
||||
copies[1].name = 'XMISC/2019/10002'
|
||||
copies[2].name = 'XMISC/2019/10003'
|
||||
copies[3].name = 'XMISC/2019/10004'
|
||||
copies[4].name = 'XMISC/2019/10005'
|
||||
copies[5].name = 'XMISC/2019/10006'
|
||||
|
||||
copies[4].button_draft()
|
||||
copies[4].with_context(force_delete=True).unlink()
|
||||
copies[5].button_draft()
|
||||
|
||||
wizard = Form(self.env['account.resequence.wizard'].with_context(
|
||||
active_ids=set(copies.ids) - set(copies[4].ids),
|
||||
active_model='account.move'),
|
||||
)
|
||||
|
||||
new_values = json.loads(wizard.new_values)
|
||||
self.assertEqual(new_values[str(copies[0].id)]['new_by_date'], 'XMISC/2019/10002')
|
||||
self.assertEqual(new_values[str(copies[0].id)]['new_by_name'], 'XMISC/2019/10001')
|
||||
|
||||
self.assertEqual(new_values[str(copies[1].id)]['new_by_date'], 'XMISC/2019/10004')
|
||||
self.assertEqual(new_values[str(copies[1].id)]['new_by_name'], 'XMISC/2019/10002')
|
||||
|
||||
self.assertEqual(new_values[str(copies[2].id)]['new_by_date'], 'XMISC/2019/10005')
|
||||
self.assertEqual(new_values[str(copies[2].id)]['new_by_name'], 'XMISC/2019/10003')
|
||||
|
||||
self.assertEqual(new_values[str(copies[3].id)]['new_by_date'], 'XMISC/2019/10001')
|
||||
self.assertEqual(new_values[str(copies[3].id)]['new_by_name'], 'XMISC/2019/10004')
|
||||
|
||||
self.assertEqual(new_values[str(copies[5].id)]['new_by_date'], 'XMISC/2019/10003')
|
||||
self.assertEqual(new_values[str(copies[5].id)]['new_by_name'], 'XMISC/2019/10005')
|
||||
|
||||
wizard.save().resequence()
|
||||
|
||||
self.assertEqual(copies[3].state, 'posted')
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
|
||||
self.assertEqual(copies[5].state, 'draft')
|
||||
|
||||
def test_sequence_get_more_specific(self):
|
||||
"""There is the ability to change the format (i.e. from yearly to montlhy)."""
|
||||
def test_date(date, name):
|
||||
test = self.create_move(date=date)
|
||||
test.action_post()
|
||||
self.assertEqual(test.name, name)
|
||||
|
||||
def set_sequence(date, name):
|
||||
return self.create_move(date=date, name=name)._post()
|
||||
|
||||
# Start with a continuous sequence
|
||||
self.test_move.name = 'MISC/00001'
|
||||
|
||||
# Change the prefix to reset every year starting in 2017
|
||||
new_year = set_sequence(self.test_move.date + relativedelta(years=1), 'MISC/2017/00001')
|
||||
|
||||
# Change the prefix to reset every month starting in February 2017
|
||||
new_month = set_sequence(new_year.date + relativedelta(months=1), 'MISC/2017/02/00001')
|
||||
|
||||
test_date(self.test_move.date, 'MISC/00002') # Keep the old prefix in 2016
|
||||
test_date(new_year.date, 'MISC/2017/00002') # Keep the new prefix in 2017
|
||||
test_date(new_month.date, 'MISC/2017/02/00002') # Keep the new prefix in February 2017
|
||||
|
||||
# Change the prefix to never reset (again) year starting in 2018 (Please don't do that)
|
||||
reset_never = set_sequence(self.test_move.date + relativedelta(years=2), 'MISC/00100')
|
||||
test_date(reset_never.date, 'MISC/00101') # Keep the new prefix in 2018
|
||||
|
||||
def test_sequence_concurency(self):
|
||||
"""Computing the same name in concurent transactions is not allowed."""
|
||||
with self.env.registry.cursor() as cr0,\
|
||||
self.env.registry.cursor() as cr1,\
|
||||
self.env.registry.cursor() as cr2:
|
||||
env0 = api.Environment(cr0, SUPERUSER_ID, {})
|
||||
env1 = api.Environment(cr1, SUPERUSER_ID, {})
|
||||
env2 = api.Environment(cr2, SUPERUSER_ID, {})
|
||||
|
||||
journal = env0['account.journal'].create({
|
||||
'name': 'concurency_test',
|
||||
'code': 'CT',
|
||||
'type': 'general',
|
||||
})
|
||||
account = env0['account.account'].create({
|
||||
'code': 'CT',
|
||||
'name': 'CT',
|
||||
'user_type_id': env0.ref('account.data_account_type_fixed_assets').id,
|
||||
})
|
||||
moves = env0['account.move'].create([{
|
||||
'journal_id': journal.id,
|
||||
'date': fields.Date.from_string('2016-01-01'),
|
||||
'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})]
|
||||
}] * 3)
|
||||
moves.name = '/'
|
||||
moves[0].action_post()
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/'])
|
||||
env0.cr.commit()
|
||||
|
||||
# start the transactions here on cr2 to simulate concurency with cr1
|
||||
env2.cr.execute('SELECT 1')
|
||||
|
||||
move = env1['account.move'].browse(moves[1].id)
|
||||
move.action_post()
|
||||
env1.cr.commit()
|
||||
|
||||
move = env2['account.move'].browse(moves[2].id)
|
||||
with self.assertRaises(psycopg2.OperationalError), env2.cr.savepoint(), mute_logger('flectra.sql_db'):
|
||||
move.action_post()
|
||||
|
||||
self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', '/'])
|
||||
moves.button_draft()
|
||||
moves.posted_before = False
|
||||
moves.unlink()
|
||||
journal.unlink()
|
||||
account.unlink()
|
||||
env0.cr.commit()
|
||||
@@ -18,7 +18,7 @@
|
||||
<group/> <!-- put Accounting group under Amount group -->
|
||||
<group name="accounting" string="Accounting">
|
||||
<field name="general_account_id" options="{'no_create': True}"/>
|
||||
<field name="move_id"/>
|
||||
<field name="move_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
</data>
|
||||
|
||||
@@ -18,19 +18,19 @@
|
||||
<field name="name"/>
|
||||
<field name="partner_id"
|
||||
domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"
|
||||
attrs="{'readonly': [('parent_state', '=', 'posted')]}"/>
|
||||
readonly="1"/>
|
||||
</group>
|
||||
<notebook colspan="4">
|
||||
<page string="Information" name="information">
|
||||
<group>
|
||||
<group string="Amount">
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="debit" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="credit" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="quantity" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" readonly="1"/>
|
||||
<field name="debit" readonly="1"/>
|
||||
<field name="credit" readonly="1"/>
|
||||
<field name="quantity" readonly="1"/>
|
||||
</group>
|
||||
<group string="Accounting Documents">
|
||||
<field name="move_id" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="move_id" readonly="1"/>
|
||||
<field name="statement_id" readonly="True" attrs="{'invisible': [('statement_id','=',False)]}"/>
|
||||
</group>
|
||||
<group string="Dates">
|
||||
@@ -41,7 +41,7 @@
|
||||
<group string="Taxes" attrs="{'invisible': [('tax_line_id','=',False), ('tax_ids','=',[])]}">
|
||||
<field name="tax_line_id" readonly="1" attrs="{'invisible': [('tax_line_id','=',False)]}"/>
|
||||
<field name="tax_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('tax_ids','=',[])]}"/>
|
||||
<field name="tax_exigible" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="tax_exigible" readonly="1"/>
|
||||
<field name="tax_audit"/>
|
||||
</group>
|
||||
<group string="Matching" attrs="{'invisible':[('matched_debit_ids', '=', []),('matched_credit_ids', '=', [])]}">
|
||||
@@ -63,7 +63,7 @@
|
||||
<field name="amount_currency"/>
|
||||
</group>
|
||||
<group string="Product" attrs="{'invisible': [('product_id', '=', False)]}">
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="States">
|
||||
<field name="blocked"/>
|
||||
@@ -71,7 +71,7 @@
|
||||
<group string="Analytic" groups="analytic.group_analytic_accounting,analytic.group_analytic_tags">
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
|
||||
attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
readonly="1"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags"
|
||||
widget="many2many_tags"/>
|
||||
</group>
|
||||
@@ -660,7 +660,7 @@
|
||||
<field name="payment_reference"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="partner_bank_id"
|
||||
context="{'default_partner_id': commercial_partner_id}"
|
||||
context="{'default_partner_id': bank_partner_id}"
|
||||
domain="[('partner_id', '=', bank_partner_id)]"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<label name="invoice_vendor_bill_id_label" for="invoice_vendor_bill_id" string="Auto-Complete" class="oe_edit_only"
|
||||
@@ -895,7 +895,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" readonly="1"/>
|
||||
<field name="tax_ids" widget="many2many_tags"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
</group>
|
||||
@@ -1060,7 +1060,7 @@
|
||||
<field name="invoice_user_id" domain="[('share', '=', False)]" widget="many2one_avatar_user"/>
|
||||
<field name="invoice_origin" string="Source Document" force_save="1" invisible="1"/>
|
||||
<field name="partner_bank_id"
|
||||
context="{'default_partner_id': commercial_partner_id}"
|
||||
context="{'default_partner_id': bank_partner_id}"
|
||||
domain="[('partner_id', '=', bank_partner_id)]"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="qr_code_method"
|
||||
|
||||
@@ -84,7 +84,7 @@ class AccountPayment(models.Model):
|
||||
def _inverse_check_number(self):
|
||||
for payment in self:
|
||||
if payment.check_number:
|
||||
sequence = payment.journal_id.check_sequence_id
|
||||
sequence = payment.journal_id.check_sequence_id.sudo()
|
||||
sequence.padding = len(payment.check_number)
|
||||
|
||||
@api.depends('payment_type', 'journal_id', 'partner_id')
|
||||
|
||||
@@ -98,6 +98,20 @@ class AccountEdiFormat(models.Model):
|
||||
# TO OVERRIDE
|
||||
return False
|
||||
|
||||
def _get_embedding_to_invoice_pdf_values(self, invoice):
|
||||
""" Get the values to embed to pdf.
|
||||
|
||||
:returns: A dictionary {'name': name, 'datas': datas} or False if there are no values to embed.
|
||||
* name: The name of the file.
|
||||
* datas: The bytes ot the file.
|
||||
"""
|
||||
self.ensure_one()
|
||||
attachment = invoice._get_edi_attachment(self)
|
||||
if not attachment or not self._is_embedding_to_invoice_pdf_needed():
|
||||
return False
|
||||
datas = base64.b64decode(attachment.with_context(bin_size=False).datas)
|
||||
return {'name': attachment.name, 'datas': datas}
|
||||
|
||||
def _support_batching(self, move=None, state=None, company=None):
|
||||
""" Indicate if we can send multiple documents in the same time to the web services.
|
||||
If True, the _post_%s_edi methods will get multiple documents in the same time.
|
||||
@@ -247,11 +261,10 @@ class AccountEdiFormat(models.Model):
|
||||
:returns: the same pdf_content with the EDI of the invoice embed in it.
|
||||
"""
|
||||
attachments = []
|
||||
for edi_format in self:
|
||||
attachment = invoice._get_edi_attachment(edi_format)
|
||||
if attachment and edi_format._is_embedding_to_invoice_pdf_needed():
|
||||
datas = base64.b64decode(attachment.with_context(bin_size=False).datas)
|
||||
attachments.append({'name': attachment.name, 'datas': datas})
|
||||
for edi_format in self.filtered(lambda edi_format: edi_format._is_embedding_to_invoice_pdf_needed()):
|
||||
attach = edi_format._get_embedding_to_invoice_pdf_values(invoice)
|
||||
if attach:
|
||||
attachments.append(attach)
|
||||
|
||||
if attachments:
|
||||
# Add the attachments to the pdf file
|
||||
|
||||
@@ -57,7 +57,7 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
|
||||
invoice.message_post(attachment_ids=[attachment.id])
|
||||
|
||||
def create_invoice_from_file(self, module_name, subfolder, filename):
|
||||
file_path = get_module_resource(module_name, 'test_file', filename)
|
||||
file_path = get_module_resource(module_name, subfolder, filename)
|
||||
file = open(file_path, 'rb').read()
|
||||
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
@@ -65,9 +65,9 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
|
||||
'datas': base64.encodebytes(file),
|
||||
'res_model': 'account.move',
|
||||
})
|
||||
|
||||
journal_id = self.company_data['default_journal_sale']
|
||||
journal_id.with_context(default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids)
|
||||
action_vals = journal_id.with_context(default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids)
|
||||
return self.env['account.move'].browse(action_vals['res_id'])
|
||||
|
||||
def assert_generated_file_equal(self, invoice, expected_values, applied_xpath=None):
|
||||
invoice.action_post()
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
<!-- Document Headers. -->
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID t-esc="record.ref"/>
|
||||
<ram:ID t-esc="record.name"/>
|
||||
<ram:TypeCode t-esc="'381' if 'refund' in record.move_type else '380'"/>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102" t-esc="format_date(record.invoice_date)"/>
|
||||
|
||||
@@ -45,6 +45,12 @@ class AccountEdiFormat(models.Model):
|
||||
self.ensure_one()
|
||||
return True if self.code == 'facturx_1_0_05' else super()._is_embedding_to_invoice_pdf_needed()
|
||||
|
||||
def _get_embedding_to_invoice_pdf_values(self, invoice):
|
||||
values = super()._get_embedding_to_invoice_pdf_values(invoice)
|
||||
if values and self.code == 'facturx_1_0_05':
|
||||
values['name'] = 'factur-x.xml'
|
||||
return values
|
||||
|
||||
def _export_facturx(self, invoice):
|
||||
|
||||
def format_date(dt):
|
||||
|
||||
@@ -66,6 +66,7 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
|
||||
</GuidelineSpecifiedDocumentContextParameter>
|
||||
</ExchangedDocumentContext>
|
||||
<ExchangedDocument>
|
||||
<ID>INV/2017/01/0001</ID>
|
||||
<TypeCode>380</TypeCode>
|
||||
<IssueDateTime>
|
||||
<DateTimeString format="102">20170101</DateTimeString>
|
||||
@@ -209,6 +210,11 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
|
||||
|
||||
self.assert_generated_file_equal(self.invoice, self.expected_invoice_facturx_values, applied_xpath)
|
||||
|
||||
def test_export_pdf(self):
|
||||
self.invoice.action_post()
|
||||
pdf_values = self.edi_format._get_embedding_to_invoice_pdf_values(self.invoice)
|
||||
self.assertEqual(pdf_values['name'], 'factur-x.xml')
|
||||
|
||||
####################################################
|
||||
# Test import
|
||||
####################################################
|
||||
|
||||
@@ -119,29 +119,6 @@ class AccountEdiFormat(models.Model):
|
||||
else:
|
||||
invoice_form.partner_id = self.env['res.partner']
|
||||
|
||||
# Regenerate PDF
|
||||
attachments = self.env['ir.attachment']
|
||||
elements = tree.xpath('//cac:AdditionalDocumentReference', namespaces=namespaces)
|
||||
for element in elements:
|
||||
attachment_name = element.xpath('cbc:ID', namespaces=namespaces)
|
||||
attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces)
|
||||
if attachment_name and attachment_data:
|
||||
text = attachment_data[0].text
|
||||
# Normalize the name of the file : some e-fff emitters put the full path of the file
|
||||
# (Windows or Linux style) and/or the name of the xml instead of the pdf.
|
||||
# Get only the filename with a pdf extension.
|
||||
name = PureWindowsPath(attachment_name[0].text).stem + '.pdf'
|
||||
attachments |= self.env['ir.attachment'].create({
|
||||
'name': name,
|
||||
'res_id': invoice.id,
|
||||
'res_model': 'account.move',
|
||||
'datas': text + '=' * (len(text) % 3), # Fix incorrect padding
|
||||
'type': 'binary',
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
if attachments:
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
|
||||
# Lines
|
||||
lines_elements = tree.xpath('//cac:InvoiceLine', namespaces=namespaces)
|
||||
for eline in lines_elements:
|
||||
@@ -196,4 +173,29 @@ class AccountEdiFormat(models.Model):
|
||||
if tax:
|
||||
invoice_line_form.tax_ids.add(tax)
|
||||
|
||||
return invoice_form.save()
|
||||
invoice = invoice_form.save()
|
||||
|
||||
# Regenerate PDF
|
||||
attachments = self.env['ir.attachment']
|
||||
elements = tree.xpath('//cac:AdditionalDocumentReference', namespaces=namespaces)
|
||||
for element in elements:
|
||||
attachment_name = element.xpath('cbc:ID', namespaces=namespaces)
|
||||
attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces)
|
||||
if attachment_name and attachment_data:
|
||||
text = attachment_data[0].text
|
||||
# Normalize the name of the file : some e-fff emitters put the full path of the file
|
||||
# (Windows or Linux style) and/or the name of the xml instead of the pdf.
|
||||
# Get only the filename with a pdf extension.
|
||||
name = PureWindowsPath(attachment_name[0].text).stem + '.pdf'
|
||||
attachments |= self.env['ir.attachment'].create({
|
||||
'name': name,
|
||||
'res_id': invoice.id,
|
||||
'res_model': 'account.move',
|
||||
'datas': text + '=' * (len(text) % 3), # Fix incorrect padding
|
||||
'type': 'binary',
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
if attachments:
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
|
||||
return invoice
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentTransaction(models.Model):
|
||||
def render_invoice_button(self, invoice, submit_txt=None, render_values=None):
|
||||
values = {
|
||||
'partner_id': invoice.partner_id.id,
|
||||
'type': self.type,
|
||||
}
|
||||
if render_values:
|
||||
values.update(render_values)
|
||||
|
||||
@@ -125,6 +125,6 @@ class IrAttachment(models.Model):
|
||||
for ftype in FTYPES:
|
||||
buf = getattr(self, '_index_%s' % ftype)(bin_data)
|
||||
if buf:
|
||||
return buf
|
||||
return buf.replace('\x00', '')
|
||||
|
||||
return super(IrAttachment, self)._index(bin_data, mimetype)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Google Authentication" for="auth_oauth_google_enabled"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/auth/google.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/auth/google.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Allow users to sign in with their Google account
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
<label for="auth_oauth_google_client_id" string="Client ID:" class="col-lg-3 o_light_label"/>
|
||||
<field name="auth_oauth_google_client_id" placeholder="e.g. 1234-xyz.apps.googleusercontent.com"/>
|
||||
</div>
|
||||
<a href="https://www.flectrahq.com/documentation/user/online/general/auth/google.html" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/auth/google.html" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<span class="alert alert-info" role="status">
|
||||
<i class="fa fa-warning"/>
|
||||
Two-factor authentication not enabled
|
||||
<a href="https://www.flectrahq.com/documentation/user/general/auth/2fa.html"
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/auth/2fa.html"
|
||||
title="What is this?" class="o_doc_link" target="_blank"></a>
|
||||
</span>
|
||||
<button name="totp_enable_wizard" type="object" string="Enable two-factor authentication"
|
||||
@@ -129,7 +129,7 @@
|
||||
<span class="text-success">
|
||||
<i class="fa fa-check-circle"/>
|
||||
Two-factor authentication enabled
|
||||
<a href="https://www.flectrahq.com/documentation/user/general/auth/2fa.html"
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/auth/2fa.html"
|
||||
title="What is this?" class="o_doc_link" target="_blank"></a>
|
||||
</span>
|
||||
<button name="totp_disable" type="object" string="(Disable two-factor authentication)"
|
||||
|
||||
@@ -93,10 +93,18 @@ class Partner(models.Model):
|
||||
if separator and field_name:
|
||||
#maxsplit set to 1 to unpack only the first element and let the rest untouched
|
||||
tmp = street_raw.split(separator, 1)
|
||||
if previous_greedy in vals:
|
||||
# attach part before space to preceding greedy field
|
||||
append_previous, sep, tmp[0] = tmp[0].rpartition(' ')
|
||||
street_raw = separator.join(tmp)
|
||||
vals[previous_greedy] += sep + append_previous
|
||||
if len(tmp) == 2:
|
||||
field_value, street_raw = tmp
|
||||
vals[field_name] = field_value
|
||||
if field_value or not field_name:
|
||||
previous_greedy = None
|
||||
if field_name == 'street_name' and separator == ' ':
|
||||
previous_greedy = field_name
|
||||
# select next field to find (first pass OR field found)
|
||||
# [2:-2] is used to remove the extra chars '%(' and ')s'
|
||||
field_name = re_match.group()[2:-2]
|
||||
|
||||
@@ -35,6 +35,7 @@ class TestStreetFields(SavepointCase):
|
||||
{'country_id': us_id, 'street': '40 Chaussee de Namur'},
|
||||
{'country_id': us_id, 'street': 'Chaussee de Namur'},
|
||||
{'country_id': mx_id, 'street': 'Av. Miguel Hidalgo y Costilla 601'},
|
||||
{'country_id': mx_id, 'street': 'Av. Miguel Hidalgo y Costilla 601/40'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur, 40 - 2b trailer'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur, 40 trailer'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur trailer'},
|
||||
@@ -43,7 +44,8 @@ class TestStreetFields(SavepointCase):
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': '2b'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': False},
|
||||
{'street_name': 'de Namur', 'street_number': 'Chaussee', 'street_number2': False},
|
||||
{'street_name': 'Av.', 'street_number': 'Miguel Hidalgo y Costilla 601', 'street_number2': False},
|
||||
{'street_name': 'Av. Miguel Hidalgo y Costilla', 'street_number': '601', 'street_number2': False},
|
||||
{'street_name': 'Av. Miguel Hidalgo y Costilla', 'street_number': '601', 'street_number2': '40'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': '2b'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': False},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': False, 'street_number2': False}
|
||||
@@ -108,6 +110,7 @@ class TestStreetFields(SavepointCase):
|
||||
{'country_id': us_id, 'street': '40 Chaussee de Namur'},
|
||||
{'country_id': us_id, 'street': 'Chaussee de Namur'},
|
||||
{'country_id': mx_id, 'street': 'Av. Miguel Hidalgo y Costilla 601'},
|
||||
{'country_id': mx_id, 'street': 'Av. Miguel Hidalgo y Costilla 601/40'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur, 40 - 2b trailer'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur, 40 trailer'},
|
||||
{'country_id': ch_id, 'street': 'header Chaussee de Namur trailer'},
|
||||
@@ -116,7 +119,8 @@ class TestStreetFields(SavepointCase):
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': '2b'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': False},
|
||||
{'street_name': 'de Namur', 'street_number': 'Chaussee', 'street_number2': False},
|
||||
{'street_name': 'Av.', 'street_number': 'Miguel Hidalgo y Costilla 601', 'street_number2': False},
|
||||
{'street_name': 'Av. Miguel Hidalgo y Costilla', 'street_number': '601', 'street_number2': False},
|
||||
{'street_name': 'Av. Miguel Hidalgo y Costilla', 'street_number': '601', 'street_number2': '40'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': '2b'},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': '40', 'street_number2': False},
|
||||
{'street_name': 'Chaussee de Namur', 'street_number': False, 'street_number2': False}
|
||||
|
||||
@@ -114,7 +114,7 @@ _map_iban_template = {
|
||||
'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil
|
||||
'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus
|
||||
'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland
|
||||
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
|
||||
'cr': 'CRkk BBBC CCCC CCCC CCCC CC', # Costa Rica
|
||||
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus
|
||||
'cz': 'CZkk BBBB SSSS SSCC CCCC CCCC', # Czech Republic
|
||||
'de': 'DEkk BBBB BBBB CCCC CCCC CC', # Germany
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<i class="fa fa-download"/> <span><t t-esc="template.label"/></span>
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/base_import/import_faq.html" target="new">Import FAQ</a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/base_import/import_faq.html" target="new">Import FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class='o_form_label' attrs="{'invisible':[('active_user_count', '<=', '1')]}">
|
||||
Active Users
|
||||
</span>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/flectra_basics/add_user.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/flectra_basics/users.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<br/>
|
||||
<button name="%(base.action_res_users)d" icon="fa-arrow-right" type="action" string="Manage Users" class="btn-link o_web_settings_access_rights"/>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<div class="o_setting_right_pane" id="sms_settings">
|
||||
<div class="o_form_label">
|
||||
Send SMS
|
||||
<a href="https://doc.flectrahq.com/2.0/sms_marketing/pricing/pricing_and_faq.html" title="Documentation" class="ml-1 o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/marketing/sms_marketing/pricing/pricing_and_faq.html" title="Documentation" class="ml-1 o_doc_link" target="_blank"></a>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Send texts to your contacts
|
||||
@@ -192,7 +192,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Import & Export" for="module_base_import"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/base_import/import_faq.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/base_import/import_faq.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Allow users to import data from CSV/XLS/XLSX/ODS files
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Google Calendar" for="module_google_calendar"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/crm/optimize/google_calendar_credentials.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/calendars/google/google_calendar_credentials.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Synchronize your calendar with Google Calendar
|
||||
</div>
|
||||
@@ -324,7 +324,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane" name="auth_ldap_right_pane">
|
||||
<label string="LDAP Authentication" for="module_auth_ldap"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/auth/ldap.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/auth/ldap.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Use LDAP credentials to log in
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane" id="web_unsplash_settings">
|
||||
<label for="module_web_unsplash"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/general/unsplash/unsplash_access_key.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/unsplash/unsplash_access_key.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Find free high-resolution images from Unsplash
|
||||
</div>
|
||||
@@ -386,12 +386,12 @@
|
||||
<div class="o_setting_right_pane">
|
||||
<!-- FIXME Those links are defined directly in the template which means that we will have to
|
||||
update the template code is the link ever changes -->
|
||||
<a class="d-block mx-auto" href="https://play.google.com/store/apps/details?id=com.flectra.flectrahq" target="blank">
|
||||
<a class="d-block mx-auto" href="https://play.google.com/store/apps/details?id=com.flectra.mobile" target="blank">
|
||||
<img alt="On Google Play" class="d-block mx-auto img img-fluid" src="/base_setup/static/src/img/google_play.png"/>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class='d-block mx-auto' href="https://itunes.apple.com/us/app/flectra/id1561830563" target="blank">
|
||||
<a class='d-block mx-auto' href="https://itunes.apple.com/us/app/flectra/id1272543640" target="blank">
|
||||
<img alt="On Apple Store" class="d-block mx-auto img img-fluid" src="/base_setup/static/src/img/app_store.png"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -327,9 +327,7 @@ var CrossTabBus = Longpolling.extend({
|
||||
}
|
||||
// update channels
|
||||
else if (key === this._generateKey('channels')) {
|
||||
var channels = value;
|
||||
_.each(_.difference(this._channels, channels), this.deleteChannel.bind(this));
|
||||
_.each(_.difference(channels, this._channels), this.addChannel.bind(this));
|
||||
this._channels = value;
|
||||
}
|
||||
// update options
|
||||
else if (key === this._generateKey('options')) {
|
||||
|
||||
@@ -2,6 +2,7 @@ flectra.define('web.bus_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BusService = require('bus.BusService');
|
||||
var CrossTabBus = require('bus.CrossTab');
|
||||
var AbstractStorageService = require('web.AbstractStorageService');
|
||||
var RamStorage = require('web.RamStorage');
|
||||
var testUtils = require('web.test_utils');
|
||||
@@ -310,5 +311,81 @@ QUnit.module('Bus', {
|
||||
parentMaster.destroy();
|
||||
parentSlave.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('two tabs calling addChannel simultaneously', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
let id = 1;
|
||||
testUtils.patch(CrossTabBus, {
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.__tabId__ = id++;
|
||||
},
|
||||
addChannel: function (channel) {
|
||||
assert.step('Tab ' + this.__tabId__ + ': addChannel ' + channel);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
deleteChannel: function (channel) {
|
||||
assert.step('Tab ' + this.__tabId__ + ': deleteChannel ' + channel);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
let pollPromise;
|
||||
const parentTab1 = new Widget();
|
||||
await testUtils.addMockEnvironment(parentTab1, {
|
||||
data: {},
|
||||
services: {
|
||||
local_storage: LocalStorageServiceMock,
|
||||
},
|
||||
mockRPC: function (route) {
|
||||
if (route === '/longpolling/poll') {
|
||||
pollPromise = testUtils.makeTestPromise();
|
||||
pollPromise.abort = (function () {
|
||||
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
|
||||
}).bind(pollPromise);
|
||||
return pollPromise;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
const parentTab2 = new Widget();
|
||||
await testUtils.addMockEnvironment(parentTab2, {
|
||||
data: {},
|
||||
services: {
|
||||
local_storage: LocalStorageServiceMock,
|
||||
},
|
||||
mockRPC: function (route) {
|
||||
if (route === '/longpolling/poll') {
|
||||
pollPromise = testUtils.makeTestPromise();
|
||||
pollPromise.abort = (function () {
|
||||
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
|
||||
}).bind(pollPromise);
|
||||
return pollPromise;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
const tab1 = new CrossTabBus(parentTab1);
|
||||
const tab2 = new CrossTabBus(parentTab2);
|
||||
|
||||
tab1.addChannel("alpha");
|
||||
tab2.addChannel("alpha");
|
||||
tab1.addChannel("beta");
|
||||
tab2.addChannel("beta");
|
||||
|
||||
assert.verifySteps([
|
||||
"Tab 1: addChannel alpha",
|
||||
"Tab 2: addChannel alpha",
|
||||
"Tab 1: addChannel beta",
|
||||
"Tab 2: addChannel beta",
|
||||
]);
|
||||
|
||||
testUtils.unpatch(CrossTabBus);
|
||||
parentTab1.destroy();
|
||||
parentTab2.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -125,11 +125,18 @@ class Attendee(models.Model):
|
||||
'mimetype': 'text/calendar',
|
||||
'datas': base64.b64encode(ics_file)})
|
||||
]
|
||||
body = invitation_template.with_context(rendering_context)._render_field(
|
||||
'body_html',
|
||||
attendee.ids,
|
||||
compute_lang=True,
|
||||
post_process=True)[attendee.id]
|
||||
try:
|
||||
body = invitation_template.with_context(rendering_context)._render_field(
|
||||
'body_html',
|
||||
attendee.ids,
|
||||
compute_lang=True,
|
||||
post_process=True)[attendee.id]
|
||||
except UserError: #TO BE REMOVED IN MASTER
|
||||
body = invitation_template.sudo().with_context(rendering_context)._render_field(
|
||||
'body_html',
|
||||
attendee.ids,
|
||||
compute_lang=True,
|
||||
post_process=True)[attendee.id]
|
||||
subject = invitation_template._render_field(
|
||||
'subject',
|
||||
attendee.ids,
|
||||
|
||||
@@ -493,7 +493,8 @@ class Lead(models.Model):
|
||||
# searching on +32485112233 should also finds 00485112233 (00 / + prefix are both valid)
|
||||
# we therefore remove it from input value and search for both of them in db
|
||||
if value.startswith('+') or value.startswith('00'):
|
||||
value = value.replace('+', '').replace('00', '', 1)
|
||||
if value.startswith('00'):
|
||||
value = value[2:]
|
||||
starts_with = '00|\+'
|
||||
else:
|
||||
starts_with = '%'
|
||||
@@ -1401,21 +1402,21 @@ class Lead(models.Model):
|
||||
""" Handle salesman recipients that can convert leads into opportunities
|
||||
and set opportunities as won / lost. """
|
||||
groups = super(Lead, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
msg_vals = msg_vals or {}
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
if self.type == 'lead':
|
||||
convert_action = self._notify_get_action_link('controller', controller='/lead/convert', **msg_vals)
|
||||
convert_action = self._notify_get_action_link('controller', controller='/lead/convert', **local_msg_vals)
|
||||
salesman_actions = [{'url': convert_action, 'title': _('Convert to opportunity')}]
|
||||
else:
|
||||
won_action = self._notify_get_action_link('controller', controller='/lead/case_mark_won', **msg_vals)
|
||||
lost_action = self._notify_get_action_link('controller', controller='/lead/case_mark_lost', **msg_vals)
|
||||
won_action = self._notify_get_action_link('controller', controller='/lead/case_mark_won', **local_msg_vals)
|
||||
lost_action = self._notify_get_action_link('controller', controller='/lead/case_mark_lost', **local_msg_vals)
|
||||
salesman_actions = [
|
||||
{'url': won_action, 'title': _('Won')},
|
||||
{'url': lost_action, 'title': _('Lost')}]
|
||||
|
||||
if self.team_id:
|
||||
custom_params = dict(msg_vals, res_id=self.team_id.id, model=self.team_id._name)
|
||||
custom_params = dict(local_msg_vals, res_id=self.team_id.id, model=self.team_id._name)
|
||||
salesman_actions.append({
|
||||
'url': self._notify_get_action_link('view', **custom_params),
|
||||
'title': _('Sales Team Settings')
|
||||
@@ -1515,6 +1516,10 @@ class Lead(models.Model):
|
||||
break
|
||||
return result
|
||||
|
||||
def _phone_get_number_fields(self):
|
||||
""" Use mobile or phone fields to compute sanitized phone number """
|
||||
return ['mobile', 'phone']
|
||||
|
||||
@api.model
|
||||
def get_import_templates(self):
|
||||
return [{
|
||||
|
||||
@@ -129,12 +129,12 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
'philip.j.fry@test.example.com',
|
||||
'turanga.leela@test.example.com',
|
||||
]
|
||||
cls.test_pĥone_data = [
|
||||
cls.test_phone_data = [
|
||||
'+1 202 555 0122', # formatted US number
|
||||
'202 555 0999', # local US number
|
||||
'202 555 0888', # local US number
|
||||
]
|
||||
cls.test_pĥone_data_sanitized = [
|
||||
cls.test_phone_data_sanitized = [
|
||||
'+12025550122',
|
||||
'+12025550999',
|
||||
'+12025550888',
|
||||
@@ -153,7 +153,7 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
cls.contact_1 = cls.env['res.partner'].create({
|
||||
'name': 'Philip J Fry',
|
||||
'email': cls.test_email_data[1],
|
||||
'mobile': cls.test_pĥone_data[0],
|
||||
'mobile': cls.test_phone_data[0],
|
||||
'title': cls.env.ref('base.res_partner_title_mister').id,
|
||||
'function': 'Delivery Boy',
|
||||
'phone': False,
|
||||
@@ -167,8 +167,8 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
cls.contact_2 = cls.env['res.partner'].create({
|
||||
'name': 'Turanga Leela',
|
||||
'email': cls.test_email_data[2],
|
||||
'mobile': cls.test_pĥone_data[1],
|
||||
'phone': cls.test_pĥone_data[2],
|
||||
'mobile': cls.test_phone_data[1],
|
||||
'phone': cls.test_phone_data[2],
|
||||
'parent_id': False,
|
||||
'is_company': False,
|
||||
'street': 'Cookieville Minimum-Security Orphanarium',
|
||||
|
||||
@@ -217,16 +217,16 @@ class TestCRMLead(TestCrmCommon):
|
||||
lead_form = Form(lead)
|
||||
|
||||
# reset partner phone to a local number and prepare formatted / sanitized values
|
||||
partner_phone, partner_mobile = self.test_pĥone_data[2], self.test_pĥone_data[1]
|
||||
partner_phone, partner_mobile = self.test_phone_data[2], self.test_phone_data[1]
|
||||
partner_phone_formatted = phone_format(partner_phone, 'US', '1')
|
||||
partner_phone_sanitized = phone_format(partner_phone, 'US', '1', force_format='E164')
|
||||
partner_mobile_formatted = phone_format(partner_mobile, 'US', '1')
|
||||
partner_mobile_sanitized = phone_format(partner_mobile, 'US', '1', force_format='E164')
|
||||
partner_email, partner_email_normalized = self.test_email_data[2], self.test_email_data_normalized[2]
|
||||
self.assertEqual(partner_phone_formatted, '+1 202-555-0888')
|
||||
self.assertEqual(partner_phone_sanitized, self.test_pĥone_data_sanitized[2])
|
||||
self.assertEqual(partner_phone_sanitized, self.test_phone_data_sanitized[2])
|
||||
self.assertEqual(partner_mobile_formatted, '+1 202-555-0999')
|
||||
self.assertEqual(partner_mobile_sanitized, self.test_pĥone_data_sanitized[1])
|
||||
self.assertEqual(partner_mobile_sanitized, self.test_phone_data_sanitized[1])
|
||||
# ensure initial data
|
||||
self.assertEqual(partner.phone, partner_phone)
|
||||
self.assertEqual(partner.mobile, partner_mobile)
|
||||
@@ -386,3 +386,46 @@ class TestCRMLead(TestCrmCommon):
|
||||
new_lead.handle_partner_assignment(create_missing=True)
|
||||
self.assertEqual(new_lead.partner_id.email, 'unknown.sender@test.example.com')
|
||||
self.assertEqual(new_lead.partner_id.team_id, self.sales_team_1)
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_phone_mobile_update(self):
|
||||
lead = self.env['crm.lead'].create({
|
||||
'name': 'Lead 1',
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
'phone': self.test_phone_data[0],
|
||||
})
|
||||
self.assertEqual(lead.phone, self.test_phone_data[0])
|
||||
self.assertFalse(lead.mobile)
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[0])
|
||||
|
||||
lead.write({'phone': False, 'mobile': self.test_phone_data[1]})
|
||||
self.assertFalse(lead.phone)
|
||||
self.assertEqual(lead.mobile, self.test_phone_data[1])
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
|
||||
|
||||
lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
|
||||
self.assertEqual(lead.phone, self.test_phone_data[1])
|
||||
self.assertEqual(lead.mobile, self.test_phone_data[2])
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[2])
|
||||
|
||||
# updating country should trigger sanitize computation
|
||||
lead.write({'country_id': self.env.ref('base.be').id})
|
||||
self.assertEqual(lead.phone, self.test_phone_data[1])
|
||||
self.assertEqual(lead.mobile, self.test_phone_data[2])
|
||||
self.assertFalse(lead.phone_sanitized)
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_phone_mobile_search(self):
|
||||
lead_1 = self.env['crm.lead'].create({
|
||||
'name': 'Lead 1',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'phone': '+32485001122',
|
||||
})
|
||||
_lead_2 = self.env['crm.lead'].create({
|
||||
'name': 'Lead 2',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'phone': '+32485112233',
|
||||
})
|
||||
self.assertEqual(lead_1, self.env['crm.lead'].search([
|
||||
('phone_mobile_search', 'like', '+32485001122')
|
||||
]))
|
||||
|
||||
@@ -5,6 +5,55 @@ from flectra import SUPERUSER_ID
|
||||
from flectra.addons.crm.tests import common as crm_common
|
||||
from flectra.fields import Datetime
|
||||
from flectra.tests.common import tagged, users
|
||||
from flectra.tests.common import Form
|
||||
|
||||
@tagged('lead_manage')
|
||||
class TestLeadConvertForm(crm_common.TestLeadConvertCommon):
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_form_action_default(self):
|
||||
""" Test Lead._find_matching_partner() """
|
||||
lead = self.env['crm.lead'].browse(self.lead_1.ids)
|
||||
customer = self.env['res.partner'].create({
|
||||
"name": "Amy Wong",
|
||||
"email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
|
||||
})
|
||||
|
||||
wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
|
||||
'active_model': 'crm.lead',
|
||||
'active_id': lead.id,
|
||||
'active_ids': lead.ids,
|
||||
}))
|
||||
|
||||
self.assertEqual(wizard.name, 'convert')
|
||||
self.assertEqual(wizard.action, 'exist')
|
||||
self.assertEqual(wizard.partner_id, customer)
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_form_name_onchange(self):
|
||||
""" Test Lead._find_matching_partner() """
|
||||
lead = self.env['crm.lead'].browse(self.lead_1.ids)
|
||||
lead_dup = lead.copy({'name': 'Duplicate'})
|
||||
customer = self.env['res.partner'].create({
|
||||
"name": "Amy Wong",
|
||||
"email": '"Amy, PhD Student, Wong" Tiny <AMY.WONG@test.example.com>'
|
||||
})
|
||||
|
||||
wizard = Form(self.env['crm.lead2opportunity.partner'].with_context({
|
||||
'active_model': 'crm.lead',
|
||||
'active_id': lead.id,
|
||||
'active_ids': lead.ids,
|
||||
}))
|
||||
|
||||
self.assertEqual(wizard.name, 'merge')
|
||||
self.assertEqual(wizard.action, 'exist')
|
||||
self.assertEqual(wizard.partner_id, customer)
|
||||
self.assertEqual(wizard.duplicated_lead_ids[:], lead + lead_dup)
|
||||
|
||||
wizard.name = 'convert'
|
||||
wizard.action = 'create'
|
||||
self.assertEqual(wizard.action, 'create', 'Should keep user input')
|
||||
self.assertEqual(wizard.name, 'convert', 'Should keep user input')
|
||||
|
||||
|
||||
@tagged('lead_manage')
|
||||
|
||||
@@ -5,7 +5,7 @@ from flectra.addons.crm.tests import common as crm_common
|
||||
from flectra.tests.common import tagged, users
|
||||
|
||||
|
||||
@tagged('lead_manage')
|
||||
@tagged('lead_manage', 'crm_performance')
|
||||
class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
|
||||
@classmethod
|
||||
@@ -24,7 +24,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
with self.assertQueryCount(user_sales_manager=0):
|
||||
test_leads = self.env['crm.lead'].browse(test_leads.ids)
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=254): # crm only: 251
|
||||
with self.assertQueryCount(user_sales_manager=254): # often 251, sometimes +3 on runbot
|
||||
test_leads.handle_salesmen_assignment(user_ids=user_ids, team_id=False)
|
||||
|
||||
self.assertEqual(test_leads.team_id, self.sales_team_convert | self.sales_team_1)
|
||||
@@ -42,7 +42,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
with self.assertQueryCount(user_sales_manager=0):
|
||||
test_leads = self.env['crm.lead'].browse(test_leads.ids)
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=220): # crm only: 215
|
||||
with self.assertQueryCount(user_sales_manager=221): # crm only: 215 - generally 218, sometimes +2/+3 on runbot
|
||||
test_leads.handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
|
||||
|
||||
self.assertEqual(test_leads.team_id, self.sales_team_convert)
|
||||
@@ -166,7 +166,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
test_leads = self._create_leads_batch(count=50, user_ids=[False])
|
||||
user_ids = self.assign_users.ids
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=1363): # crm only: 1352
|
||||
with self.assertQueryCount(user_sales_manager=1361): # still some randomness (1360 spotted) - crm only: 1352
|
||||
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
|
||||
'active_model': 'crm.lead',
|
||||
'active_ids': test_leads.ids,
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
<div class="o_setting_right_pane" id="crm_iap_lead_settings">
|
||||
<label string="Lead Mining" for="module_crm_iap_lead"/>
|
||||
<a href="https://doc.flectrahq.com/2.0/crm/acquire_leads/lead_mining.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/sales/crm/acquire_leads/lead_mining.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Generate new leads based on their country, industry, size, etc.
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
<div class="o_setting_right_pane" id="mail_client_extension">
|
||||
<label string="Outlook CRM Extension" for="module_mail_client_extension"/>
|
||||
<a href="https://www.flectrahq.com/documentation/user/crm/optimize/outlook_extension.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/sales/crm/optimize/outlook_extension.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Turn emails received in your Outlook mailbox into leads and log their content as internal notes.
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,8 @@ class Lead2OpportunityPartner(models.TransientModel):
|
||||
@api.depends('duplicated_lead_ids')
|
||||
def _compute_name(self):
|
||||
for convert in self:
|
||||
convert.name = 'merge' if convert.duplicated_lead_ids and len(convert.duplicated_lead_ids) >= 2 else 'convert'
|
||||
if not convert.name:
|
||||
convert.name = 'merge' if convert.duplicated_lead_ids and len(convert.duplicated_lead_ids) >= 2 else 'convert'
|
||||
|
||||
@api.depends('lead_id')
|
||||
def _compute_action(self):
|
||||
@@ -77,7 +78,7 @@ class Lead2OpportunityPartner(models.TransientModel):
|
||||
convert.lead_id.partner_id.email if convert.lead_id.partner_id.email else convert.lead_id.email_from,
|
||||
include_lost=True).ids
|
||||
|
||||
@api.depends('action')
|
||||
@api.depends('action', 'lead_id')
|
||||
def _compute_partner_id(self):
|
||||
for convert in self:
|
||||
if convert.action == 'exist':
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<field name="team_id" widget="selection"/>
|
||||
</group>
|
||||
<group string="Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
|
||||
<field name="lead_id" invisible="1"/>
|
||||
<field name="duplicated_lead_ids" nolabel="1">
|
||||
<tree>
|
||||
<field name="create_date" widget="date"/>
|
||||
|
||||
@@ -39,7 +39,7 @@ class MailChannel(models.Model):
|
||||
# anonymous user whatever the participants. Otherwise keep only share
|
||||
# partners (no user or portal user) to link to the lead.
|
||||
customers = self.env['res.partner']
|
||||
for customer in channel_partners.partner_id.filtered('partner_share'):
|
||||
for customer in channel_partners.partner_id.filtered('partner_share').with_context(active_test=False):
|
||||
if customer.user_ids and all(user._is_public() for user in customer.user_ids):
|
||||
customers = self.env['res.partner']
|
||||
break
|
||||
|
||||
@@ -44,6 +44,23 @@ class TestLivechatLead(TestCrmCommon):
|
||||
self.assertEqual(lead.name, 'TestLead command')
|
||||
self.assertEqual(lead.partner_id, self.env['res.partner'])
|
||||
|
||||
# public user: should not be set as customer
|
||||
# 'base.public_user' is archived by default
|
||||
self.assertFalse(self.env.ref('base.public_user').active)
|
||||
|
||||
channel = self.env['mail.channel'].create({
|
||||
'name': 'Chat with Visitor',
|
||||
'channel_partner_ids': [(4, self.env.ref('base.public_partner').id)]
|
||||
})
|
||||
lead = channel._convert_visitor_to_lead(self.env.user.partner_id, channel.channel_last_seen_partner_ids, '/lead TestLead command')
|
||||
|
||||
self.assertEqual(
|
||||
channel.channel_last_seen_partner_ids.partner_id,
|
||||
self.user_sales_leads.partner_id | self.env.ref('base.public_partner')
|
||||
)
|
||||
self.assertEqual(lead.name, 'TestLead command')
|
||||
self.assertEqual(lead.partner_id, self.env['res.partner'])
|
||||
|
||||
# public + someone else: no customer (as he was anonymous)
|
||||
channel.write({
|
||||
'channel_partner_ids': [(4, self.user_sales_manager.partner_id.id)]
|
||||
|
||||
@@ -10,4 +10,5 @@ class CrmLead(models.Model):
|
||||
def _sms_get_number_fields(self):
|
||||
""" This method returns the fields to use to find the number to use to
|
||||
send an SMS on a record. """
|
||||
# TDE FIXME: to be cleaned in 14.4+ as it conflicts with _phone_get_number_fields
|
||||
return ['mobile', 'phone']
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_crm_lead
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.crm.tests.common import TestCrmCommon
|
||||
from flectra.tests.common import Form, users
|
||||
|
||||
|
||||
class TestCRMLead(TestCrmCommon):
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_phone_mobile_update(self):
|
||||
lead = self.env['crm.lead'].create({
|
||||
'name': 'Lead 1',
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
'phone': self.test_phone_data[0],
|
||||
})
|
||||
self.assertEqual(lead.phone, self.test_phone_data[0])
|
||||
self.assertFalse(lead.mobile)
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[0])
|
||||
|
||||
lead.write({'phone': False, 'mobile': self.test_phone_data[1]})
|
||||
self.assertFalse(lead.phone)
|
||||
self.assertEqual(lead.mobile, self.test_phone_data[1])
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[1])
|
||||
|
||||
lead.write({'phone': self.test_phone_data[1], 'mobile': self.test_phone_data[2]})
|
||||
self.assertEqual(lead.phone, self.test_phone_data[1])
|
||||
self.assertEqual(lead.mobile, self.test_phone_data[2])
|
||||
self.assertEqual(lead.phone_sanitized, self.test_phone_data_sanitized[2])
|
||||
@@ -195,14 +195,13 @@ class StockPicking(models.Model):
|
||||
delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id)
|
||||
carrier_price = self.carrier_price * (1.0 + (float(self.carrier_id.margin) / 100.0))
|
||||
if not delivery_lines:
|
||||
sale_order._create_delivery_line(self.carrier_id, carrier_price)
|
||||
else:
|
||||
delivery_line = delivery_lines[0]
|
||||
delivery_line[0].write({
|
||||
'price_unit': carrier_price,
|
||||
# remove the estimated price from the description
|
||||
'name': sale_order.carrier_id.with_context(lang=self.partner_id.lang).name,
|
||||
})
|
||||
delivery_lines = [sale_order._create_delivery_line(self.carrier_id, carrier_price)]
|
||||
delivery_line = delivery_lines[0]
|
||||
delivery_line[0].write({
|
||||
'price_unit': carrier_price,
|
||||
# remove the estimated price from the description
|
||||
'name': sale_order.carrier_id.with_context(lang=self.partner_id.lang).name,
|
||||
})
|
||||
|
||||
def open_website_url(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Carrier">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_prod_environment"
|
||||
attrs="{'invisible': ['|', '|', ('prod_environment', '=', False), ('delivery_type', '=', 'fixed'), ('delivery_type', '=', 'base_on_rule')]}"
|
||||
@@ -87,6 +86,7 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" text="Archived" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title" name="title">
|
||||
<label for="name" string="Name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -57,20 +57,14 @@ tour.register('event_tour', {
|
||||
}, {
|
||||
trigger: '.o_event_form_view div[name="event_ticket_ids"] .o_field_x2many_list_row_add a',
|
||||
content: _t("Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one."),
|
||||
}, {
|
||||
trigger: '.o_form_button_save',
|
||||
extra_trigger: '.o_event_form_view',
|
||||
content: _t("Awesome! Now, let's <b>save</b> your changes."),
|
||||
position: 'bottom',
|
||||
width: 250,
|
||||
}, ...new EventAdditionalTourSteps()._get_website_event_steps(), {
|
||||
trigger: '.o_event_form_view div[name="stage_id"] button:contains("Booked")',
|
||||
trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
extra_trigger: 'div.o_form_buttons_view:not(.o_hidden)',
|
||||
content: _t("Now that your event is ready, click here to move it to another stage."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'ol.breadcrumb li.breadcrumb-item:first',
|
||||
extra_trigger: '.o_event_form_view div[name="stage_id"] button.disabled:contains("Booked")',
|
||||
extra_trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
content: _t("Use the <b>breadcrumbs</b> to go back to your kanban overview."),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
|
||||
@@ -180,7 +180,7 @@ class EventLeadRule(models.Model):
|
||||
'description': "%s\n%s" % (lead.description, additionnal_description),
|
||||
'registration_ids': [(4, reg.id) for reg in group_registrations],
|
||||
})
|
||||
else:
|
||||
elif group_registrations:
|
||||
lead_vals_list.append(group_registrations._get_lead_values(rule))
|
||||
|
||||
return self.env['crm.lead'].create(lead_vals_list)
|
||||
|
||||
@@ -145,3 +145,23 @@ class TestEventCrmFlow(TestEventCrmCommon):
|
||||
})
|
||||
self.assertEqual(len(self.event_0.registration_ids), 4)
|
||||
self.assertLeadConvertion(self.test_rule_attendee, registration, partner=None)
|
||||
|
||||
@users('user_eventmanager')
|
||||
def test_order_rule_duplicate_lead(self):
|
||||
""" Check when two rules match one event
|
||||
but only one match the registration,
|
||||
only one lead should be created
|
||||
"""
|
||||
test_rule_order_2 = self.test_rule_order.copy(default={
|
||||
'event_registration_filter': [['email', 'not ilike', '@test.example.com']]
|
||||
})
|
||||
self.env['event.registration'].create({
|
||||
'name': 'My Registration',
|
||||
'partner_id': False,
|
||||
'email': 'super.email@test.example.com',
|
||||
'phone': False,
|
||||
'mobile': '0456332211',
|
||||
'event_id': self.event_0.id,
|
||||
})
|
||||
self.assertEqual(len(self.test_rule_order.lead_ids), 1)
|
||||
self.assertEqual(len(test_rule_order_2.lead_ids), 0)
|
||||
|
||||
@@ -137,11 +137,6 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
def _get_display_price(self, product):
|
||||
if self.event_ticket_id and self.event_id:
|
||||
company = self.event_id.company_id or self.env.company
|
||||
currency = company.currency_id
|
||||
return currency._convert(
|
||||
self.event_ticket_id.price, self.order_id.currency_id,
|
||||
self.order_id.company_id or self.env.company.id,
|
||||
self.order_id.date_order or fields.Date.today())
|
||||
return self.event_ticket_id.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price_reduce
|
||||
else:
|
||||
return super()._get_display_price(product)
|
||||
|
||||
@@ -50,6 +50,7 @@ class TestEventSale(TestEventSaleCommon):
|
||||
'event_ticket_id': ticket1.id,
|
||||
'product_id': ticket1.product_id.id,
|
||||
'product_uom_qty': TICKET1_COUNT,
|
||||
'price_unit': 10,
|
||||
}), (0, 0, {
|
||||
'event_id': self.event_0.id,
|
||||
'event_ticket_id': ticket2.id,
|
||||
@@ -153,3 +154,58 @@ class TestEventSale(TestEventSaleCommon):
|
||||
|
||||
self.assertEqual(editor_action['type'], 'ir.actions.act_window')
|
||||
self.assertEqual(editor_action['res_model'], 'registration.editor')
|
||||
|
||||
def test_ticket_price_with_pricelist_and_tax(self):
|
||||
self.env.user.partner_id.country_id = False
|
||||
pricelist = self.env['product.pricelist'].search([], limit=1)
|
||||
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': "Tax 10",
|
||||
'amount': 10,
|
||||
})
|
||||
|
||||
event_product = self.env['product.template'].create({
|
||||
'name': 'Event Product',
|
||||
'list_price': 10.0,
|
||||
})
|
||||
|
||||
event_product.taxes_id = tax
|
||||
|
||||
event = self.env['event.event'].create({
|
||||
'name': 'New Event',
|
||||
'date_begin': '2020-02-02',
|
||||
'date_end': '2020-04-04',
|
||||
})
|
||||
event_ticket = self.env['event.event.ticket'].create({
|
||||
'name': 'VIP',
|
||||
'price': 1000.0,
|
||||
'event_id': event.id,
|
||||
'product_id': event_product.product_variant_id.id,
|
||||
})
|
||||
|
||||
pricelist.item_ids = self.env['product.pricelist.item'].create({
|
||||
'applied_on': "1_product",
|
||||
'base': "list_price",
|
||||
'compute_price': "fixed",
|
||||
'fixed_price': 6.0,
|
||||
'product_tmpl_id': event_product.id,
|
||||
})
|
||||
|
||||
pricelist.discount_policy = 'without_discount'
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
})
|
||||
sol = self.env['sale.order.line'].create({
|
||||
'name': event.name,
|
||||
'product_id': event_product.product_variant_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': event_product.uom_id.id,
|
||||
'price_unit': event_product.list_price,
|
||||
'order_id': so.id,
|
||||
'event_id': event.id,
|
||||
'event_ticket_id': event_ticket.id,
|
||||
})
|
||||
sol.product_id_change()
|
||||
self.assertEqual(so.amount_total, 660.0, "Ticket is $1000 but the event product is on a pricelist 10 -> 6. So, $600 + a 10% tax.")
|
||||
|
||||
@@ -59,7 +59,7 @@ class FleetVehicleLogContract(models.Model):
|
||||
def _compute_contract_name(self):
|
||||
for record in self:
|
||||
name = record.vehicle_id.name
|
||||
if record.cost_subtype_id.name:
|
||||
if name and record.cost_subtype_id.name:
|
||||
name = record.cost_subtype_id.name + ' ' + name
|
||||
record.name = name
|
||||
|
||||
|
||||
@@ -146,14 +146,25 @@ class GamificationBadge(models.Model):
|
||||
self.update(defaults)
|
||||
return
|
||||
|
||||
self.env.cr.execute("""
|
||||
SELECT badge_id, count(user_id) as granted_count,
|
||||
count(distinct(user_id)) as granted_users_count,
|
||||
array_agg(distinct(user_id)) as unique_owner_ids
|
||||
FROM gamification_badge_user
|
||||
WHERE badge_id in %s
|
||||
GROUP BY badge_id
|
||||
""", [tuple(self.ids)])
|
||||
Users = self.env["res.users"]
|
||||
query = Users._where_calc([])
|
||||
Users._apply_ir_rules(query)
|
||||
badge_alias = query.join("res_users", "id", "gamification_badge_user", "user_id", "badges")
|
||||
|
||||
tables, where_clauses, where_params = query.get_sql()
|
||||
|
||||
self.env.cr.execute(
|
||||
f"""
|
||||
SELECT {badge_alias}.badge_id, count(res_users.id) as stat_count,
|
||||
count(distinct(res_users.id)) as stat_count_distinct,
|
||||
array_agg(distinct(res_users.id)) as unique_owner_ids
|
||||
FROM {tables}
|
||||
WHERE {where_clauses}
|
||||
AND {badge_alias}.badge_id IN %s
|
||||
GROUP BY {badge_alias}.badge_id
|
||||
""",
|
||||
[*where_params, tuple(self.ids)]
|
||||
)
|
||||
|
||||
mapping = {
|
||||
badge_id: {
|
||||
|
||||
@@ -245,19 +245,13 @@ class Challenge(models.Model):
|
||||
# exclude goals for users that did not connect since the last update
|
||||
yesterday = fields.Date.to_string(date.today() - timedelta(days=1))
|
||||
self.env.cr.execute("""SELECT gg.id
|
||||
FROM gamification_goal as gg,
|
||||
gamification_challenge as gc,
|
||||
res_users as ru,
|
||||
res_users_log as log
|
||||
WHERE gg.challenge_id = gc.id
|
||||
AND gg.user_id = ru.id
|
||||
AND ru.id = log.create_uid
|
||||
AND gg.write_date < log.create_date
|
||||
FROM gamification_goal as gg
|
||||
JOIN res_users_log as log ON gg.user_id = log.create_uid
|
||||
WHERE gg.write_date < log.create_date
|
||||
AND gg.closed IS NOT TRUE
|
||||
AND gc.id IN %s
|
||||
AND gg.challenge_id IN %s
|
||||
AND (gg.state = 'inprogress'
|
||||
OR (gg.state = 'reached'
|
||||
AND (gg.end_date >= %s OR gg.end_date IS NULL)))
|
||||
OR (gg.state = 'reached' AND gg.end_date >= %s))
|
||||
GROUP BY gg.id
|
||||
""", [tuple(self.ids), yesterday])
|
||||
|
||||
@@ -360,7 +354,7 @@ class Challenge(models.Model):
|
||||
participant_user_ids = set(challenge.user_ids.ids)
|
||||
user_squating_challenge_ids = user_with_goal_ids - participant_user_ids
|
||||
if user_squating_challenge_ids:
|
||||
# users that used to match the challenge
|
||||
# users that used to match the challenge
|
||||
Goals.search([
|
||||
('challenge_id', '=', challenge.id),
|
||||
('user_id', 'in', list(user_squating_challenge_ids))
|
||||
@@ -451,7 +445,7 @@ class Challenge(models.Model):
|
||||
'action': <{True,False}>,
|
||||
'display_mode': <{progress,boolean}>,
|
||||
'target': <challenge line target>,
|
||||
'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
|
||||
'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
|
||||
'completeness': <percentage>,
|
||||
'current': <current value>,
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class GoogleCalendarService():
|
||||
|
||||
@requires_auth_token
|
||||
def insert(self, values, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events"
|
||||
url = "/calendar/v3/calendars/primary/events?sendUpdates=all"
|
||||
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
|
||||
if not values.get('id'):
|
||||
values['id'] = uuid4().hex
|
||||
@@ -67,13 +67,13 @@ class GoogleCalendarService():
|
||||
|
||||
@requires_auth_token
|
||||
def patch(self, event_id, values, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events/%s" % event_id
|
||||
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)
|
||||
|
||||
@requires_auth_token
|
||||
def delete(self, event_id, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events/%s" % event_id
|
||||
url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
|
||||
headers = {'Content-type': 'application/json'}
|
||||
params = {'access_token': token}
|
||||
try:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<label for="cal_client_secret" string="Client Secret" class="col-3 col-lg-3 o_light_label"/>
|
||||
<field name="cal_client_secret" password="True" nolabel="1"/>
|
||||
</div>
|
||||
<a href="https://doc.flectrahq.com/2.0/crm/optimize/google_calendar_credentials.html" class="oe-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/calendars/google/google_calendar_credentials.html" class="oe-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -39,7 +39,7 @@ class HrAttendance(models.Model):
|
||||
@api.depends('check_in', 'check_out')
|
||||
def _compute_worked_hours(self):
|
||||
for attendance in self:
|
||||
if attendance.check_out:
|
||||
if attendance.check_out and attendance.check_in:
|
||||
delta = attendance.check_out - attendance.check_in
|
||||
attendance.worked_hours = delta.total_seconds() / 3600.0
|
||||
else:
|
||||
|
||||
@@ -30,7 +30,7 @@ class Contract(models.Model):
|
||||
help="End date of the trial period (if there is one).")
|
||||
resource_calendar_id = fields.Many2one(
|
||||
'resource.calendar', 'Working Schedule', compute='_compute_employee_contract', store=True, readonly=False,
|
||||
default=lambda self: self.env.company.resource_calendar_id.id, copy=False,
|
||||
default=lambda self: self.env.company.resource_calendar_id.id, copy=False, index=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
wage = fields.Monetary('Wage', required=True, tracking=True, help="Employee's monthly gross wage.")
|
||||
notes = fields.Text('Notes')
|
||||
|
||||
@@ -510,37 +510,11 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
move_line_values_by_expense = self._get_account_move_line_values()
|
||||
|
||||
for expense in self:
|
||||
company_currency = expense.company_id.currency_id
|
||||
different_currency = expense.currency_id != company_currency
|
||||
|
||||
# get the account move of the related sheet
|
||||
move = move_group_by_sheet[expense.sheet_id.id]
|
||||
|
||||
# get move line values
|
||||
move_line_values = move_line_values_by_expense.get(expense.id)
|
||||
move_line_dst = move_line_values[-1]
|
||||
total_amount = move_line_dst['debit'] or -move_line_dst['credit']
|
||||
total_amount_currency = move_line_dst['amount_currency']
|
||||
|
||||
# create one more move line, a counterline for the total on payable account
|
||||
if expense.payment_mode == 'company_account':
|
||||
if not expense.sheet_id.bank_journal_id.default_account_id:
|
||||
raise UserError(_("No account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
|
||||
journal = expense.sheet_id.bank_journal_id
|
||||
# create payment
|
||||
payment_methods = journal.outbound_payment_method_ids if total_amount < 0 else journal.inbound_payment_method_ids
|
||||
journal_currency = journal.currency_id or journal.company_id.currency_id
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_method_id': payment_methods and payment_methods[0].id or False,
|
||||
'payment_type': 'outbound' if total_amount < 0 else 'inbound',
|
||||
'partner_id': expense.employee_id.sudo().address_home_id.commercial_partner_id.id,
|
||||
'partner_type': 'supplier',
|
||||
'journal_id': journal.id,
|
||||
'date': expense.date,
|
||||
'currency_id': expense.currency_id.id if different_currency else journal_currency.id,
|
||||
'amount': abs(total_amount_currency) if different_currency else abs(total_amount),
|
||||
'ref': expense.name,
|
||||
})
|
||||
|
||||
# link move lines to move, and move to expense sheet
|
||||
move.write({'line_ids': [(0, 0, line) for line in move_line_values]})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -594,7 +594,10 @@ class HolidaysRequest(models.Model):
|
||||
""" Returns a float equals to the timedelta between two dates given as string."""
|
||||
if employee_id:
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
return employee._get_work_days_data_batch(date_from, date_to)[employee.id]
|
||||
result = employee._get_work_days_data_batch(date_from, date_to)[employee.id]
|
||||
if self.request_unit_half:
|
||||
result['days'] = 0.5
|
||||
return result
|
||||
|
||||
today_hours = self.env.company.resource_calendar_id.get_work_hours_count(
|
||||
datetime.combine(date_from.date(), time.min),
|
||||
@@ -602,8 +605,8 @@ class HolidaysRequest(models.Model):
|
||||
False)
|
||||
|
||||
hours = self.env.company.resource_calendar_id.get_work_hours_count(date_from, date_to)
|
||||
|
||||
return {'days': hours / (today_hours or HOURS_PER_DAY), 'hours': hours}
|
||||
days = hours / (today_hours or HOURS_PER_DAY) if not self.request_unit_half else 0.5
|
||||
return {'days': days, 'hours': hours}
|
||||
|
||||
def _adjust_date_based_on_tz(self, leave_date, hour):
|
||||
""" request_date_{from,to} are local to the user's tz but hour_{from,to} are in UTC.
|
||||
@@ -1220,15 +1223,15 @@ class HolidaysRequest(models.Model):
|
||||
""" Handle HR users and officers recipients that can validate or refuse holidays
|
||||
directly from email. """
|
||||
groups = super(HolidaysRequest, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
msg_vals = msg_vals or {}
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
hr_actions = []
|
||||
if self.state == 'confirm':
|
||||
app_action = self._notify_get_action_link('controller', controller='/leave/validate', **msg_vals)
|
||||
app_action = self._notify_get_action_link('controller', controller='/leave/validate', **local_msg_vals)
|
||||
hr_actions += [{'url': app_action, 'title': _('Approve')}]
|
||||
if self.state in ['confirm', 'validate', 'validate1']:
|
||||
ref_action = self._notify_get_action_link('controller', controller='/leave/refuse', **msg_vals)
|
||||
ref_action = self._notify_get_action_link('controller', controller='/leave/refuse', **local_msg_vals)
|
||||
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
|
||||
|
||||
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
|
||||
|
||||
@@ -670,15 +670,15 @@ class HolidaysAllocation(models.Model):
|
||||
""" Handle HR users and officers recipients that can validate or refuse holidays
|
||||
directly from email. """
|
||||
groups = super(HolidaysAllocation, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
msg_vals = msg_vals or {}
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
hr_actions = []
|
||||
if self.state == 'confirm':
|
||||
app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **msg_vals)
|
||||
app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **local_msg_vals)
|
||||
hr_actions += [{'url': app_action, 'title': _('Approve')}]
|
||||
if self.state in ['confirm', 'validate', 'validate1']:
|
||||
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **msg_vals)
|
||||
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
|
||||
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
|
||||
|
||||
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
|
||||
|
||||
@@ -33,8 +33,8 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
|
||||
|
||||
def test_single_attendance_on_morning_and_afternoon(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
@@ -140,13 +140,13 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
# Ask for morning
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '8 Hours')
|
||||
|
||||
# Ask for afternoon
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '8 Hours')
|
||||
|
||||
def test_attendance_next_day(self):
|
||||
@@ -173,8 +173,8 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
|
||||
|
||||
@@ -202,8 +202,8 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0))
|
||||
|
||||
@@ -241,7 +241,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '2 Hours')
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 8, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
|
||||
@@ -254,7 +254,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 9, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 9, 10, 0, 0))
|
||||
@@ -285,7 +285,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
|
||||
self.assertEqual(leave_form.number_of_days_display, 0.5)
|
||||
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
|
||||
|
||||
@@ -313,7 +313,7 @@ class TestCompanyLeave(SavepointCase):
|
||||
})
|
||||
company_leave._compute_date_from_to()
|
||||
|
||||
count = 865
|
||||
count = 732
|
||||
with self.assertQueryCount(__system__=count, admin=count):
|
||||
# Original query count: 1987
|
||||
# Without tracking/activity context keys: 5154
|
||||
|
||||
@@ -16,7 +16,7 @@ class MaintenanceEquipment(models.Model):
|
||||
required=True,
|
||||
default='employee')
|
||||
owner_user_id = fields.Many2one(compute='_compute_owner', store=True)
|
||||
assign_date = fields.Date(compute='_compute_equipement_assign', store=True, readonly=False, copy=True)
|
||||
assign_date = fields.Date(compute='_compute_equipment_assign', store=True, readonly=False, copy=True)
|
||||
|
||||
@api.depends('employee_id', 'department_id', 'equipment_assign_to')
|
||||
def _compute_owner(self):
|
||||
|
||||
@@ -515,7 +515,8 @@ class Applicant(models.Model):
|
||||
], order='sequence asc', limit=1).id
|
||||
for applicant in self:
|
||||
applicant.write(
|
||||
{'stage_id': default_stage[applicant.job_id.id], 'refuse_reason_id': False})
|
||||
{'stage_id': applicant.job_id.id and default_stage[applicant.job_id.id],
|
||||
'refuse_reason_id': False})
|
||||
|
||||
def toggle_active(self):
|
||||
res = super(Applicant, self).toggle_active()
|
||||
|
||||
@@ -152,7 +152,7 @@ class AccountAnalyticLine(models.Model):
|
||||
if vals.get('project_id') and not vals.get('account_id'):
|
||||
project = self.env['project.project'].browse(vals.get('project_id'))
|
||||
vals['account_id'] = project.analytic_account_id.id
|
||||
vals['company_id'] = project.analytic_account_id.company_id.id
|
||||
vals['company_id'] = project.analytic_account_id.company_id.id or project.company_id.id
|
||||
if not project.analytic_account_id.active:
|
||||
raise UserError(_('The project you are timesheeting on is not linked to an active analytic account. Set one on the project configuration.'))
|
||||
# employee implies user
|
||||
|
||||
@@ -19,7 +19,7 @@ class HrWorkEntry(models.Model):
|
||||
date_start = fields.Datetime(required=True, string='From')
|
||||
date_stop = fields.Datetime(compute='_compute_date_stop', store=True, readonly=False, string='To')
|
||||
duration = fields.Float(compute='_compute_duration', store=True, string="Period")
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type')
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', index=True)
|
||||
color = fields.Integer(related='work_entry_type_id.color', readonly=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
|
||||
@@ -12,8 +12,6 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
class InterfaceMetaClass(type):
|
||||
def __new__(cls, clsname, bases, attrs):
|
||||
if clsname in interfaces:
|
||||
return interfaces[clsname]
|
||||
new_interface = super(InterfaceMetaClass, cls).__new__(cls, clsname, bases, attrs)
|
||||
interfaces[clsname] = new_interface
|
||||
return new_interface
|
||||
@@ -38,6 +36,15 @@ class Interface(Thread, metaclass=InterfaceMetaClass):
|
||||
def update_iot_devices(self, devices={}):
|
||||
added = devices.keys() - self._detected_devices
|
||||
removed = self._detected_devices - devices.keys()
|
||||
# keys() returns a dict_keys, and the values of that stay in sync with the
|
||||
# original dictionary if it changes. This means that get_devices needs to return
|
||||
# a newly created dictionary every time. If it doesn't do that and reuses the
|
||||
# same dictionary, this logic won't detect any changes that are made. Could be
|
||||
# avoided by converting the dict_keys into a regular dict. The current logic
|
||||
# also can't detect if a device is replaced by a different one with the same
|
||||
# key. Also, _detected_devices starts out as a class variable but gets turned
|
||||
# into an instance variable here. It would be better if it was an instance
|
||||
# variable from the start to avoid confusion.
|
||||
self._detected_devices = devices.keys()
|
||||
|
||||
for identifier in removed:
|
||||
@@ -51,8 +58,12 @@ class Interface(Thread, metaclass=InterfaceMetaClass):
|
||||
_logger.info('Device %s is now connected', identifier)
|
||||
d = driver(identifier, devices[identifier])
|
||||
d.daemon = True
|
||||
d.start()
|
||||
iot_devices[identifier] = d
|
||||
# Start the thread after creating the iot_devices entry so the
|
||||
# thread can assume the iot_devices entry will exist while it's
|
||||
# running, at least until the `disconnect` above gets triggered
|
||||
# when `removed` is not empty.
|
||||
d.start()
|
||||
break
|
||||
|
||||
def get_devices(self):
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a href='https://www.flectrahq.com/help'>Help</a>
|
||||
<a href='https://doc.flectrahq.com/2.0/iot.html'>Documentation</a>
|
||||
<a href='https://doc.flectrahq.com/2.0/applications/productivity/iot.html'>Documentation</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
However the preferred method to upgrade the IoTBox is to flash the sd-card with
|
||||
the <a href='https://nightly.flectrahq.com/master/iotbox/iotbox-latest.zip'>latest image</a>. The upgrade
|
||||
procedure is explained into to the
|
||||
<a href='https://doc.flectrahq.com/2.0/iot.html'>IoTBox manual</a>
|
||||
<a href='https://doc.flectrahq.com/2.0/applications/productivity/iot.html'>IoTBox manual</a>
|
||||
</p>
|
||||
<p>
|
||||
To upgrade the IoTBox, click on the upgrade button. The upgrade will take a few minutes. <b>Do not reboot</b> the IoTBox during the upgrade.
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div t-name="iap.buy_more_credits" class="mt-2 row">
|
||||
<div class="col-sm">
|
||||
<button class="btn btn-link buy_credits"><i class="fa fa-arrow-right"/> Buy credits</button>
|
||||
<button class="btn btn-link buy_credits o-hidden-ios"><i class="fa fa-arrow-right"/> Buy credits</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,14 +6,34 @@ import logging
|
||||
import json
|
||||
import requests
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
from flectra import exceptions, _
|
||||
from flectra.tests.common import BaseCase
|
||||
from flectra.tools import pycompat
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap.flectrahq.com'
|
||||
|
||||
|
||||
# We need to mock iap_jsonrpc during tests as we don't want to perform real calls to RPC endpoints
|
||||
def iap_jsonrpc_mocked(*args, **kwargs):
|
||||
raise exceptions.AccessError("Unavailable during tests.")
|
||||
|
||||
|
||||
iap_patch = patch('flectra.addons.iap.tools.iap_tools.iap_jsonrpc', iap_jsonrpc_mocked)
|
||||
|
||||
|
||||
def setUp(self):
|
||||
old_setup_func(self)
|
||||
iap_patch.start()
|
||||
self.addCleanup(iap_patch.stop)
|
||||
|
||||
|
||||
old_setup_func = BaseCase.setUp
|
||||
BaseCase.setUp = setUp
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Helpers for both clients and proxy
|
||||
#----------------------------------------------------------
|
||||
|
||||
@@ -24,8 +24,8 @@ if records:
|
||||
<div class='o_setting_right_pane'>
|
||||
<div class="o_form_label">
|
||||
Flectra IAP
|
||||
<a href="https://doc.flectrahq.com/2.0/general/in_app_purchase/in_app_purchase.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://www.flectrahq.com/documentation/14.0/webservices/iap.html" title="Documentation" class="ml-1 o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/applications/general/in_app_purchase/in_app_purchase.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<a href="https://doc.flectrahq.com/2.0/developer/webservices/iap.html" title="Documentation" class="ml-1 o_doc_link" target="_blank"></a>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
View your IAP Services and recharge your credits
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<flectra>
|
||||
<data>
|
||||
<data noupdate="1">
|
||||
<record id="im_livechat_channel_data" model="im_livechat.channel">
|
||||
<field name="name">YourWebsite.com</field>
|
||||
<field name="default_message">Hello, how may I help you?</field>
|
||||
|
||||
@@ -4,7 +4,7 @@ import base64
|
||||
import random
|
||||
import re
|
||||
|
||||
from flectra import api, fields, models, modules
|
||||
from flectra import api, fields, models, modules, _
|
||||
|
||||
|
||||
class ImLivechatChannel(models.Model):
|
||||
@@ -225,6 +225,8 @@ class ImLivechatChannel(models.Model):
|
||||
def get_livechat_info(self, username='Visitor'):
|
||||
self.ensure_one()
|
||||
|
||||
if username == 'Visitor':
|
||||
username = _('Visitor')
|
||||
info = {}
|
||||
info['available'] = len(self._get_available_users()) > 0
|
||||
info['server_url'] = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Argentina - Accounting',
|
||||
'version': "3.2",
|
||||
'version': "3.3",
|
||||
'description': """
|
||||
Functional
|
||||
----------
|
||||
|
||||
@@ -7,19 +7,79 @@ l10n_ar.base_deudores_por_ventas,l10n_ar.l10nar_base_chart_template,1.1.3.01.010
|
||||
l10n_ar.base_deudores_por_ventas_pos,l10n_ar.l10nar_base_chart_template,1.1.3.01.020,account.data_account_type_receivable,Deudores por ventas (PoS),True
|
||||
l10n_ar.base_ret_percepcion_tasa_municipal,l10n_ar.l10nar_base_chart_template,1.1.4.01.010,account.data_account_type_current_assets,Ret/Percepción Tasa Municipal,False
|
||||
l10n_ar.base_saldo_a_favor_tasa_municipal,l10n_ar.l10nar_base_chart_template,1.1.4.01.020,account.data_account_type_current_assets,Saldo a favor Tasa Municipal,False
|
||||
l10n_ar.base_saldo_favor_iibb_sf,l10n_ar.l10nar_base_chart_template,1.1.4.02.010,account.data_account_type_current_assets,Saldo a favor IIBB p. Santa Fé,False
|
||||
l10n_ar.base_retencion_iibb_sf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.020,account.data_account_type_current_assets,Retención IIBB p. Santa Fé sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_sf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.030,account.data_account_type_current_assets,Percepción IIBB p. Santa Fé sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_co,l10n_ar.l10nar_base_chart_template,1.1.4.02.040,account.data_account_type_current_assets,Saldo a favor IIBB p. Córdoba,False
|
||||
l10n_ar.base_retencion_iibb_co_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.050,account.data_account_type_current_assets,Retención IIBB p. Córdoba sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_co_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.060,account.data_account_type_current_assets,Percepción IIBB p. Córdoba sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ba,l10n_ar.l10nar_base_chart_template,1.1.4.02.070,account.data_account_type_current_assets,Saldo a favor IIBB p. Buenos Aires,False
|
||||
l10n_ar.base_retencion_iibb_ba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.080,account.data_account_type_current_assets,Retención IIBB p. Buenos Aires sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.090,account.data_account_type_current_assets,Percepción IIBB p. Buenos Aires sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_caba,l10n_ar.l10nar_base_chart_template,1.1.4.02.100,account.data_account_type_current_assets,Saldo a favor IIBB p. CABA,False
|
||||
l10n_ar.base_retencion_iibb_caba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.110,account.data_account_type_current_assets,Retención IIBB CABA sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_caba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.120,account.data_account_type_current_assets,Percepción IIBB CABA sufrida,False
|
||||
l10n_ar.base_sircreb,l10n_ar.l10nar_base_chart_template,1.1.4.02.130,account.data_account_type_current_assets,SIRCREB,False
|
||||
l10n_ar.base_saldo_favor_iibb_caba,l10n_ar.l10nar_base_chart_template,1.1.4.02.010,account.data_account_type_current_assets,Saldo a favor IIBB CABA,False
|
||||
l10n_ar.base_retencion_iibb_caba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.020,account.data_account_type_current_assets,Retención IIBB CABA sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_caba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.030,account.data_account_type_current_assets,Percepción IIBB CABA sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ba,l10n_ar.l10nar_base_chart_template,1.1.4.02.040,account.data_account_type_current_assets,Saldo a favor IIBB Buenos Aires,False
|
||||
l10n_ar.base_retencion_iibb_ba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.050,account.data_account_type_current_assets,Retención IIBB Buenos Aires sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ba_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.060,account.data_account_type_current_assets,Percepción IIBB Buenos Aires sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ca,l10n_ar.l10nar_base_chart_template,1.1.4.02.070,account.data_account_type_current_assets,Saldo a favor IIBB Catamarca,False
|
||||
l10n_ar.base_retencion_iibb_ca_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.080,account.data_account_type_current_assets,Retención IIBB Catamarca sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ca_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.090,account.data_account_type_current_assets,Percepción IIBB Catamarca sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_co,l10n_ar.l10nar_base_chart_template,1.1.4.02.100,account.data_account_type_current_assets,Saldo a favor IIBB Córdoba,False
|
||||
l10n_ar.base_retencion_iibb_co_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.110,account.data_account_type_current_assets,Retención IIBB Córdoba sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_co_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.120,account.data_account_type_current_assets,Percepción IIBB Córdoba sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_rr,l10n_ar.l10nar_base_chart_template,1.1.4.02.130,account.data_account_type_current_assets,Saldo a favor IIBB Corrientes,False
|
||||
l10n_ar.base_retencion_iibb_rr_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.140,account.data_account_type_current_assets,Retención IIBB Corrientes sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_rr_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.150,account.data_account_type_current_assets,Percepción IIBB Corrientes sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_er,l10n_ar.l10nar_base_chart_template,1.1.4.02.160,account.data_account_type_current_assets,Saldo a favor IIBB Entre Ríos,False
|
||||
l10n_ar.base_retencion_iibb_er_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.170,account.data_account_type_current_assets,Retención IIBB Entre Ríos sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_er_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.180,account.data_account_type_current_assets,Percepción IIBB Entre Ríos sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ju,l10n_ar.l10nar_base_chart_template,1.1.4.02.190,account.data_account_type_current_assets,Saldo a favor IIBB Jujuy,False
|
||||
l10n_ar.base_retencion_iibb_ju_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.200,account.data_account_type_current_assets,Retención IIBB Jujuy sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ju_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.210,account.data_account_type_current_assets,Percepción IIBB Jujuy sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_za,l10n_ar.l10nar_base_chart_template,1.1.4.02.220,account.data_account_type_current_assets,Saldo a favor IIBB Mendoza,False
|
||||
l10n_ar.base_retencion_iibb_za_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.230,account.data_account_type_current_assets,Retención IIBB Mendoza sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_za_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.240,account.data_account_type_current_assets,Percepción IIBB Mendoza sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_lr,l10n_ar.l10nar_base_chart_template,1.1.4.02.250,account.data_account_type_current_assets,Saldo a favor IIBB La Rioja,False
|
||||
l10n_ar.base_retencion_iibb_lr_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.260,account.data_account_type_current_assets,Retención IIBB La Rioja sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_lr_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.270,account.data_account_type_current_assets,Percepción IIBB La Rioja sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_sa,l10n_ar.l10nar_base_chart_template,1.1.4.02.280,account.data_account_type_current_assets,Saldo a favor IIBB Salta,False
|
||||
l10n_ar.base_retencion_iibb_sa_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.290,account.data_account_type_current_assets,Retención IIBB Salta sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_sa_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.300,account.data_account_type_current_assets,Percepción IIBB Salta sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_nn,l10n_ar.l10nar_base_chart_template,1.1.4.02.310,account.data_account_type_current_assets,Saldo a favor IIBB San Juan,False
|
||||
l10n_ar.base_retencion_iibb_nn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.320,account.data_account_type_current_assets,Retención IIBB San Juan sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_nn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.330,account.data_account_type_current_assets,Percepción IIBB San Juan sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_sl,l10n_ar.l10nar_base_chart_template,1.1.4.02.340,account.data_account_type_current_assets,Saldo a favor IIBB San Luis,False
|
||||
l10n_ar.base_retencion_iibb_sl_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.350,account.data_account_type_current_assets,Retención IIBB San Luis sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_sl_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.360,account.data_account_type_current_assets,Percepción IIBB San Luis sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_sf,l10n_ar.l10nar_base_chart_template,1.1.4.02.370,account.data_account_type_current_assets,Saldo a favor IIBB Santa Fe,False
|
||||
l10n_ar.base_retencion_iibb_sf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.380,account.data_account_type_current_assets,Retención IIBB Santa Fe sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_sf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.390,account.data_account_type_current_assets,Percepción IIBB Santa Fe sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_se,l10n_ar.l10nar_base_chart_template,1.1.4.02.400,account.data_account_type_current_assets,Saldo a favor IIBB Santiago del Estero,False
|
||||
l10n_ar.base_retencion_iibb_se_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.410,account.data_account_type_current_assets,Retención IIBB Santiago del Estero sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_se_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.420,account.data_account_type_current_assets,Percepción IIBB Santiago del Estero sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_tn,l10n_ar.l10nar_base_chart_template,1.1.4.02.430,account.data_account_type_current_assets,Saldo a favor IIBB Tucumán,False
|
||||
l10n_ar.base_retencion_iibb_tn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.440,account.data_account_type_current_assets,Retención IIBB Tucumán sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_tn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.450,account.data_account_type_current_assets,Percepción IIBB Tucumán sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ha,l10n_ar.l10nar_base_chart_template,1.1.4.02.460,account.data_account_type_current_assets,Saldo a favor IIBB Chaco,False
|
||||
l10n_ar.base_retencion_iibb_ha_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.470,account.data_account_type_current_assets,Retención IIBB Chaco sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ha_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.480,account.data_account_type_current_assets,Percepción IIBB Chaco sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ct,l10n_ar.l10nar_base_chart_template,1.1.4.02.490,account.data_account_type_current_assets,Saldo a favor IIBB Chubut,False
|
||||
l10n_ar.base_retencion_iibb_ct_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.500,account.data_account_type_current_assets,Retención IIBB Chubut sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ct_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.510,account.data_account_type_current_assets,Percepción IIBB Chubut sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_fo,l10n_ar.l10nar_base_chart_template,1.1.4.02.520,account.data_account_type_current_assets,Saldo a favor IIBB Formosa,False
|
||||
l10n_ar.base_retencion_iibb_fo_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.530,account.data_account_type_current_assets,Retención IIBB Formosa sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_fo_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.540,account.data_account_type_current_assets,Percepción IIBB Formosa sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_mi,l10n_ar.l10nar_base_chart_template,1.1.4.02.550,account.data_account_type_current_assets,Saldo a favor IIBB Misiones,False
|
||||
l10n_ar.base_retencion_iibb_mi_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.560,account.data_account_type_current_assets,Retención IIBB Misiones sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_mi_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.570,account.data_account_type_current_assets,Percepción IIBB Misiones sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_ne,l10n_ar.l10nar_base_chart_template,1.1.4.02.580,account.data_account_type_current_assets,Saldo a favor IIBB Neuquén,False
|
||||
l10n_ar.base_retencion_iibb_ne_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.590,account.data_account_type_current_assets,Retención IIBB Neuquén sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_ne_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.600,account.data_account_type_current_assets,Percepción IIBB Neuquén sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_lp,l10n_ar.l10nar_base_chart_template,1.1.4.02.610,account.data_account_type_current_assets,Saldo a favor IIBB La Pampa,False
|
||||
l10n_ar.base_retencion_iibb_lp_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.620,account.data_account_type_current_assets,Retención IIBB La Pampa sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_lp_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.630,account.data_account_type_current_assets,Percepción IIBB La Pampa sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_rn,l10n_ar.l10nar_base_chart_template,1.1.4.02.640,account.data_account_type_current_assets,Saldo a favor IIBB Río Negro,False
|
||||
l10n_ar.base_retencion_iibb_rn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.650,account.data_account_type_current_assets,Retención IIBB Río Negro sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_rn_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.660,account.data_account_type_current_assets,Percepción IIBB Río Negro sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_az,l10n_ar.l10nar_base_chart_template,1.1.4.02.670,account.data_account_type_current_assets,Saldo a favor IIBB Santa Cruz,False
|
||||
l10n_ar.base_retencion_iibb_az_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.680,account.data_account_type_current_assets,Retención IIBB Santa Cruz sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_az_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.690,account.data_account_type_current_assets,Percepción IIBB Santa Cruz sufrida,False
|
||||
l10n_ar.base_saldo_favor_iibb_tf,l10n_ar.l10nar_base_chart_template,1.1.4.02.700,account.data_account_type_current_assets,Saldo a favor IIBB Tierra del Fuego,False
|
||||
l10n_ar.base_retencion_iibb_tf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.710,account.data_account_type_current_assets,Retención IIBB Tierra del Fuego sufrida,False
|
||||
l10n_ar.base_percepcion_iibb_tf_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.02.720,account.data_account_type_current_assets,Percepción IIBB Tierra del Fuego sufrida,False
|
||||
l10n_ar.base_sircreb,l10n_ar.l10nar_base_chart_template,1.1.4.02.730,account.data_account_type_current_assets,SIRCREB,False
|
||||
l10n_ar.base_saldo_a_favor_suss,l10n_ar.l10nar_base_chart_template,1.1.4.03.010,account.data_account_type_current_assets,Saldo a favor SUSS,False
|
||||
l10n_ar.base_retencion_suss_sufrida,l10n_ar.l10nar_base_chart_template,1.1.4.03.020,account.data_account_type_current_assets,Retención SUSS Sufrida,False
|
||||
l10n_ar.ri_iva_credito_fiscal,l10n_ar.l10nar_ri_chart_template,1.1.4.04.010,account.data_account_type_current_assets,IVA crédito fiscal,False
|
||||
@@ -62,10 +122,56 @@ l10n_ar.base_tasa_municipal_a_pagar,l10n_ar.l10nar_base_chart_template,2.1.3.01.
|
||||
l10n_ar.base_plan_tasa_municipal_a_pagar,l10n_ar.l10nar_base_chart_template,2.1.3.01.020,account.data_account_type_payable,Plan Tasa Municipal a pagar,True
|
||||
l10n_ar.base_iibb_a_pagar,l10n_ar.l10nar_base_chart_template,2.1.3.02.010,account.data_account_type_payable,IIBB a pagar,True
|
||||
l10n_ar.ri_retencion_sicore_a_pagar,l10n_ar.l10nar_ex_chart_template,2.1.3.02.020,account.data_account_type_payable,SICORE a pagar,True
|
||||
l10n_ar.ri_retencion_iibb_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.030,account.data_account_type_current_liabilities,Retención IIBB aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.040,account.data_account_type_current_liabilities,Percepción IIBB aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_a_pagar,l10n_ar.l10nar_ex_chart_template,2.1.3.02.050,account.data_account_type_payable,Retención/Percepción IIBB a pagar,True
|
||||
l10n_ar.base_plan_de_iibb_a_pagar,l10n_ar.l10nar_base_chart_template,2.1.3.02.060,account.data_account_type_payable,Plan de IIBB a pagar,True
|
||||
l10n_ar.ri_retencion_iibb_caba_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.030,account.data_account_type_current_liabilities,Retención IIBB CABA aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_caba_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.040,account.data_account_type_current_liabilities,Percepción IIBB CABA aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ba_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.050,account.data_account_type_current_liabilities,Retención IIBB ARBA aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ba_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.060,account.data_account_type_current_liabilities,Percepción IIBB ARBA aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ca_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.070,account.data_account_type_current_liabilities,Retención IIBB Catamarca aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ca_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.080,account.data_account_type_current_liabilities,Percepción IIBB Catamarca aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_co_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.090,account.data_account_type_current_liabilities,Retención IIBB Córdoba aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_co_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.100,account.data_account_type_current_liabilities,Percepción IIBB Córdoba aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_rr_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.110,account.data_account_type_current_liabilities,Retención IIBB Corrientes aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_rr_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.120,account.data_account_type_current_liabilities,Percepción IIBB Corrientes aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_er_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.130,account.data_account_type_current_liabilities,Retención IIBB Entre Río aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_er_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.140,account.data_account_type_current_liabilities,Percepción IIBB Entre Río aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ju_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.150,account.data_account_type_current_liabilities,Retención IIBB Jujuy aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ju_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.160,account.data_account_type_current_liabilities,Percepción IIBB Jujuy aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_za_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.170,account.data_account_type_current_liabilities,Retención IIBB Mendoza aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_za_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.180,account.data_account_type_current_liabilities,Percepción IIBB Mendoza aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_lr_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.190,account.data_account_type_current_liabilities,Retención IIBB La Rioja aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_lr_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.200,account.data_account_type_current_liabilities,Percepción IIBB La Rioja aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_sa_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.210,account.data_account_type_current_liabilities,Retención IIBB Salta aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_sa_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.220,account.data_account_type_current_liabilities,Percepción IIBB Salta aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_nn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.230,account.data_account_type_current_liabilities,Retención IIBB San Juan aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_nn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.240,account.data_account_type_current_liabilities,Percepción IIBB San Juan aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_sl_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.250,account.data_account_type_current_liabilities,Retención IIBB San Luis aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_sl_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.260,account.data_account_type_current_liabilities,Percepción IIBB San Luis aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_sf_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.270,account.data_account_type_current_liabilities,Retención IIBB Santa Fe aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_sf_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.280,account.data_account_type_current_liabilities,Percepción IIBB Santa Fe aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_se_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.290,account.data_account_type_current_liabilities,Retención IIBB Santiago del Estero aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_se_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.300,account.data_account_type_current_liabilities,Percepción IIBB Santiago del Estero aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_tn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.310,account.data_account_type_current_liabilities,Retención IIBB Tucumán aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_tn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.320,account.data_account_type_current_liabilities,Percepción IIBB Tucumán aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ha_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.330,account.data_account_type_current_liabilities,Retención IIBB Chaco aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ha_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.340,account.data_account_type_current_liabilities,Percepción IIBB Chaco aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ct_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.350,account.data_account_type_current_liabilities,Retención IIBB Chubut aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ct_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.360,account.data_account_type_current_liabilities,Percepción IIBB Chubut aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_fo_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.370,account.data_account_type_current_liabilities,Retención IIBB Formosa aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_fo_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.380,account.data_account_type_current_liabilities,Percepción IIBB Formosa aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_mi_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.390,account.data_account_type_current_liabilities,Retención IIBB Misiones aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_mi_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.400,account.data_account_type_current_liabilities,Percepción IIBB Misiones aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_ne_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.410,account.data_account_type_current_liabilities,Retención IIBB Neuquén aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_ne_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.420,account.data_account_type_current_liabilities,Percepción IIBB Neuquén aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_lp_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.430,account.data_account_type_current_liabilities,Retención IIBB La Pampa aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_lp_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.440,account.data_account_type_current_liabilities,Percepción IIBB La Pampa aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_rn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.450,account.data_account_type_current_liabilities,Retención IIBB Río Negro aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_rn_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.460,account.data_account_type_current_liabilities,Percepción IIBB Río Negro aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_az_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.470,account.data_account_type_current_liabilities,Retención IIBB Santa Cruz aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_az_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.480,account.data_account_type_current_liabilities,Percepción IIBB Santa Cruz aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_tf_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.490,account.data_account_type_current_liabilities,Retención IIBB Tierra del Fuego aplicada,False
|
||||
l10n_ar.ri_percepcion_iibb_tf_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.02.500,account.data_account_type_current_liabilities,Percepción IIBB Tierra del Fuego aplicada,False
|
||||
l10n_ar.ri_retencion_iibb_a_pagar,l10n_ar.l10nar_ex_chart_template,2.1.3.02.510,account.data_account_type_payable,Retención/Percepción IIBB a pagar,True
|
||||
l10n_ar.base_plan_de_iibb_a_pagar,l10n_ar.l10nar_base_chart_template,2.1.3.02.520,account.data_account_type_payable,Plan de IIBB a pagar,True
|
||||
l10n_ar.ri_iva_debito_fiscal,l10n_ar.l10nar_ri_chart_template,2.1.3.03.010,account.data_account_type_current_liabilities,IVA débito fiscal,False
|
||||
l10n_ar.ri_iva_saldo_a_pagar,l10n_ar.l10nar_ri_chart_template,2.1.3.03.020,account.data_account_type_payable,IVA saldo a pagar,True
|
||||
l10n_ar.ri_retencion_iva_aplicada,l10n_ar.l10nar_ex_chart_template,2.1.3.03.030,account.data_account_type_current_liabilities,Retención IVA aplicada,False
|
||||
@@ -151,9 +257,29 @@ l10n_ar.base_seguros_administracion,l10n_ar.l10nar_base_chart_template,5.3.1.01.
|
||||
l10n_ar.base_sellados_y_certificaciones,l10n_ar.l10nar_base_chart_template,5.3.1.01.140,account.data_account_type_expenses,Sellados y Certificaciones,False
|
||||
l10n_ar.base_tasa_municipal,l10n_ar.l10nar_base_chart_template,5.4.1.01.010,account.data_account_type_expenses,Tasa Municipal,False
|
||||
l10n_ar.base_impuestos_iibb_caba,l10n_ar.l10nar_base_chart_template,5.4.2.01.010,account.data_account_type_expenses,IIBB CABA,False
|
||||
l10n_ar.base_impuestos_iibb_ba,l10n_ar.l10nar_base_chart_template,5.4.2.01.020,account.data_account_type_expenses,IIBB Prov. Bs. As.,False
|
||||
l10n_ar.base_impuestos_iibb_co,l10n_ar.l10nar_base_chart_template,5.4.2.01.030,account.data_account_type_expenses,IIBB Prov. Córdoba,False
|
||||
l10n_ar.base_impuestos_iibb_sf,l10n_ar.l10nar_base_chart_template,5.4.2.01.040,account.data_account_type_expenses,IIBB Prov. Santa Fé,False
|
||||
l10n_ar.base_impuestos_iibb_ba,l10n_ar.l10nar_base_chart_template,5.4.2.01.020,account.data_account_type_expenses,IIBB ARBA,False
|
||||
l10n_ar.base_impuestos_iibb_ca,l10n_ar.l10nar_base_chart_template,5.4.2.01.030,account.data_account_type_expenses,IIBB Catamarca,False
|
||||
l10n_ar.base_impuestos_iibb_co,l10n_ar.l10nar_base_chart_template,5.4.2.01.040,account.data_account_type_expenses,IIBB Córdoba,False
|
||||
l10n_ar.base_impuestos_iibb_rr,l10n_ar.l10nar_base_chart_template,5.4.2.01.050,account.data_account_type_expenses,IIBB Corrientes,False
|
||||
l10n_ar.base_impuestos_iibb_er,l10n_ar.l10nar_base_chart_template,5.4.2.01.060,account.data_account_type_expenses,IIBB Entre Ríos,False
|
||||
l10n_ar.base_impuestos_iibb_ju,l10n_ar.l10nar_base_chart_template,5.4.2.01.070,account.data_account_type_expenses,IIBB Jujuy,False
|
||||
l10n_ar.base_impuestos_iibb_za,l10n_ar.l10nar_base_chart_template,5.4.2.01.080,account.data_account_type_expenses,IIBB Mendoza,False
|
||||
l10n_ar.base_impuestos_iibb_lr,l10n_ar.l10nar_base_chart_template,5.4.2.01.090,account.data_account_type_expenses,IIBB La Rioja,False
|
||||
l10n_ar.base_impuestos_iibb_sa,l10n_ar.l10nar_base_chart_template,5.4.2.01.100,account.data_account_type_expenses,IIBB Salta,False
|
||||
l10n_ar.base_impuestos_iibb_nn,l10n_ar.l10nar_base_chart_template,5.4.2.01.110,account.data_account_type_expenses,IIBB San Juan,False
|
||||
l10n_ar.base_impuestos_iibb_sl,l10n_ar.l10nar_base_chart_template,5.4.2.01.120,account.data_account_type_expenses,IIBB San Luis,False
|
||||
l10n_ar.base_impuestos_iibb_sf,l10n_ar.l10nar_base_chart_template,5.4.2.01.130,account.data_account_type_expenses,IIBB Santa Fe,False
|
||||
l10n_ar.base_impuestos_iibb_se,l10n_ar.l10nar_base_chart_template,5.4.2.01.140,account.data_account_type_expenses,IIBB Santiago del Estero,False
|
||||
l10n_ar.base_impuestos_iibb_tn,l10n_ar.l10nar_base_chart_template,5.4.2.01.150,account.data_account_type_expenses,IIBB Tucumán,False
|
||||
l10n_ar.base_impuestos_iibb_ha,l10n_ar.l10nar_base_chart_template,5.4.2.01.160,account.data_account_type_expenses,IIBB Chaco,False
|
||||
l10n_ar.base_impuestos_iibb_ct,l10n_ar.l10nar_base_chart_template,5.4.2.01.170,account.data_account_type_expenses,IIBB Chubut,False
|
||||
l10n_ar.base_impuestos_iibb_fo,l10n_ar.l10nar_base_chart_template,5.4.2.01.180,account.data_account_type_expenses,IIBB Formosa,False
|
||||
l10n_ar.base_impuestos_iibb_mi,l10n_ar.l10nar_base_chart_template,5.4.2.01.190,account.data_account_type_expenses,IIBB Misiones,False
|
||||
l10n_ar.base_impuestos_iibb_ne,l10n_ar.l10nar_base_chart_template,5.4.2.01.200,account.data_account_type_expenses,IIBB Neuquén,False
|
||||
l10n_ar.base_impuestos_iibb_lp,l10n_ar.l10nar_base_chart_template,5.4.2.01.210,account.data_account_type_expenses,IIBB La Pampa,False
|
||||
l10n_ar.base_impuestos_iibb_rn,l10n_ar.l10nar_base_chart_template,5.4.2.01.220,account.data_account_type_expenses,IIBB Río Negro,False
|
||||
l10n_ar.base_impuestos_iibb_az,l10n_ar.l10nar_base_chart_template,5.4.2.01.230,account.data_account_type_expenses,IIBB Santa Cruz,False
|
||||
l10n_ar.base_impuestos_iibb_tf,l10n_ar.l10nar_base_chart_template,5.4.2.01.240,account.data_account_type_expenses,IIBB Tierra del Fuego,False
|
||||
l10n_ar.base_impuestos_debitos_y_creditos,l10n_ar.l10nar_base_chart_template,5.4.3.01.010,account.data_account_type_expenses,Impuestos a los débitos y créditos bancarios,False
|
||||
l10n_ar.base_impuestos_a_las_ganancias,l10n_ar.l10nar_ex_chart_template,5.5.1.01.010,account.data_account_type_expenses,Impuestos a las ganancias,False
|
||||
l10n_ar.base_resultado_intereses_y_recargos,l10n_ar.l10nar_base_chart_template,5.6.1.01.020,account.data_account_type_expenses,Intereses por préstamos,False
|
||||
|
||||
|
@@ -69,8 +69,129 @@
|
||||
<field name="l10n_ar_tribute_afip_code">06</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_caba">
|
||||
<field name="name">Perc IIBB CABA</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ba">
|
||||
<field name="name">Perc IIBB ARBA</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ca">
|
||||
<field name="name">Perc IIBB Catamarca</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_co">
|
||||
<field name="name">Perc IIBB Córdoba</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_rr">
|
||||
<field name="name">Perc IIBB Corrientes</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_er">
|
||||
<field name="name">Perc IIBB Entre Ríos</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ju">
|
||||
<field name="name">Perc IIBB Jujuy</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_za">
|
||||
<field name="name">Perc IIBB Mendoza</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_lr">
|
||||
<field name="name">Perc IIBB La Rioja</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_sa">
|
||||
<field name="name">Perc IIBB Salta</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_nn">
|
||||
<field name="name">Perc IIBB San Juan</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_sl">
|
||||
<field name="name">Perc IIBB San Luis</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_sf">
|
||||
<field name="name">Perc IIBB Santa Fe</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_se">
|
||||
<field name="name">Perc IIBB Santiago del Estero</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_tn">
|
||||
<field name="name">Perc IIBB Tucumán</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ha">
|
||||
<field name="name">Perc IIBB Chaco</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ct">
|
||||
<field name="name">Perc IIBB Chubut</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_fo">
|
||||
<field name="name">Perc IIBB Formosa</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_mi">
|
||||
<field name="name">Perc IIBB Misiones</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_ne">
|
||||
<field name="name">Perc IIBB Neuquén</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_lp">
|
||||
<field name="name">Perc IIBB La Pampa</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_rn">
|
||||
<field name="name">Perc IIBB Río Negro</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_az">
|
||||
<field name="name">Perc IIBB Santa Cruz</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb_tf">
|
||||
<field name="name">Perc IIBB Tierra del Fuego</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.group" id="tax_group_percepcion_iibb">
|
||||
<field name="name">IIBB Perceptions</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="l10n_ar_tribute_afip_code">07</field>
|
||||
</record>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,13 @@
|
||||
|
||||
<function model="account.tax" name="write" context="{'active_test': False}">
|
||||
<value model="account.tax" eval="obj().search([('company_id', '=', ref('company_ri')), ('tax_group_id', 'in',
|
||||
[ref('tax_group_percepcion_iva'), ref('tax_group_percepcion_ganancias'), ref('tax_group_percepcion_iibb'), ref('account.tax_group_taxes')]
|
||||
[ref('tax_group_percepcion_iva'),
|
||||
ref('tax_group_percepcion_ganancias'),
|
||||
ref('tax_group_percepcion_iibb_caba'),
|
||||
ref('tax_group_percepcion_iibb_ba'),
|
||||
ref('tax_group_percepcion_iibb_co'),
|
||||
ref('tax_group_percepcion_iibb_sf'),
|
||||
ref('account.tax_group_taxes')]
|
||||
)]).ids"/>
|
||||
<value eval="{'amount': 0.1, 'active': True}"/>
|
||||
</function>
|
||||
|
||||
@@ -159,9 +159,6 @@
|
||||
<attribute name="t-esc">', '.join(map(lambda x: (x.description or x.name), line.l10n_latam_tax_ids.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code)))</attribute>
|
||||
</span>
|
||||
|
||||
<!-- remove payment term, this is added on information section -->
|
||||
<p name="payment_term" position="replace"/>
|
||||
|
||||
<!-- remove payment reference that is not used in Argentina -->
|
||||
<xpath expr="//span[@t-field='o.payment_reference']/../.." position="replace"/>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_be
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0\n"
|
||||
"Project-Id-Version: Flectra Server 13.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-27 14:01+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 14:01+0000\n"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_be
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0\n"
|
||||
"Project-Id-Version: Flectra Server 13.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-27 14:01+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 14:01+0000\n"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_be
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0\n"
|
||||
"Project-Id-Version: Flectra Server 13.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-27 14:01+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 14:01+0000\n"
|
||||
@@ -4549,5 +4549,5 @@ msgstr ""
|
||||
#: model:ir.model.fields,help:l10n_be.field_account_journal__invoice_reference_model
|
||||
msgid ""
|
||||
"You can choose different models for each type of reference. The default one "
|
||||
"is the Odoo reference."
|
||||
"is the Flectra reference."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_be
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0\n"
|
||||
"Project-Id-Version: Flectra Server 13.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-27 14:01+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 14:01+0000\n"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ class TestUBL(AccountEdiTestCommon):
|
||||
|
||||
cls.partner_a.vat = 'BE0477472701'
|
||||
|
||||
def test_invoice_edi_xml(self):
|
||||
def test_invoice_edi_xml_update(self):
|
||||
invoice = self._create_empty_vendor_bill()
|
||||
invoice_count = len(self.env['account.move'].search([]))
|
||||
self.update_invoice_from_file('l10n_be_edi', 'test_xml_file', 'efff_test.xml', invoice)
|
||||
@@ -18,3 +18,11 @@ class TestUBL(AccountEdiTestCommon):
|
||||
self.assertEqual(invoice.amount_total, 666.50)
|
||||
self.assertEqual(invoice.amount_tax, 115.67)
|
||||
self.assertEqual(invoice.partner_id, self.partner_a)
|
||||
|
||||
def test_invoice_edi_xml_create(self):
|
||||
invoice_count = len(self.env['account.move'].search([]))
|
||||
invoice = self.create_invoice_from_file('l10n_be_edi', 'test_xml_file', 'efff_test.xml')
|
||||
self.assertEqual(len(self.env['account.move'].search([])), invoice_count + 1)
|
||||
self.assertEqual(invoice.amount_total, 666.50)
|
||||
self.assertEqual(invoice.amount_tax, 115.67)
|
||||
self.assertEqual(invoice.partner_id, self.partner_a)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_ca
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 9.0\n"
|
||||
"Project-Id-Version: Flectra Server 9.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-11-03 22:23+0000\n"
|
||||
"PO-Revision-Date: 2015-11-03 17:52-0500\n"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_ca
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0c\n"
|
||||
"Project-Id-Version: Flectra Server 10.0c\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-11-03 22:23+0000\n"
|
||||
"PO-Revision-Date: 2015-11-03 22:23+0000\n"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_ch
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0+e\n"
|
||||
"Project-Id-Version: Flectra Server 2.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-11-27 07:17+0000\n"
|
||||
"PO-Revision-Date: 2020-11-27 07:17+0000\n"
|
||||
@@ -1789,10 +1789,10 @@ msgstr "Fahrzeuge"
|
||||
#: model:ir.model.fields,help:l10n_ch.field_account_journal__invoice_reference_model
|
||||
msgid ""
|
||||
"You can choose different models for each type of reference. The default one "
|
||||
"is the Odoo reference."
|
||||
"is the Flectra reference."
|
||||
msgstr ""
|
||||
"Sie können für jede Art von Referenz verschiedene Modelle auswählen. Die "
|
||||
"Standardeinstellung ist die Odoo-Referenz."
|
||||
"Standardeinstellung ist die Flectra-Referenz."
|
||||
|
||||
#. module: l10n_ch
|
||||
#: code:addons/l10n_ch/models/account_invoice.py:0
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_ch
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0+e\n"
|
||||
"Project-Id-Version: Flectra Server 2.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-11-27 07:25+0000\n"
|
||||
"PO-Revision-Date: 2020-11-27 07:25+0000\n"
|
||||
@@ -1796,7 +1796,7 @@ msgstr "Vehicles"
|
||||
#: model:ir.model.fields,help:l10n_ch.field_account_journal__invoice_reference_model
|
||||
msgid ""
|
||||
"You can choose different models for each type of reference. The default one "
|
||||
"is the Odoo reference."
|
||||
"is the Flectra reference."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_ch
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user