mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-25 07:27:39 -05:00
[PATCH] Upstream patch
This commit is contained in:
1 parent
45655fd76d
commit
3e576b368d
155 files changed
+1673
-901
No files matched your search
@@ -367,9 +367,12 @@ class AccountMove(models.Model):
|
||||
if self.invoice_date:
|
||||
if not self.invoice_payment_term_id and (not self.invoice_date_due or self.invoice_date_due < self.invoice_date):
|
||||
self.invoice_date_due = self.invoice_date
|
||||
if self.date != self.invoice_date: # Don't flag date as dirty if not needed
|
||||
if (
|
||||
self.is_sale_document() and self.date != self.invoice_date
|
||||
or self.is_purchase_document() and not self.date
|
||||
):
|
||||
self.date = self.invoice_date
|
||||
self._onchange_currency()
|
||||
self._onchange_currency()
|
||||
|
||||
@api.onchange('journal_id')
|
||||
def _onchange_journal(self):
|
||||
@@ -795,7 +798,7 @@ class AccountMove(models.Model):
|
||||
'amount_currency': diff_amount_currency,
|
||||
'partner_id': self.partner_id.id,
|
||||
'move_id': self.id,
|
||||
'currency_id': self.currency_id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
'company_currency_id': self.company_id.currency_id.id,
|
||||
'is_rounding_line': True,
|
||||
@@ -2338,13 +2341,17 @@ class AccountMove(models.Model):
|
||||
if cancel:
|
||||
reverse_moves.with_context(move_reverse_cancel=cancel)._post(soft=False)
|
||||
for move, reverse_move in zip(self, reverse_moves):
|
||||
accounts = move.mapped('line_ids.account_id') \
|
||||
.filtered(lambda account: account.reconcile or account.internal_type == 'liquidity')
|
||||
for account in accounts:
|
||||
(move.line_ids + reverse_move.line_ids)\
|
||||
.filtered(lambda line: line.account_id == account and not line.reconciled)\
|
||||
.with_context(move_reverse_cancel=cancel)\
|
||||
.reconcile()
|
||||
lines = move.line_ids.filtered(
|
||||
lambda x: (x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
|
||||
and not x.reconciled
|
||||
)
|
||||
for line in lines:
|
||||
counterpart_lines = reverse_move.line_ids.filtered(
|
||||
lambda x: x.account_id == line.account_id
|
||||
and x.currency_id == line.currency_id
|
||||
and not x.reconciled
|
||||
)
|
||||
(line + counterpart_lines).with_context(move_reverse_cancel=cancel).reconcile()
|
||||
|
||||
return reverse_moves
|
||||
|
||||
@@ -3540,6 +3547,8 @@ class AccountMoveLine(models.Model):
|
||||
@api.onchange('product_uom_id')
|
||||
def _onchange_uom_id(self):
|
||||
''' Recompute the 'price_unit' depending of the unit of measure. '''
|
||||
if self.display_type in ('line_section', 'line_note'):
|
||||
return
|
||||
taxes = self._get_computed_taxes()
|
||||
if taxes and self.move_id.fiscal_position_id:
|
||||
taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
|
||||
@@ -4235,12 +4244,16 @@ class AccountMoveLine(models.Model):
|
||||
if debit_line_currency == credit_line_currency:
|
||||
# Reconcile on the same currency.
|
||||
|
||||
# The debit line is now fully reconciled.
|
||||
# The debit line is now fully reconciled because:
|
||||
# - either amount_residual & amount_residual_currency are at 0.
|
||||
# - either the credit_line is not an exchange difference one.
|
||||
if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left):
|
||||
debit_line = None
|
||||
continue
|
||||
|
||||
# The credit line is now fully reconciled.
|
||||
# The credit line is now fully reconciled because:
|
||||
# - either amount_residual & amount_residual_currency are at 0.
|
||||
# - either the debit is not an exchange difference one.
|
||||
if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left):
|
||||
credit_line = None
|
||||
continue
|
||||
@@ -4252,13 +4265,13 @@ class AccountMoveLine(models.Model):
|
||||
else:
|
||||
# Reconcile on the company's currency.
|
||||
|
||||
# The debit line is now fully reconciled.
|
||||
if not has_debit_residual_left and (has_credit_residual_left or not has_debit_residual_curr_left):
|
||||
# The debit line is now fully reconciled since amount_residual is 0.
|
||||
if not has_debit_residual_left:
|
||||
debit_line = None
|
||||
continue
|
||||
|
||||
# The credit line is now fully reconciled.
|
||||
if not has_credit_residual_left and (has_debit_residual_left or not has_credit_residual_curr_left):
|
||||
# The credit line is now fully reconciled since amount_residual is 0.
|
||||
if not has_credit_residual_left:
|
||||
credit_line = None
|
||||
continue
|
||||
|
||||
@@ -4418,12 +4431,23 @@ class AccountMoveLine(models.Model):
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': line.account_id.id,
|
||||
'tax_base_amount': line.tax_base_amount,
|
||||
'tax_repartition_line_id': line.tax_repartition_line_id.id,
|
||||
}
|
||||
if grouping_key in account_vals_to_fix:
|
||||
debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit']
|
||||
credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit']
|
||||
balance = debit - credit
|
||||
|
||||
account_vals_to_fix[grouping_key].update({
|
||||
'debit': balance if balance > 0 else 0,
|
||||
'credit': -balance if balance < 0 else 0,
|
||||
'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount,
|
||||
})
|
||||
else:
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': line.account_id.id,
|
||||
'tax_base_amount': line.tax_base_amount,
|
||||
'tax_repartition_line_id': line.tax_repartition_line_id.id,
|
||||
}
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
account_to_fix = line.company_id.account_cash_basis_base_account_id
|
||||
|
||||
@@ -522,33 +522,40 @@ class AccountPartialReconcile(models.Model):
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
|
||||
cb_tax_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_tax_line_vals)
|
||||
partial_lines_to_create[grouping_key] = {
|
||||
'tax_line': line,
|
||||
'vals': cb_tax_line_vals,
|
||||
}
|
||||
|
||||
cb_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_line_vals)
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
|
||||
cb_base_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_base_line_vals)
|
||||
cb_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_line_vals)
|
||||
|
||||
if grouping_key in partial_lines_to_create:
|
||||
aggregated_vals = partial_lines_to_create[grouping_key]['vals']
|
||||
balance = aggregated_vals['debit'] - aggregated_vals['credit']
|
||||
balance += cb_base_line_vals['debit'] - cb_base_line_vals['credit']
|
||||
if grouping_key in partial_lines_to_create:
|
||||
aggregated_vals = partial_lines_to_create[grouping_key]['vals']
|
||||
|
||||
debit = aggregated_vals['debit'] + cb_line_vals['debit']
|
||||
credit = aggregated_vals['credit'] + cb_line_vals['credit']
|
||||
balance = debit - credit
|
||||
|
||||
aggregated_vals.update({
|
||||
'debit': balance if balance > 0 else 0,
|
||||
'credit': -balance if balance < 0 else 0,
|
||||
'amount_currency': aggregated_vals['amount_currency'] + cb_line_vals['amount_currency'],
|
||||
})
|
||||
|
||||
if line.tax_repartition_line_id:
|
||||
aggregated_vals.update({
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'tax_base_amount': aggregated_vals['tax_base_amount'] + cb_line_vals['tax_base_amount'],
|
||||
})
|
||||
partial_lines_to_create[grouping_key]['tax_line'] += line
|
||||
else:
|
||||
partial_lines_to_create[grouping_key] = {
|
||||
'vals': cb_line_vals,
|
||||
}
|
||||
if line.tax_repartition_line_id:
|
||||
partial_lines_to_create[grouping_key].update({
|
||||
'tax_line': line,
|
||||
})
|
||||
aggregated_vals['amount_currency'] += cb_base_line_vals['amount_currency']
|
||||
else:
|
||||
partial_lines_to_create[grouping_key] = {
|
||||
'vals': cb_base_line_vals,
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# Create the counterpart journal items.
|
||||
|
||||
@@ -884,7 +884,8 @@ 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 = 100 - abs(line_residual_after_reconciliation) / abs(line_residual - line_residual_after_reconciliation) * 100
|
||||
residual_difference = line_residual - line_residual_after_reconciliation
|
||||
reconciled_percentage = 100 - abs(line_residual_after_reconciliation) / abs(residual_difference) * 100 if (residual_difference != 0) else 0
|
||||
return reconciled_percentage >= self.match_total_amount_param
|
||||
|
||||
def _filter_candidates(self, candidates, aml_ids_to_exclude, reconciled_amls_ids):
|
||||
|
||||
@@ -31,12 +31,11 @@ class AccountTaxReport(models.Model):
|
||||
new_tags = tags_cache[cache_key]
|
||||
|
||||
if new_tags:
|
||||
tags_to_unlink = line.tag_ids.filtered(lambda x: record == x.mapped('tax_report_line_ids.report_id'))
|
||||
# == instead of in, as we only want tags_to_unlink to contain the tags that are not linked to any other report than the one we're considering
|
||||
line._remove_tags_used_only_by_self()
|
||||
line.write({'tag_ids': [(6, 0, new_tags.ids)]})
|
||||
self.env['account.tax.report.line']._delete_tags_from_taxes(tags_to_unlink.ids)
|
||||
|
||||
elif line.mapped('tag_ids.tax_report_line_ids.report_id').filtered(lambda x: x not in self):
|
||||
line._remove_tags_used_only_by_self()
|
||||
line.write({'tag_ids': [(5, 0, 0)] + line._get_tags_create_vals(line.tag_name, vals['country_id'])})
|
||||
tags_cache[cache_key] = line.tag_ids
|
||||
|
||||
@@ -203,7 +202,7 @@ class AccountTaxReportLine(models.Model):
|
||||
# All the lines sharing their tags must always be synchronized,
|
||||
tags_to_remove += records_to_link.mapped('tag_ids')
|
||||
records_to_link = tags_to_remove.mapped('tax_report_line_ids')
|
||||
self._delete_tags_from_taxes(tags_to_remove.ids)
|
||||
tags_to_remove.mapped('tax_report_line_ids')._remove_tags_used_only_by_self()
|
||||
records_to_link.write({'tag_name': tag_name_postponed, 'tag_ids': [(2, tag.id) for tag in tags_to_remove] + [(6, 0, existing_tags.ids)]})
|
||||
|
||||
else:
|
||||
@@ -220,17 +219,27 @@ class AccountTaxReportLine(models.Model):
|
||||
return rslt
|
||||
|
||||
def unlink(self):
|
||||
self._delete_tags_from_taxes(self.mapped('tag_ids.id'))
|
||||
self._remove_tags_used_only_by_self()
|
||||
children = self.mapped('children_line_ids')
|
||||
if children:
|
||||
children.unlink()
|
||||
return super(AccountTaxReportLine, self).unlink()
|
||||
|
||||
def _remove_tags_used_only_by_self(self):
|
||||
""" Deletes and removes from taxes and move lines all the
|
||||
tags from the provided tax report lines that are not linked
|
||||
to any other tax report lines.
|
||||
"""
|
||||
all_tags = self.mapped('tag_ids')
|
||||
tags_to_unlink = all_tags.filtered(lambda x: not (x.tax_report_line_ids - self))
|
||||
self.write({'tag_ids': [(3, tag.id, 0) for tag in tags_to_unlink]})
|
||||
self._delete_tags_from_taxes(tags_to_unlink.ids)
|
||||
|
||||
@api.model
|
||||
def _delete_tags_from_taxes(self, tag_ids_to_delete):
|
||||
""" Based on a list of tag ids, removes them first from the
|
||||
repartition lines they are linked to, then deletes them
|
||||
from the account move lines.
|
||||
from the account move lines, and finally unlink them.
|
||||
"""
|
||||
if not tag_ids_to_delete:
|
||||
# Nothing to do, then!
|
||||
@@ -247,6 +256,8 @@ class AccountTaxReportLine(models.Model):
|
||||
self.env['account.move.line'].invalidate_cache(fnames=['tax_tag_ids'])
|
||||
self.env['account.tax.repartition.line'].invalidate_cache(fnames=['tag_ids'])
|
||||
|
||||
self.env['account.account.tag'].browse(tag_ids_to_delete).unlink()
|
||||
|
||||
@api.constrains('formula', 'tag_name')
|
||||
def _validate_formula(self):
|
||||
for record in self:
|
||||
|
||||
@@ -98,7 +98,11 @@ class AccountInvoiceReport(models.Model):
|
||||
line.quantity / NULLIF(COALESCE(uom_line.factor, 1) / COALESCE(uom_template.factor, 1), 0.0) * (CASE WHEN move.move_type IN ('in_invoice','out_refund','in_receipt') THEN -1 ELSE 1 END)
|
||||
AS quantity,
|
||||
-line.balance * currency_table.rate AS price_subtotal,
|
||||
-line.balance / NULLIF(COALESCE(uom_line.factor, 1) / COALESCE(uom_template.factor, 1), 0.0) * currency_table.rate
|
||||
-COALESCE(line.balance
|
||||
/ NULLIF(line.quantity, 0.0)
|
||||
/ NULLIF(COALESCE(uom_line.factor, 1), 0.0)
|
||||
/ NULLIF(COALESCE(uom_template.factor, 1), 0.0),
|
||||
0.0) * currency_table.rate
|
||||
AS price_average,
|
||||
COALESCE(partner.country_id, commercial_partner.country_id) AS country_id
|
||||
'''
|
||||
|
||||
@@ -364,6 +364,7 @@ class AccountTestInvoicingCommon(SavepointCase):
|
||||
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=[], amounts=[], taxes=None):
|
||||
move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, account_predictive_bills_disable_prediction=True))
|
||||
move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01')
|
||||
move_form.date = move_form.invoice_date
|
||||
move_form.partner_id = partner or cls.partner_a
|
||||
|
||||
for product in products:
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
||||
(0, None, {
|
||||
'product_id': cls.product_a.id,
|
||||
'quantity': 3,
|
||||
'price_unit': 1000,
|
||||
'price_unit': 750,
|
||||
}),
|
||||
(0, None, {
|
||||
'product_id': cls.product_a.id,
|
||||
@@ -110,7 +110,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
|
||||
#price_average price_subtotal quantity
|
||||
[2000, 2000, 1],
|
||||
[1000, 1000, 1],
|
||||
[1000, 1000, 3],
|
||||
[250, 750, 3],
|
||||
[6, 6, 1],
|
||||
[-20, -20, -1],
|
||||
[-20, -20, -1],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
@@ -677,6 +679,232 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
|
||||
self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
|
||||
|
||||
def test_reconcile_asymetric_rate_change(self):
|
||||
account_id = self.company_data['default_account_receivable'].id
|
||||
|
||||
# Rate is 3.0 in 2016, 2.0 in 2017.
|
||||
currency1_id = self.currency_data['currency'].id
|
||||
# Rate is 6.0 in 2016, 4.0 in 2017.
|
||||
currency2_id = self.currency_data_2['currency'].id
|
||||
|
||||
# Create rate changes for 2018: currency1 rate increases while currency2 rate decreases.
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 8.0,
|
||||
'currency_id': currency1_id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-01-01',
|
||||
'rate': 2.0,
|
||||
'currency_id': currency2_id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
|
||||
moves = self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': '2018-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 1200.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 9600.0,
|
||||
'account_id': account_id,
|
||||
'currency_id': currency1_id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 960.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 1920.0,
|
||||
'account_id': account_id,
|
||||
'currency_id': currency2_id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 2160.0,
|
||||
'account_id': account_id,
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 1200.0,
|
||||
'amount_currency': -4800.0,
|
||||
'account_id': account_id,
|
||||
'currency_id': currency2_id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 960.0,
|
||||
'amount_currency': -1920.0,
|
||||
'account_id': account_id,
|
||||
'currency_id': currency1_id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 2160.0,
|
||||
'credit': 0.0,
|
||||
'account_id': account_id,
|
||||
}),
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
moves.action_post()
|
||||
|
||||
line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
|
||||
line_2 = moves.line_ids.filtered(lambda line: line.debit == 960.0)
|
||||
line_3 = moves.line_ids.filtered(lambda line: line.credit == 1200.0)
|
||||
line_4 = moves.line_ids.filtered(lambda line: line.credit == 960.0)
|
||||
|
||||
self.assertRecordValues(line_1 + line_2 + line_3 + line_4, [
|
||||
{'amount_residual': 1200.0, 'amount_residual_currency': 9600.0, 'reconciled': False},
|
||||
{'amount_residual': 960.0, 'amount_residual_currency': 1920.0, 'reconciled': False},
|
||||
{'amount_residual': -1200.0, 'amount_residual_currency': -4800.0, 'reconciled': False},
|
||||
{'amount_residual': -960.0, 'amount_residual_currency': -1920.0, 'reconciled': False},
|
||||
])
|
||||
|
||||
# Reconcile with debit_line currency rate increased and credit_line currency rate decreased between
|
||||
# credit_line.date and debit_line.date.
|
||||
|
||||
res = (line_1 + line_3).reconcile()
|
||||
|
||||
exchange_diff = res['full_reconcile'].exchange_move_id
|
||||
exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 2400.0,
|
||||
'currency_id': currency2_id,
|
||||
'account_id': account_id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -2400.0,
|
||||
'currency_id': currency2_id,
|
||||
'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 7200.0,
|
||||
'currency_id': currency1_id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -7200.0,
|
||||
'currency_id': currency1_id,
|
||||
'account_id': account_id,
|
||||
}
|
||||
])
|
||||
|
||||
self.assertPartialReconcile(res['partials'], [
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 2400.0,
|
||||
'credit_amount_currency': 2400.0,
|
||||
'debit_move_id': exchange_diff_lines[0].id,
|
||||
'credit_move_id': line_3.id,
|
||||
},
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 7200.0,
|
||||
'credit_amount_currency': 7200.0,
|
||||
'debit_move_id': line_1.id,
|
||||
'credit_move_id': exchange_diff_lines[3].id,
|
||||
},
|
||||
{
|
||||
'amount': 1200.0,
|
||||
'debit_amount_currency': 2400.0,
|
||||
'credit_amount_currency': 2400.0,
|
||||
'debit_move_id': line_1.id,
|
||||
'credit_move_id': line_3.id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(line_1 + line_3, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
# Reconcile with debit_line currency rate decreased and credit_line currency rate increased between
|
||||
# credit_line.date and debit_line.date.
|
||||
|
||||
res = (line_2 + line_4).reconcile()
|
||||
|
||||
exchange_diff = res['full_reconcile'].exchange_move_id
|
||||
exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 5760.0,
|
||||
'currency_id': currency1_id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -5760.0,
|
||||
'currency_id': currency1_id,
|
||||
'account_id': account_id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 1920.0,
|
||||
'currency_id': currency2_id,
|
||||
'account_id': account_id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -1920.0,
|
||||
'currency_id': currency2_id,
|
||||
'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
|
||||
}
|
||||
])
|
||||
|
||||
self.assertPartialReconcile(res['partials'], [
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 1920.0,
|
||||
'credit_amount_currency': 1920.0,
|
||||
'debit_move_id': exchange_diff_lines[2].id,
|
||||
'credit_move_id': line_2.id,
|
||||
},
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 5760.0,
|
||||
'credit_amount_currency': 5760.0,
|
||||
'debit_move_id': line_4.id,
|
||||
'credit_move_id': exchange_diff_lines[1].id,
|
||||
},
|
||||
{
|
||||
'amount': 960.0,
|
||||
'debit_amount_currency': 3840.0,
|
||||
'credit_amount_currency': 7680.0,
|
||||
'debit_move_id': line_2.id,
|
||||
'credit_move_id': line_4.id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(line_2 + line_4, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
def test_reverse_exchange_difference_same_foreign_currency(self):
|
||||
move_2016 = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
@@ -2076,6 +2304,82 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
# Check full reconciliation
|
||||
self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
|
||||
|
||||
def test_caba_double_tax(self):
|
||||
""" Test the CABA entries generated from an invoice with almost
|
||||
equal lines, different only on analytic accounting
|
||||
"""
|
||||
# Make the tax account reconcilable
|
||||
self.tax_account_1.reconcile = True
|
||||
|
||||
# Create an invoice with a CABA tax using 'Include in analytic cost'
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', account_predictive_bills_disable_prediction=True))
|
||||
move_form.invoice_date = fields.Date.from_string('2019-01-01')
|
||||
move_form.partner_id = self.partner_a
|
||||
self.cash_basis_tax_a_third_amount.analytic = True
|
||||
test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account'})
|
||||
|
||||
tax = self.cash_basis_tax_a_third_amount
|
||||
|
||||
# line with analytic account, will generate 2 lines in CABA move
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = "test line with analytic account"
|
||||
line_form.product_id = self.product_a
|
||||
line_form.tax_ids.clear()
|
||||
line_form.tax_ids.add(tax)
|
||||
line_form.analytic_account_id = test_analytic_account
|
||||
line_form.price_unit = 100
|
||||
|
||||
# line with analytic account, will generate other 2 lines in CABA move
|
||||
# even if the tax is the same
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = "test line"
|
||||
line_form.product_id = self.product_a
|
||||
line_form.tax_ids.clear()
|
||||
line_form.tax_ids.add(tax)
|
||||
line_form.price_unit = 100
|
||||
|
||||
rslt = move_form.save()
|
||||
rslt.action_post()
|
||||
|
||||
pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=rslt.ids).create({
|
||||
'amount': rslt.amount_total,
|
||||
'payment_date': rslt.date,
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
})
|
||||
pmt_wizard._create_payments()
|
||||
|
||||
partial_rec = rslt.mapped('line_ids.matched_debit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
|
||||
self.assertEqual(len(caba_move.line_ids), 4, "All lines should be there")
|
||||
self.assertEqual(caba_move.line_ids.filtered(lambda x: x.tax_line_id).balance, 66.66, "Tax amount should take into account both lines")
|
||||
|
||||
def test_caba_double_tax_negative_line(self):
|
||||
""" Tests making a cash basis invoice with 2 lines using the same tax: a positive and a negative one.
|
||||
"""
|
||||
invoice = self.init_invoice('in_invoice', amounts=[300, -60], post=True, taxes=self.cash_basis_tax_a_third_amount)
|
||||
|
||||
pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
|
||||
'amount': 320,
|
||||
'payment_date': invoice.date,
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
})
|
||||
|
||||
pmt_wizard._create_payments()
|
||||
|
||||
partial_rec = invoice.mapped('line_ids.matched_debit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
|
||||
|
||||
self.assertRecordValues(caba_move.line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id)), [
|
||||
# Base amount:
|
||||
{'debit': 240.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[0].ids, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 240.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
|
||||
# tax:
|
||||
{'debit': 80.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
|
||||
{'debit': 0.0, 'credit': 80.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
|
||||
])
|
||||
|
||||
def test_caba_dest_acc_reconciliation_partial_pmt(self):
|
||||
""" Test the reconciliation of tax lines (when using a reconcilable tax account)
|
||||
for partially paid invoices with cash basis taxes.
|
||||
|
||||
@@ -68,6 +68,13 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
||||
'sequence': 7,
|
||||
})
|
||||
|
||||
cls.tax_report_line_1_6 = cls.env['account.tax.report.line'].create({
|
||||
'name': "[100] Line 100",
|
||||
'tag_name': '100',
|
||||
'report_id': cls.tax_report_1.id,
|
||||
'sequence': 8,
|
||||
})
|
||||
|
||||
cls.tax_report_2 = cls.env['account.tax.report'].create({
|
||||
'name': "Tax report 2",
|
||||
'country_id': cls.test_country_1.id,
|
||||
@@ -93,6 +100,13 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
||||
'sequence': 3,
|
||||
})
|
||||
|
||||
cls.tax_report_line_2_6 = cls.env['account.tax.report.line'].create({
|
||||
'name': "[100] Line 100",
|
||||
'tag_name': '100',
|
||||
'report_id': cls.tax_report_2.id,
|
||||
'sequence': 4,
|
||||
})
|
||||
|
||||
def _get_tax_tags(self, tag_name=None):
|
||||
domain = [('country_id', '=', self.test_country_1.id), ('applicability', '=', 'taxes')]
|
||||
if tag_name:
|
||||
@@ -255,3 +269,17 @@ class TaxReportTest(AccountTestInvoicingCommon):
|
||||
self.assertEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines not sharing their tags with any other report should keep the same tags when the country of their report is changed")
|
||||
elif line.tag_ids or original_report_2_tags[line.id]:
|
||||
self.assertNotEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines sharing their tags with other report should receive new tags when the country of their report is changed")
|
||||
|
||||
def test_unlink_report_line_tags(self):
|
||||
""" Under certain circumstances, unlinking a tax report line should also unlink
|
||||
the tags that are linked to it. We test those cases here.
|
||||
"""
|
||||
def check_tags_unlink(tag_name, report_lines, unlinked, error_message):
|
||||
report_lines.unlink()
|
||||
surviving_tags = self._get_tax_tags(tag_name)
|
||||
required_len = 0 if unlinked else 2 # 2 for + and - tag
|
||||
self.assertEqual(len(surviving_tags), required_len, error_message)
|
||||
|
||||
check_tags_unlink('42', self.tax_report_line_2_42, True, "Unlinking one line not sharing its tags should also unlink them")
|
||||
check_tags_unlink('01', self.tax_report_line_1_1, False, "Unlinking one line sharing its tags with others should keep the tags")
|
||||
check_tags_unlink('100', self.tax_report_line_1_6 + self.tax_report_line_2_6, True, "Unlinkink all the lines sharing the same tags should also unlink them")
|
||||
@@ -13,9 +13,9 @@
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="applicability"/>
|
||||
<field name="tax_negate" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
<field name="country_id" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
<field name="tax_report_line_ids" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
<field name="tax_negate" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
<field name="country_id" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
<field name="tax_report_line_ids" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
@@ -1186,12 +1186,12 @@
|
||||
<filter name="due_date" string="Due Date" date="invoice_date_due"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Salesperson" name="salesperson" context="{'group_by':'invoice_user_id'}"/>
|
||||
<filter name="status" string="Status" context="{'group_by':'state'}"/>
|
||||
|
||||
@@ -118,12 +118,12 @@
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -128,7 +128,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'partner_id': line.partner_id.id,
|
||||
'account_id': line.account_id.id,
|
||||
'currency_id': (line.currency_id or line.company_currency_id).id,
|
||||
'partner_bank_id': line.move_id.partner_bank_id.id,
|
||||
'partner_bank_id': (line.move_id.partner_bank_id or line.partner_id.commercial_partner_id.bank_ids[:1]).id,
|
||||
'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
|
||||
'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from flectra import models, fields, api
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tools.pdf import FlectraPdfFileReader, FlectraPdfFileWriter
|
||||
from flectra.osv import expression
|
||||
from flectra.tools import html_escape
|
||||
|
||||
from lxml import etree
|
||||
import base64
|
||||
@@ -499,5 +500,5 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
@api.model
|
||||
def _format_error_message(self, error_title, errors):
|
||||
bullet_list_msg = ''.join('<li>%s</li>' % msg for msg in errors)
|
||||
bullet_list_msg = ''.join('<li>%s</li>' % html_escape(msg) for msg in errors)
|
||||
return '%s<ul>%s</ul>' % (error_title, bullet_list_msg)
|
||||
@@ -192,17 +192,19 @@ class AccountEdiFormat(models.Model):
|
||||
account_predictive_bills_disable_prediction=True)) as invoice_form:
|
||||
# Partner (first step to avoid warning 'Warning! You must first select a partner.').
|
||||
partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty'
|
||||
elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap)
|
||||
partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//ram:' + partner_type + '/ram:Name', namespaces=tree.nsmap)
|
||||
partner_name = elements and elements[0].text
|
||||
partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap)
|
||||
partner = elements and self.env['res.partner'].search([('email', '=', elements[0].text)], limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
invoice_form.partner_id = self._retrieve_partner(
|
||||
name=self._find_value('//ram:' + partner_type + '/ram:Name', tree, namespaces=tree.nsmap),
|
||||
mail=self._find_value('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
|
||||
vat=self._find_value('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
|
||||
)
|
||||
|
||||
# Delivery partner
|
||||
if 'partner_shipping_id' in invoice._fields:
|
||||
invoice_form.partner_shipping_id = self._retrieve_partner(
|
||||
name=self._find_value('//ram:ShipToTradeParty/ram:Name', tree, namespaces=tree.nsmap),
|
||||
mail=self._find_value('//ram:ShipToTradeParty//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
|
||||
vat=self._find_value('//ram:ShipToTradeParty/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
|
||||
)
|
||||
|
||||
# Reference.
|
||||
elements = tree.xpath('//rsm:ExchangedDocument/ram:ID', namespaces=tree.nsmap)
|
||||
|
||||
@@ -312,7 +312,11 @@ class ResPartner(models.Model):
|
||||
|
||||
vat = clean(vat, ' -.').upper().strip()
|
||||
|
||||
if not (len(vat) == 14):
|
||||
# Remove the prefix
|
||||
if vat.startswith("NL"):
|
||||
vat = vat[2:]
|
||||
|
||||
if not len(vat) == 12:
|
||||
return False
|
||||
|
||||
# Check the format
|
||||
@@ -325,9 +329,6 @@ class ResPartner(models.Model):
|
||||
char_to_int['+'] = '36'
|
||||
char_to_int['*'] = '37'
|
||||
|
||||
# Remove the prefix
|
||||
vat = vat[2:]
|
||||
|
||||
# 2 possible checks:
|
||||
# - For natural persons
|
||||
# - For non-natural persons and combinations of natural persons (company)
|
||||
|
||||
@@ -348,40 +348,24 @@ class Lead(models.Model):
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_email_from(self):
|
||||
for lead in self:
|
||||
if lead.partner_id.email and lead.partner_id.email != lead.email_from:
|
||||
if lead.partner_id.email and lead._get_partner_email_update():
|
||||
lead.email_from = lead.partner_id.email
|
||||
|
||||
def _inverse_email_from(self):
|
||||
for lead in self:
|
||||
if lead.partner_id and lead.email_from != lead.partner_id.email:
|
||||
# force reset
|
||||
if not lead.email_from or not lead.partner_id.email:
|
||||
lead.partner_id.email = lead.email_from
|
||||
# compare formatted values as we may have formatting differences between equivalent email
|
||||
else:
|
||||
lead_email_normalized = tools.email_normalize(lead.email_from)
|
||||
partner_email_normalized = tools.email_normalize(lead.partner_id.email)
|
||||
if lead_email_normalized != partner_email_normalized:
|
||||
lead.partner_id.email = lead.email_from
|
||||
if lead._get_partner_email_update():
|
||||
lead.partner_id.email = lead.email_from
|
||||
|
||||
@api.depends('partner_id.phone')
|
||||
def _compute_phone(self):
|
||||
for lead in self:
|
||||
if lead.partner_id.phone and lead.phone != lead.partner_id.phone:
|
||||
if lead.partner_id.phone and lead._get_partner_phone_update():
|
||||
lead.phone = lead.partner_id.phone
|
||||
|
||||
def _inverse_phone(self):
|
||||
for lead in self:
|
||||
if lead.partner_id and lead.phone != lead.partner_id.phone:
|
||||
# force reset
|
||||
if not lead.phone or not lead.partner_id.phone:
|
||||
lead.partner_id.phone = lead.phone
|
||||
# compare formatted values as we may have encoding differences between equivalent numbers
|
||||
else:
|
||||
lead_phone_formatted = lead.phone_format(lead.phone)
|
||||
partner_phone_formatted = lead.phone_format(lead.partner_id.phone)
|
||||
if lead_phone_formatted != partner_phone_formatted:
|
||||
lead.partner_id.phone = lead.phone
|
||||
if lead._get_partner_phone_update():
|
||||
lead.partner_id.phone = lead.phone
|
||||
|
||||
@api.depends('phone', 'country_id.code')
|
||||
def _compute_phone_state(self):
|
||||
@@ -454,21 +438,8 @@ class Lead(models.Model):
|
||||
@api.depends('email_from', 'phone', 'partner_id')
|
||||
def _compute_ribbon_message(self):
|
||||
for lead in self:
|
||||
# beware: void user input gives '' which is different from False
|
||||
lead_email_normalized = tools.email_normalize(lead.email_from) or (lead.email_from if lead.email_from else False)
|
||||
partner_email_normalized = tools.email_normalize(lead.partner_id.email) or lead.partner_id.email
|
||||
will_write_email = lead_email_normalized != partner_email_normalized if lead.partner_id else False
|
||||
will_write_phone = False
|
||||
if lead.partner_id and lead.phone != lead.partner_id.phone:
|
||||
# if reset -> obviously new value will be propagated
|
||||
if not lead.phone or not lead.partner_id.phone:
|
||||
will_write_phone = True
|
||||
# otherwise compare formatted values as we may have encoding differences
|
||||
else:
|
||||
lead_phone_formatted = lead.phone_format(lead.phone)
|
||||
partner_phone_formatted = lead.phone_format(lead.partner_id.phone)
|
||||
if lead_phone_formatted != partner_phone_formatted:
|
||||
will_write_phone = True
|
||||
will_write_email = lead._get_partner_email_update()
|
||||
will_write_phone = lead._get_partner_phone_update()
|
||||
|
||||
if will_write_email and will_write_phone:
|
||||
lead.ribbon_message = _('By saving this change, the customer email and phone number will also be updated.')
|
||||
@@ -550,6 +521,36 @@ class Lead(models.Model):
|
||||
partner_name = partner.name
|
||||
return {'partner_name': partner_name or self.partner_name}
|
||||
|
||||
def _get_partner_email_update(self):
|
||||
"""Calculate if we should write the email on the related partner. When
|
||||
the email of the lead / partner is an empty string, we force it to False
|
||||
to not propagate a False on an empty string.
|
||||
|
||||
Done in a separate method so it can be used in both ribbon and inverse
|
||||
and compute of email update methods.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.partner_id and self.email_from != self.partner_id.email:
|
||||
lead_email_normalized = tools.email_normalize(self.email_from) or self.email_from or False
|
||||
partner_email_normalized = tools.email_normalize(self.partner_id.email) or self.partner_id.email or False
|
||||
return lead_email_normalized != partner_email_normalized
|
||||
return False
|
||||
|
||||
def _get_partner_phone_update(self):
|
||||
"""Calculate if we should write the phone on the related partner. When
|
||||
the phone of the lead / partner is an empty string, we force it to False
|
||||
to not propagate a False on an empty string.
|
||||
|
||||
Done in a separate method so it can be used in both ribbon and inverse
|
||||
and compute of phone update methods.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.partner_id and self.phone != self.partner_id.phone:
|
||||
lead_phone_formatted = self.phone_format(self.phone) if self.phone else False or self.phone or False
|
||||
partner_phone_formatted = self.phone_format(self.partner_id.phone) if self.partner_id.phone else False or self.partner_id.phone or False
|
||||
return lead_phone_formatted != partner_phone_formatted
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -302,6 +302,62 @@ class TestCRMLead(TestCrmCommon):
|
||||
self.assertEqual(partner.phone_sanitized, partner_mobile_sanitized,
|
||||
'Partner sanitized should be computed on mobile')
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_crm_lead_partner_sync_email_phone_corner_cases(self):
|
||||
""" Test corner cases of email and phone sync (False versus '', formatting
|
||||
differences, wrong input, ...) """
|
||||
test_email = 'amy.wong@test.example.com'
|
||||
lead = self.lead_1.with_user(self.env.user)
|
||||
contact = self.env['res.partner'].create({
|
||||
'name': 'NoContact Partner',
|
||||
'phone': '',
|
||||
'email': '',
|
||||
'mobile': '',
|
||||
})
|
||||
|
||||
lead_form = Form(lead)
|
||||
self.assertEqual(lead_form.email_from, test_email)
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
|
||||
# email: False versus empty string
|
||||
lead_form.partner_id = contact
|
||||
self.assertIn('the customer email', lead_form.ribbon_message)
|
||||
lead_form.email_from = ''
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
lead_form.email_from = False
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
|
||||
# phone: False versus empty string
|
||||
lead_form.phone = '+1 202-555-0888'
|
||||
self.assertIn('the customer phone', lead_form.ribbon_message)
|
||||
lead_form.phone = ''
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
lead_form.phone = False
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
|
||||
# email/phone: formatting should not trigger ribbon
|
||||
lead.write({
|
||||
'email_from': '"My Name" <%s>' % test_email,
|
||||
'phone': '+1 202-555-0888',
|
||||
})
|
||||
contact.write({
|
||||
'email': '"My Name" <%s>' % test_email,
|
||||
'phone': '+1 202-555-0888',
|
||||
})
|
||||
|
||||
lead_form = Form(lead)
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
lead_form.partner_id = contact
|
||||
self.assertFalse(lead_form.ribbon_message)
|
||||
lead_form.email_from = '"Another Name" <%s>' % test_email # same email normalized
|
||||
self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
|
||||
lead_form.phone = '2025550888' # same number but another format
|
||||
self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
|
||||
|
||||
# wrong value are also propagated
|
||||
lead_form.phone = '666 789456789456789456'
|
||||
self.assertIn('the customer phone', lead_form.ribbon_message)
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_crm_lead_stages(self):
|
||||
lead = self.lead_1.with_user(self.env.user)
|
||||
|
||||
@@ -734,16 +734,16 @@
|
||||
to 0. but this approach is not working. the work around is temporary -->
|
||||
<xpath expr="//filter[@name='activities_overdue']" position="replace">
|
||||
<filter string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all opportunities for which the next action date is before today"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='activities_today']" position="replace">
|
||||
<filter string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='activities_upcoming_all']" position="replace">
|
||||
<filter string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='assigned_to_me']" position="replace">
|
||||
<filter string="My Activities" name="assigned_to_me"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
font-family: Arial, Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
#header_background {
|
||||
background-color: #2496f6;
|
||||
background-color: #875a7b;
|
||||
}
|
||||
.global_layout {
|
||||
max-width: 588px;
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
.button {
|
||||
float: right;
|
||||
background-color: #2496f6;
|
||||
background-color: #875a7b;
|
||||
color: #ffffff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -142,7 +142,7 @@
|
||||
}
|
||||
.flectra_link_text {
|
||||
font-weight: bold;
|
||||
color: #2496f6;
|
||||
color: #875a7b;
|
||||
}
|
||||
.run_business {
|
||||
color: #2d2a26;
|
||||
@@ -238,8 +238,8 @@
|
||||
}
|
||||
#header {
|
||||
padding: 20px 30px 25px 30px;
|
||||
border-left: 1px solid #2496f6;
|
||||
border-right: 1px solid #2496f6;
|
||||
border-left: 1px solid #875a7b;
|
||||
border-right: 1px solid #875a7b;
|
||||
}
|
||||
.global_layout {
|
||||
padding: 25px 30px 30px 30px;
|
||||
|
||||
@@ -436,12 +436,12 @@
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Responsible" name="responsible" context="{'group_by': 'user_id'}"/>
|
||||
<filter string="Template" name="event_type_id" context="{'group_by': 'event_type_id'}"/>
|
||||
@@ -705,12 +705,12 @@
|
||||
<field name="partner_id"/>
|
||||
<field name="company_id"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Partner" name="partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Event" name="group_event" domain="[]" context="{'group_by':'event_id'}"/>
|
||||
|
||||
@@ -145,12 +145,12 @@
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Vehicle" name="vehicle" context="{'group_by': 'vehicle_id'}"/>
|
||||
</group>
|
||||
|
||||
@@ -176,12 +176,12 @@
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="1" string="Group By">
|
||||
<filter string="Status" name="groupby_status" context="{'group_by': 'state_id'}"/>
|
||||
<filter string="Model" name="groupby_model" context="{'group_by': 'model_id'}"/>
|
||||
|
||||
@@ -158,7 +158,7 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
def _get_employee_working_now(self):
|
||||
working_now = []
|
||||
# We loop over all the employee tz and the resource calendar_id to detect working hours in batch.
|
||||
all_employee_tz = self.mapped('tz')
|
||||
all_employee_tz = set(self.mapped('tz'))
|
||||
for tz in all_employee_tz:
|
||||
employee_ids = self.filtered(lambda e: e.tz == tz)
|
||||
resource_calendar_ids = employee_ids.mapped('resource_calendar_id')
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
|
||||
@@ -115,12 +115,12 @@
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which have a next action date before today"/>
|
||||
<filter string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
<filter string="Job Position" name="job" domain="[]" context="{'group_by': 'job_id'}"/>
|
||||
|
||||
@@ -401,16 +401,10 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
|
||||
def _get_expense_account_destination(self):
|
||||
self.ensure_one()
|
||||
account_dest = self.env['account.account']
|
||||
if self.payment_mode == 'company_account':
|
||||
if not self.sheet_id.bank_journal_id.payment_credit_account_id:
|
||||
raise UserError(_("No Outstanding Payments Account found for the %s journal, please configure one.") % (self.sheet_id.bank_journal_id.name))
|
||||
account_dest = self.sheet_id.bank_journal_id.payment_credit_account_id.id
|
||||
else:
|
||||
if not self.employee_id.sudo().address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (self.employee_id.name))
|
||||
partner = self.employee_id.sudo().address_home_id.with_company(self.company_id)
|
||||
account_dest = partner.property_account_payable_id.id or partner.parent_id.property_account_payable_id.id
|
||||
if not self.employee_id.sudo().address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (self.employee_id.name))
|
||||
partner = self.employee_id.sudo().address_home_id.with_company(self.company_id)
|
||||
account_dest = partner.property_account_payable_id.id or partner.parent_id.property_account_payable_id.id
|
||||
return account_dest
|
||||
|
||||
def _get_account_move_line_values(self):
|
||||
@@ -524,8 +518,11 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
expense.sheet_id.paid_expense_sheets()
|
||||
|
||||
# post the moves
|
||||
for move in move_group_by_sheet.values():
|
||||
move._post()
|
||||
for expense in self:
|
||||
if not expense.payment_mode == 'company_account':
|
||||
for move in move_group_by_sheet[expense.sheet_id.id]:
|
||||
if move.state != 'posted':
|
||||
move._post()
|
||||
|
||||
return move_group_by_sheet
|
||||
|
||||
|
||||
@@ -310,12 +310,12 @@
|
||||
<filter string="Former Employees" name="inactive" domain="[('employee_id.active', '=', False)]" groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
|
||||
@@ -842,12 +842,12 @@
|
||||
<separator/>
|
||||
<filter domain="[('employee_id.active', '=', False)]" string="Former Employees" name="inactive" groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
|
||||
@@ -9,6 +9,15 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_line_batch_key(self, line):
|
||||
# OVERRIDE to set the bank account defined on the employee
|
||||
res = super()._get_line_batch_key(line)
|
||||
expense_sheet = self.env['hr.expense.sheet'].search([('payment_mode', '=', 'own_account'), ('account_move_id', 'in', line.move_id.ids)])
|
||||
if expense_sheet.employee_id.bank_account_id and not line.move_id.partner_bank_id:
|
||||
res['partner_bank_id'] = expense_sheet.employee_id.bank_account_id.id
|
||||
return res
|
||||
|
||||
def _create_payments(self):
|
||||
# OVERRIDE to set the 'done' state on expense sheets.
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
<field name="department_id" operator="child_of"/>
|
||||
<field name="holiday_status_id"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
|
||||
|
||||
@@ -70,12 +70,12 @@
|
||||
<filter name="year" string="Active Time Off"
|
||||
domain="[('holiday_status_id.active', '=', True)]" help="Active Time Off"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
By setting an alias to a job position, emails sent to this address create applications automatically. You can even use multiple trackers to get statistics according to the source of the application: LinkedIn, Monster, Indeed, etc.
|
||||
% set record = object.env['hr.job'].search([('alias_name', '!=', False)], limit=1)
|
||||
% if record and record.alias_domain
|
||||
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #2496f6; text-decoration: none;">Try sending an email</a>
|
||||
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #875a7b; text-decoration: none;">Try sending an email</a>
|
||||
% endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
% if 'website_url' in object.job_id and object.job_id.website_url:
|
||||
<div style="margin: 16px 8px 16px 8px;">
|
||||
<a href="${object.job_id.website_url}"
|
||||
style="background-color: #2496f6; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
style="background-color: #875a7b; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
% if 'website_url' in object.job_id and object.job_id.website_url:
|
||||
<div style="margin: 16px 8px 16px 8px;">
|
||||
<a href="${object.job_id.website_url}"
|
||||
style="background-color: #2496f6; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
style="background-color: #875a7b; text-decoration: none; color: #fff; padding: 8px 16px 8px 16px; border-radius: 5px;">Job Description</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
|
||||
@@ -212,12 +212,12 @@
|
||||
<filter string="Archived / Refused" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Responsible" name="responsible" domain="[]" context="{'group_by': 'user_id'}"/>
|
||||
|
||||
@@ -146,7 +146,7 @@ def get_mac_address():
|
||||
return netifaces.ifaddresses('wlan0')[netifaces.AF_LINK][0]['addr']
|
||||
|
||||
def get_ssid():
|
||||
ap = subprocess.call(['systemctl', 'is-active', 'hostapd']) # if service is active return 0 else inactive
|
||||
ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
|
||||
if not ap:
|
||||
return subprocess.check_output(['grep', '-oP', '(?<=ssid=).*', '/etc/hostapd/hostapd.conf']).decode('utf-8').rstrip()
|
||||
process_iwconfig = subprocess.Popen(['iwconfig'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
@@ -154,7 +154,7 @@ def get_ssid():
|
||||
return subprocess.check_output(['sed', 's/.*"\\(.*\\)"/\\1/'], stdin=process_grep.stdout).decode('utf-8').rstrip()
|
||||
|
||||
def get_flectra_server_url():
|
||||
ap = subprocess.call(['systemctl', 'is-active', 'hostapd']) # if service is active return 0 else inactive
|
||||
ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
|
||||
if not ap:
|
||||
return False
|
||||
return read_file_first_line('flectra-remote-server.conf')
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<data noupdate="1">
|
||||
<!-- add VAT, codice fiscal and tax system for main company -->
|
||||
<record id="l10n_it.demo_company_it" model="res.company">
|
||||
<field name="l10n_it_codice_fiscale">0123456789987654</field>
|
||||
<field name="l10n_it_codice_fiscale">09814700101</field>
|
||||
<field name="l10n_it_tax_system">RF01</field>
|
||||
<field name="zip">12345</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_demo_it" model="res.partner">
|
||||
<field name="name">Palazzo dell'Arte</field>
|
||||
<field name="vat">IT12345670124</field>
|
||||
<field name="vat">IT00000010215</field>
|
||||
<field name="street">Piazza Marconi 5</field>
|
||||
<field name="city">Cremona</field>
|
||||
<field name="country_id" ref="base.it"/>
|
||||
@@ -21,10 +21,12 @@
|
||||
</record>
|
||||
|
||||
<record id="base.res_partner_2" model="res.partner">
|
||||
<field name="vat">IT12345670231</field>
|
||||
<field name="vat">IT00079760328</field>
|
||||
<field name="l10n_it_pa_index">XS00001</field>
|
||||
</record>
|
||||
<record id="base.res_partner_12" model="res.partner">
|
||||
<field name="vat">IT12345670348</field>
|
||||
<field name="vat">IT00140390501</field>
|
||||
<field name="l10n_it_pa_index">XS00001</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<DatiTrasmissione>
|
||||
<IdTrasmittente>
|
||||
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
|
||||
<IdCodice t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<IdCodice t-esc="get_vat_number(record.company_id.vat)"/>
|
||||
</IdTrasmittente>
|
||||
<ProgressivoInvio t-esc="record.name.replace('/','')[-10:]"/>
|
||||
<FormatoTrasmissione t-esc="formato_trasmissione"/>
|
||||
@@ -72,7 +72,7 @@
|
||||
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
|
||||
<IdCodice t-esc="get_vat_number(record.company_id.vat)"/>
|
||||
</IdFiscaleIVA>
|
||||
<CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale and not test_mode" t-esc="record.company_id.l10n_it_codice_fiscale"/>
|
||||
<Anagrafica>
|
||||
<Denominazione t-esc="record.company_id.partner_id.display_name"/>
|
||||
</Anagrafica>
|
||||
|
||||
@@ -20,6 +20,96 @@ DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _l10n_it_edi_generate_electronic_invoice_filename(self, invoice):
|
||||
'''Returns a name conform to the Fattura pa Specifications:
|
||||
See ES documentation 2.2
|
||||
'''
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
n = invoice.id
|
||||
progressive_number = ""
|
||||
while n:
|
||||
(n, m) = divmod(n, len(a))
|
||||
progressive_number = a[m] + progressive_number
|
||||
|
||||
return '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
|
||||
'country_code': invoice.company_id.country_id.code,
|
||||
'codice': invoice.company_id.l10n_it_codice_fiscale.replace(' ', ''),
|
||||
'progressive_number': progressive_number.zfill(5),
|
||||
}
|
||||
|
||||
def _l10n_it_edi_check_invoice_configuration(self, invoice):
|
||||
errors = []
|
||||
seller = invoice.company_id
|
||||
buyer = invoice.commercial_partner_id
|
||||
|
||||
# <1.1.1.1>
|
||||
if not seller.country_id:
|
||||
errors.append(_("%s must have a country", seller.display_name))
|
||||
|
||||
# <1.1.1.2>
|
||||
if not seller.vat:
|
||||
errors.append(_("%s must have a VAT number", seller.display_name))
|
||||
elif len(seller.vat) > 30:
|
||||
errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat))
|
||||
|
||||
# <1.2.1.2>
|
||||
if not seller.l10n_it_codice_fiscale:
|
||||
errors.append(_("%s must have a codice fiscale number", seller.display_name))
|
||||
|
||||
# <1.2.1.8>
|
||||
if not seller.l10n_it_tax_system:
|
||||
errors.append(_("The seller's company must have a tax system."))
|
||||
|
||||
# <1.2.2>
|
||||
if not seller.street and not seller.street2:
|
||||
errors.append(_("%s must have a street.", seller.display_name))
|
||||
if not seller.zip:
|
||||
errors.append(_("%s must have a post code.", seller.display_name))
|
||||
elif len(seller.zip) != 5 and seller.country_id.code == 'IT':
|
||||
errors.append(_("%s must have a post code of length 5.", seller.display_name))
|
||||
if not seller.city:
|
||||
errors.append(_("%s must have a city.", seller.display_name))
|
||||
if not seller.country_id:
|
||||
errors.append(_("%s must have a country.", seller.display_name))
|
||||
|
||||
if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
|
||||
errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
|
||||
|
||||
# <1.4.1>
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
|
||||
errors.append(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).", buyer.display_name))
|
||||
|
||||
# <1.4.2>
|
||||
if not buyer.street and not buyer.street2:
|
||||
errors.append(_("%s must have a street.", buyer.display_name))
|
||||
if not buyer.zip:
|
||||
errors.append(_("%s must have a post code.", buyer.display_name))
|
||||
if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
|
||||
errors.append(_("%s must have a post code of length 5.", buyer.display_name))
|
||||
if not buyer.city:
|
||||
errors.append(_("%s must have a city.", buyer.display_name))
|
||||
if not buyer.country_id:
|
||||
errors.append(_("%s must have a country.", buyer.display_name))
|
||||
|
||||
# <2.2.1>
|
||||
for invoice_line in invoice.invoice_line_ids:
|
||||
if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
|
||||
raise UserError(_("You must select one and only one tax by line."))
|
||||
|
||||
for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
|
||||
errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
|
||||
|
||||
if not invoice.partner_bank_id:
|
||||
errors.append(_("The seller must have a bank account."))
|
||||
|
||||
return errors
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Export
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -36,25 +126,30 @@ class AccountEdiFormat(models.Model):
|
||||
return super()._is_compatible_with_journal(journal)
|
||||
return journal.type == 'sale' and journal.country_code == 'IT'
|
||||
|
||||
def _l10n_it_edi_is_required_for_invoice(self, invoice):
|
||||
""" Is the edi required for this invoice based on the method (here: PEC mail)
|
||||
Deprecated: in future release PEC mail will be removed.
|
||||
TO OVERRIDE
|
||||
"""
|
||||
return invoice.is_sale_document() and invoice.l10n_it_send_state not in ('sent', 'delivered', 'delivered_accepted') and invoice.country_code == 'IT'
|
||||
|
||||
def _is_required_for_invoice(self, invoice):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
if self.code != 'fattura_pa':
|
||||
return super()._is_required_for_invoice(invoice)
|
||||
|
||||
# Determine on which invoices the Mexican CFDI must be generated.
|
||||
return invoice.is_sale_document() and invoice.l10n_it_send_state not in ['sent', 'delivered', 'delivered_accepted'] and invoice.country_code == 'IT'
|
||||
|
||||
def _post_invoice_edi(self, invoices, test_mode=False):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
edi_result = super()._post_invoice_edi(invoices, test_mode=test_mode)
|
||||
if self.code != 'fattura_pa':
|
||||
return edi_result
|
||||
return self._l10n_it_edi_is_required_for_invoice(invoice)
|
||||
|
||||
def _post_fattura_pa(self, invoices):
|
||||
# TO OVERRIDE
|
||||
invoice = invoices # no batching ensure that we only have one invoice
|
||||
invoice.l10n_it_send_state = 'other'
|
||||
invoice._check_before_xml_exporting()
|
||||
if invoice.l10n_it_einvoice_id and invoice.l10n_it_send_state not in ['invalid', 'to_send']:
|
||||
return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
|
||||
if invoice.l10n_it_einvoice_id:
|
||||
invoice.l10n_it_einvoice_id.unlink()
|
||||
res = invoice.invoice_generate_xml()
|
||||
if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6:
|
||||
invoice.message_post(
|
||||
@@ -64,6 +159,15 @@ class AccountEdiFormat(models.Model):
|
||||
invoice.l10n_it_send_state = 'to_send'
|
||||
return {invoice: res}
|
||||
|
||||
def _post_invoice_edi(self, invoices, test_mode=False):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
edi_result = super()._post_invoice_edi(invoices)
|
||||
if self.code != 'fattura_pa':
|
||||
return edi_result
|
||||
|
||||
return self._post_fattura_pa(invoices)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Import
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -109,6 +213,18 @@ class AccountEdiFormat(models.Model):
|
||||
invoice = self.env['account.move']
|
||||
first_run = False
|
||||
|
||||
# Type must be present in the context to get the right behavior of the _default_journal method (account.move).
|
||||
# journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
|
||||
elements = tree.xpath('//CessionarioCommittente//IdCodice')
|
||||
company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
|
||||
if not company:
|
||||
elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
|
||||
company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
|
||||
if not company:
|
||||
# Only invoices with a correct VAT or Codice Fiscale can be imported
|
||||
_logger.warning('No company found with VAT or Codice Fiscale like %r.', elements[0].text)
|
||||
continue
|
||||
|
||||
# Refund type.
|
||||
# TD01 == invoice
|
||||
# TD02 == advance/down payment on invoice
|
||||
@@ -123,32 +239,14 @@ class AccountEdiFormat(models.Model):
|
||||
move_type = 'in_refund'
|
||||
elif elements and elements[0].text and elements[0].text != 'TD01':
|
||||
_logger.info('Document type not managed: %s. Invoice type is set by default.', elements[0].text)
|
||||
invoice_ctx = invoice.with_context(default_move_type=move_type)
|
||||
|
||||
# type must be present in the context to get the right behavior of the _default_journal method (account.move).
|
||||
# journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
|
||||
|
||||
elements = tree.xpath('//CessionarioCommittente//IdCodice')
|
||||
company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
|
||||
if not company:
|
||||
elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
|
||||
company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
|
||||
|
||||
if company:
|
||||
invoice_ctx = invoice_ctx.with_context(company_id=company.id)
|
||||
else:
|
||||
company = self.env.company
|
||||
if elements:
|
||||
_logger.info('No company found with codice fiscale: %s. The user\'s company is set by default.', elements[0].text)
|
||||
else:
|
||||
_logger.info('Company not found. The user\'s company is set by default.')
|
||||
|
||||
if not self.env.is_superuser():
|
||||
if self.env.company != company:
|
||||
raise UserError(_("You can only import invoice concern your current company: %s", self.env.company.display_name))
|
||||
# Setup the context for the Invoice Form
|
||||
invoice_ctx = invoice.with_company(company) \
|
||||
.with_context(default_move_type=move_type,
|
||||
account_predictive_bills_disable_prediction=True)
|
||||
|
||||
# move could be a single record (editing) or be empty (new).
|
||||
with Form(invoice_ctx.with_context(account_predictive_bills_disable_prediction=True)) as invoice_form:
|
||||
with Form(invoice_ctx) as invoice_form:
|
||||
message_to_log = []
|
||||
|
||||
# Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
|
||||
@@ -190,7 +288,7 @@ class AccountEdiFormat(models.Model):
|
||||
if elements:
|
||||
date_str = elements[0].text
|
||||
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date = date_obj
|
||||
|
||||
# Dati Bollo. <2.1.1.6>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
|
||||
@@ -295,7 +393,7 @@ class AccountEdiFormat(models.Model):
|
||||
if elements:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Bank account not found, useful informations from XML file:"),
|
||||
self._compose_info_message(body_tree, './/DatiPagamento')))
|
||||
invoice._compose_info_message(body_tree, './/DatiPagamento')))
|
||||
|
||||
# Invoice lines. <2.2.1>
|
||||
elements = body_tree.xpath('.//DettaglioLinee')
|
||||
|
||||
@@ -56,87 +56,14 @@ class AccountMove(models.Model):
|
||||
invoice.l10n_it_einvoice_name = einvoice.attachment_id.name
|
||||
|
||||
def _check_before_xml_exporting(self):
|
||||
self.ensure_one()
|
||||
seller = self.company_id
|
||||
buyer = self.commercial_partner_id
|
||||
|
||||
# <1.1.1.1>
|
||||
if not seller.country_id:
|
||||
raise UserError(_("%s must have a country") % (seller.display_name))
|
||||
|
||||
# <1.1.1.2>
|
||||
if not seller.vat:
|
||||
raise UserError(_("%s must have a VAT number") % (seller.display_name))
|
||||
elif len(seller.vat) > 30:
|
||||
raise UserError(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.") % (seller.display_name, seller.vat))
|
||||
|
||||
# <1.2.1.2>
|
||||
if not seller.l10n_it_codice_fiscale:
|
||||
raise UserError(_("%s must have a codice fiscale number") % (seller.display_name))
|
||||
|
||||
# <1.2.1.8>
|
||||
if not seller.l10n_it_tax_system:
|
||||
raise UserError(_("The seller's company must have a tax system."))
|
||||
|
||||
# <1.2.2>
|
||||
if not seller.street and not seller.street2:
|
||||
raise UserError(_("%s must have a street.") % (seller.display_name))
|
||||
if not seller.zip:
|
||||
raise UserError(_("%s must have a post code.") % (seller.display_name))
|
||||
if len(seller.zip) != 5 and seller.country_id.code == 'IT':
|
||||
raise UserError(_("%s must have a post code of length 5.") % (seller.display_name))
|
||||
if not seller.city:
|
||||
raise UserError(_("%s must have a city.") % (seller.display_name))
|
||||
if not seller.country_id:
|
||||
raise UserError(_("%s must have a country.") % (seller.display_name))
|
||||
|
||||
if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
|
||||
raise UserError(_("Tax representative partner %s of %s must have a tax number.") % (seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
|
||||
|
||||
# <1.4.1>
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
|
||||
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
|
||||
|
||||
# <1.4.2>
|
||||
if not buyer.street and not buyer.street2:
|
||||
raise UserError(_("%s must have a street.") % (buyer.display_name))
|
||||
if not buyer.zip:
|
||||
raise UserError(_("%s must have a post code.") % (buyer.display_name))
|
||||
if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
|
||||
raise UserError(_("%s must have a post code of length 5.") % (buyer.display_name))
|
||||
if not buyer.city:
|
||||
raise UserError(_("%s must have a city.") % (buyer.display_name))
|
||||
if not buyer.country_id:
|
||||
raise UserError(_("%s must have a country.") % (buyer.display_name))
|
||||
|
||||
# <2.2.1>
|
||||
for invoice_line in self.invoice_line_ids:
|
||||
if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
|
||||
raise UserError(_("You must select one and only one tax by line."))
|
||||
|
||||
for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_has_exoneration and tax_line.tax_line_id.amount == 0:
|
||||
raise ValidationError(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
|
||||
# DEPRECATED use AccountEdiFormat._l10n_it_edi_check_invoice_configuration instead
|
||||
errors = self.env['account.edi.format']._l10n_it_edi_check_invoice_configuration(self)
|
||||
if errors:
|
||||
raise UserError(self.env['account.edi.format']._format_error_message(_("Invalid configuration:"), errors))
|
||||
|
||||
def invoice_generate_xml(self):
|
||||
self.ensure_one()
|
||||
if self.l10n_it_einvoice_id and self.l10n_it_send_state not in ['invalid', 'to_send']:
|
||||
return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
|
||||
if self.l10n_it_einvoice_id:
|
||||
self.l10n_it_einvoice_id.unlink()
|
||||
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
n = self.id
|
||||
progressive_number = ""
|
||||
while n:
|
||||
(n,m) = divmod(n,len(a))
|
||||
progressive_number = a[m] + progressive_number
|
||||
|
||||
report_name = '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
|
||||
'country_code': self.company_id.country_id.code,
|
||||
'codice': self.company_id.l10n_it_codice_fiscale,
|
||||
'progressive_number': progressive_number.zfill(5),
|
||||
}
|
||||
report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self)
|
||||
|
||||
data = b"<?xml version='1.0' encoding='UTF-8'?>" + self._export_as_xml()
|
||||
description = _('Italian invoice: %s', self.move_type)
|
||||
@@ -155,9 +82,6 @@ class AccountMove(models.Model):
|
||||
return {'attachment': attachment}
|
||||
|
||||
def _prepare_fatturapa_export_values(self):
|
||||
''' Create the xml file content.
|
||||
:return: The XML content as str.
|
||||
'''
|
||||
self.ensure_one()
|
||||
|
||||
def format_date(dt):
|
||||
@@ -252,6 +176,10 @@ class AccountMove(models.Model):
|
||||
return template_values
|
||||
|
||||
def _export_as_xml(self):
|
||||
'''DEPRECATED : this will be moved to AccountEdiFormat in a future version.
|
||||
Create the xml file content.
|
||||
:return: The XML content as str.
|
||||
'''
|
||||
template_values = self._prepare_fatturapa_export_values()
|
||||
content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export')._render(template_values)
|
||||
return content
|
||||
|
||||
@@ -135,7 +135,8 @@ class FetchmailServer(models.Model):
|
||||
# we have a receipt
|
||||
self._message_receipt_invoice(split_underscore[1], attachment)
|
||||
else:
|
||||
match = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname)
|
||||
att_filename = attachment.fname
|
||||
match = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", att_filename)
|
||||
# If match, we have an invoice.
|
||||
if match:
|
||||
# If it's signed, the content has a bytes type and we just remove the signature's envelope
|
||||
@@ -143,12 +144,13 @@ class FetchmailServer(models.Model):
|
||||
att_content_data = remove_signature(attachment.content)
|
||||
# If the envelope cannot be removed, the remove_signature returns None, so we skip
|
||||
if not att_content_data:
|
||||
_logger.warning("E-invoice couldn't be read: %s", attachment.fname)
|
||||
_logger.warning("E-invoice couldn't be read: %s", att_filename)
|
||||
continue
|
||||
# Otherwise, it should be an utf-8 encoded XML string
|
||||
att_filename = att_filename.replace('.xml.p7m', '.xml')
|
||||
else:
|
||||
# Otherwise, it should be an utf-8 encoded XML string
|
||||
att_content_data = attachment.content.encode()
|
||||
self._create_invoice_from_mail(att_content_data, attachment.fname, from_address)
|
||||
self._create_invoice_from_mail(att_content_data, att_filename, from_address)
|
||||
else:
|
||||
if split_underscore[1] == 'AT':
|
||||
# Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito
|
||||
@@ -174,7 +176,7 @@ class FetchmailServer(models.Model):
|
||||
return invoices
|
||||
|
||||
# Create the new attachment for the file
|
||||
self.env['ir.attachment'].create({
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': att_name,
|
||||
'raw': att_content_data,
|
||||
'res_model': 'account.move',
|
||||
@@ -187,14 +189,16 @@ class FetchmailServer(models.Model):
|
||||
_logger.info('The xml file is badly formatted: %s', att_name)
|
||||
return invoices
|
||||
|
||||
# Create the invoice
|
||||
invoices = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
|
||||
if not invoices:
|
||||
_logger.info('E-invoice not found in file: %s', att_name)
|
||||
return invoices
|
||||
|
||||
invoices.l10n_it_send_state = 'new'
|
||||
invoices.l10n_it_send_state = "new"
|
||||
invoices.invoice_source_email = from_address
|
||||
for invoice in invoices:
|
||||
invoice.with_context(no_new_invoice=True, default_res_id=invoice.id) \
|
||||
.message_post(body=(_("Original E-invoice XML file")), attachment_ids=[attachment.id])
|
||||
|
||||
self._cr.commit()
|
||||
|
||||
_logger.info('New E-invoices (%s), ids: %s', att_name, [x.id for x in invoices])
|
||||
@@ -403,8 +407,10 @@ class FetchmailServer(models.Model):
|
||||
class IrMailServer(models.Model):
|
||||
_name = "ir.mail_server"
|
||||
_inherit = "ir.mail_server"
|
||||
|
||||
def _get_test_email_addresses(self):
|
||||
self.ensure_one()
|
||||
|
||||
company = self.env["res.company"].search([("l10n_it_mail_pec_server_id", "=", self.id)], limit=1)
|
||||
if not company:
|
||||
# it's not a PEC server
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from unittest.mock import patch
|
||||
import freezegun
|
||||
|
||||
from flectra import tools
|
||||
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
|
||||
@@ -44,8 +45,10 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic',
|
||||
edi_format_ref='l10n_it_edi.edi_fatturaPA')
|
||||
|
||||
# Use the company_data_2 to test that the e-invoice is imported for the right company
|
||||
cls.company = cls.company_data_2['company']
|
||||
|
||||
# Initialize the company's codice fiscale
|
||||
cls.company = cls.company_data['company']
|
||||
cls.company.l10n_it_codice_fiscale = 'IT01234560157'
|
||||
|
||||
# Build test data.
|
||||
@@ -106,12 +109,14 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
|
||||
def test_receive_signed_vendor_bill(self):
|
||||
""" Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
|
||||
invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
|
||||
self.assertRecordValues(invoices, [{
|
||||
'name': 'BILL/2014/12/0001',
|
||||
'date': datetime.date(2014, 12, 18),
|
||||
'ref': '01234567890',
|
||||
}])
|
||||
with freezegun.freeze_time('2020-04-06'):
|
||||
invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
|
||||
self.assertRecordValues(invoices, [{
|
||||
'company_id': self.company.id,
|
||||
'name': 'BILL/2020/04/0001',
|
||||
'invoice_date': datetime.date(2014, 12, 18),
|
||||
'ref': '01234567890',
|
||||
}])
|
||||
|
||||
def test_receive_same_vendor_bill_twice(self):
|
||||
""" Test that the second time we are receiving a PEC mail with the same attachment, the second is discarded """
|
||||
|
||||
@@ -85,8 +85,11 @@ class LinkTracker(models.Model):
|
||||
for tracker in self:
|
||||
parsed = urls.url_parse(tracker.url)
|
||||
utms = {}
|
||||
for key, field, cook in self.env['utm.mixin'].tracking_fields():
|
||||
attr = getattr(tracker, field).name
|
||||
for key, field_name, cook in self.env['utm.mixin'].tracking_fields():
|
||||
field = self._fields[field_name]
|
||||
attr = getattr(tracker, field_name)
|
||||
if field.type == 'many2one':
|
||||
attr = attr.name
|
||||
if attr:
|
||||
utms[key] = attr
|
||||
utms.update(parsed.decode_query())
|
||||
|
||||
@@ -688,6 +688,10 @@ class MailActivityMixin(models.AbstractModel):
|
||||
compute='_compute_activity_date_deadline', search='_search_activity_date_deadline',
|
||||
compute_sudo=False, readonly=True, store=False,
|
||||
groups="base.group_user")
|
||||
my_activity_date_deadline = fields.Date(
|
||||
'My Activity Deadline',
|
||||
compute='_compute_my_activity_date_deadline', search='_search_my_activity_date_deadline',
|
||||
compute_sudo=False, readonly=True, groups="base.group_user")
|
||||
activity_summary = fields.Char(
|
||||
'Next Activity Summary',
|
||||
related='activity_ids.summary', readonly=False,
|
||||
@@ -757,6 +761,24 @@ class MailActivityMixin(models.AbstractModel):
|
||||
def _search_activity_summary(self, operator, operand):
|
||||
return [('activity_ids.summary', operator, operand)]
|
||||
|
||||
@api.depends('activity_ids.date_deadline', 'activity_ids.user_id')
|
||||
@api.depends_context('uid')
|
||||
def _compute_my_activity_date_deadline(self):
|
||||
for record in self:
|
||||
record.my_activity_date_deadline = next((
|
||||
activity.date_deadline
|
||||
for activity in record.activity_ids
|
||||
if activity.user_id.id == record.env.uid
|
||||
), False)
|
||||
|
||||
def _search_my_activity_date_deadline(self, operator, operand):
|
||||
activity_ids = self.env['mail.activity']._search([
|
||||
('date_deadline', operator, operand),
|
||||
('res_model', '=', self._name),
|
||||
('user_id', '=', self.env.user.id)
|
||||
])
|
||||
return [('activity_ids', 'in', activity_ids)]
|
||||
|
||||
def write(self, vals):
|
||||
# Delete activities of archived record.
|
||||
if 'active' in vals and vals['active'] is False:
|
||||
|
||||
@@ -1502,6 +1502,77 @@ QUnit.test('rendering of tracked field of type many2one: from no related record
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('message should not be considered as "clicked" after clicking on its author name', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await this.start();
|
||||
const message = this.env.models['mail.message'].create({
|
||||
author: [['insert', { id: 7, display_name: "Demo User" }]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
});
|
||||
await this.createMessageComponent(message);
|
||||
document.querySelector(`.o_Message_authorName`).click();
|
||||
await nextAnimationFrame();
|
||||
assert.doesNotHaveClass(
|
||||
document.querySelector(`.o_Message`),
|
||||
'o-clicked',
|
||||
"message should not be considered as 'clicked' after clicking on its author name"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('message should not be considered as "clicked" after clicking on its author avatar', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await this.start();
|
||||
const message = this.env.models['mail.message'].create({
|
||||
author: [['insert', { id: 7, display_name: "Demo User" }]],
|
||||
body: "<p>Test</p>",
|
||||
id: 100,
|
||||
});
|
||||
await this.createMessageComponent(message);
|
||||
document.querySelector(`.o_Message_authorAvatar`).click();
|
||||
await nextAnimationFrame();
|
||||
assert.doesNotHaveClass(
|
||||
document.querySelector(`.o_Message`),
|
||||
'o-clicked',
|
||||
"message should not be considered as 'clicked' after clicking on its author avatar"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('message should not be considered as "clicked" after clicking on notification failure icon', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await this.start();
|
||||
const threadViewer = this.env.models['mail.thread_viewer'].create({
|
||||
hasThreadView: true,
|
||||
thread: [['create', {
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
}]],
|
||||
});
|
||||
const message = this.env.models['mail.message'].create({
|
||||
id: 10,
|
||||
message_type: 'email',
|
||||
notifications: [['insert', {
|
||||
id: 11,
|
||||
notification_status: 'exception',
|
||||
notification_type: 'email',
|
||||
}]],
|
||||
originThread: [['link', threadViewer.thread]],
|
||||
});
|
||||
await this.createMessageComponent(message, {
|
||||
threadViewLocalId: threadViewer.threadView.localId
|
||||
});
|
||||
document.querySelector('.o_Message_notificationIconClickable.o-error').click();
|
||||
await nextAnimationFrame();
|
||||
assert.doesNotHaveClass(
|
||||
document.querySelector(`.o_Message`),
|
||||
'o-clicked',
|
||||
"message should not be considered as 'clicked' after clicking on notification failure icon"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<filter name="inactive" position="after">
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
</filter>
|
||||
</field>
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('archive', '=', True)]"/>
|
||||
<group expand='0' string='Group by...'>
|
||||
@@ -518,12 +518,12 @@
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group expand='0' string='Group by...'>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<tr>
|
||||
<td style="margin: 0; padding:0;">
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
|
||||
<tr style="color: #2496f6; font-size: 16px; font-weight: 500; border-bottom: 1px solid #e7e7e7;">
|
||||
<tr style="color: #875a7b; font-size: 16px; font-weight: 500; border-bottom: 1px solid #e7e7e7;">
|
||||
<td style="width: 70%;padding: 10px 0; text-align: left;">Label</td>
|
||||
<td style="width: 30%;padding: 10px 0; text-align: right;">%Click (Total)</td>
|
||||
</tr>
|
||||
|
||||
@@ -1715,7 +1715,9 @@ class MrpProduction(models.Model):
|
||||
])
|
||||
if duplicates:
|
||||
# Maybe some move lines have been compensated by unbuild
|
||||
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
|
||||
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [
|
||||
('move_id.unbuild_id', '!=', False)
|
||||
])
|
||||
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
|
||||
raise UserError(message)
|
||||
# Check presence of same sn in current production
|
||||
@@ -1749,7 +1751,9 @@ class MrpProduction(models.Model):
|
||||
])
|
||||
if duplicates:
|
||||
# Maybe some move lines have been compensated by unbuild
|
||||
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
|
||||
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [
|
||||
('move_id.unbuild_id', '!=', False)
|
||||
])
|
||||
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
|
||||
raise UserError(message)
|
||||
# Check presence of same sn in current production
|
||||
|
||||
@@ -133,7 +133,7 @@ class StockMove(models.Model):
|
||||
operation_domain = [
|
||||
('id', 'in', move.raw_material_production_id.bom_id.operation_ids.ids),
|
||||
'|',
|
||||
('company_id', '=', self.company_id.id),
|
||||
('company_id', '=', move.company_id.id),
|
||||
('company_id', '=', False)
|
||||
]
|
||||
move.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
|
||||
@@ -359,12 +359,14 @@ class StockMove(models.Model):
|
||||
def _prepare_merge_moves_distinct_fields(self):
|
||||
distinct_fields = super()._prepare_merge_moves_distinct_fields()
|
||||
distinct_fields.append('created_production_id')
|
||||
distinct_fields.append('bom_line_id')
|
||||
return distinct_fields
|
||||
|
||||
@api.model
|
||||
def _prepare_merge_move_sort_method(self, move):
|
||||
keys_sorted = super()._prepare_merge_move_sort_method(move)
|
||||
keys_sorted.append(move.created_production_id.id)
|
||||
keys_sorted.append(move.bom_line_id.id)
|
||||
return keys_sorted
|
||||
|
||||
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
|
||||
|
||||
@@ -123,11 +123,11 @@
|
||||
<page string="Miscellaneous" name="miscellaneous">
|
||||
<group>
|
||||
<group>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','!=','normal')]}" widget="radio"/>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','=','phantom')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','=','phantom')]}" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="picking_type_id" attrs="{'invisible': [('type','!=','normal')]}" string="Operation" groups="stock.group_adv_location"/>
|
||||
<field name="picking_type_id" attrs="{'invisible': [('type','=','phantom')]}" string="Operation" groups="stock.group_adv_location"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
@@ -483,12 +483,12 @@
|
||||
domain="['|', ('delay_alert_date', '!=', False), '&', ('date_deadline', '<', current_date), ('state', '=', 'confirmed')]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter name="filter_date_planned_start" string="Scheduled Date" date="date_planned_start"/>
|
||||
<separator/>
|
||||
<filter string="Warnings" name="activities_exception"
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="done" string="Done" domain="[('state', '=', 'done')]"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
</group>
|
||||
<group expand='0' string='Group by...'>
|
||||
<filter string='Product' name="productgroup" context="{'group_by': 'product_id'}"/>
|
||||
|
||||
@@ -54,7 +54,7 @@ class ProductProduct(models.Model):
|
||||
bom_line = move.bom_line_id
|
||||
bom_line_data = bom_lines[bom_line]
|
||||
bom_line_qty = bom_line_data['qty']
|
||||
value += move.product_id._compute_average_price(qty_invoiced * bom_line_qty, qty_to_invoice * bom_line_qty, move)
|
||||
value += bom_line_qty * move.product_id._compute_average_price(qty_invoiced * bom_line_qty, qty_to_invoice * bom_line_qty, move)
|
||||
return value
|
||||
|
||||
def _compute_bom_price(self, bom, boms_to_recompute=False):
|
||||
|
||||
@@ -22,3 +22,8 @@ class StockMove(models.Model):
|
||||
return super()._get_dest_account(accounts_data)
|
||||
else:
|
||||
return self.location_id.valuation_in_account_id.id or accounts_data['stock_output'].id
|
||||
|
||||
def _filter_anglo_saxon_moves(self, product):
|
||||
res = super(StockMove, self)._filter_anglo_saxon_moves(product)
|
||||
res += self.filtered(lambda m: m.bom_line_id.bom_id.product_tmpl_id.id == product.product_tmpl_id.id)
|
||||
return res
|
||||
@@ -179,12 +179,12 @@
|
||||
<filter name="open_true" string="Active" domain="[('open', '=', True)]"/>
|
||||
<filter name="open_false" string="Archive" domain="[('open', '=', False)]"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Stage" name="stage" help="By sticky note Category" context="{'group_by':'stage_id'}"/>
|
||||
|
||||
@@ -23,7 +23,7 @@ class OgoneController(http.Controller):
|
||||
'/payment/ogone/decline', '/payment/ogone/test/decline',
|
||||
'/payment/ogone/exception', '/payment/ogone/test/exception',
|
||||
'/payment/ogone/cancel', '/payment/ogone/test/cancel',
|
||||
], type='http', auth='public', csrf=False, method=['GET', 'POST'])
|
||||
], type='http', auth='public', csrf=False)
|
||||
def ogone_form_feedback(self, **post):
|
||||
""" Handle both redirection from Ingenico (GET) and s2s notification (POST/GET) """
|
||||
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
|
||||
@@ -57,7 +57,9 @@ class PosController(http.Controller):
|
||||
'session_info': session_info,
|
||||
'login_number': pos_session.login(),
|
||||
}
|
||||
return request.render('point_of_sale.index', qcontext=context)
|
||||
response = request.render('point_of_sale.index', context)
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
|
||||
@http.route('/pos/ui/tests', type='http', auth="user")
|
||||
def test_suite(self, mod=None, **kwargs):
|
||||
|
||||
@@ -198,7 +198,7 @@ class PosOrder(models.Model):
|
||||
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
|
||||
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
|
||||
.mapped('picking_ids.move_lines')\
|
||||
.filtered(lambda m: m.product_id.id == product.id)\
|
||||
._filter_anglo_saxon_moves(product)\
|
||||
.sorted(lambda x: x.date)
|
||||
price_unit = product._compute_average_price(0, quantity, moves)
|
||||
return price_unit
|
||||
|
||||
@@ -259,6 +259,8 @@ class PosSession(models.Model):
|
||||
def action_pos_session_closing_control(self):
|
||||
self._check_pos_session_balance()
|
||||
for session in self:
|
||||
if any(order.state == 'draft' for order in session.order_ids):
|
||||
raise UserError(_("You cannot close the POS when orders are still in draft"))
|
||||
if session.state == 'closed':
|
||||
raise UserError(_('This session is already closed.'))
|
||||
session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()})
|
||||
@@ -296,6 +298,7 @@ class PosSession(models.Model):
|
||||
|
||||
def _validate_session(self):
|
||||
self.ensure_one()
|
||||
sudo = self.user_has_groups('point_of_sale.group_pos_user')
|
||||
if self.order_ids or self.statement_ids.line_ids:
|
||||
self.cash_real_transaction = self.cash_register_total_entry_encoding
|
||||
self.cash_real_expected = self.cash_register_balance_end
|
||||
@@ -310,7 +313,7 @@ class PosSession(models.Model):
|
||||
try:
|
||||
self.with_company(self.company_id)._create_account_move()
|
||||
except AccessError as e:
|
||||
if self.user_has_groups('point_of_sale.group_pos_user'):
|
||||
if sudo:
|
||||
self.sudo().with_company(self.company_id)._create_account_move()
|
||||
else:
|
||||
raise e
|
||||
@@ -319,6 +322,10 @@ class PosSession(models.Model):
|
||||
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
|
||||
else:
|
||||
self.move_id.unlink()
|
||||
elif not self.cash_register_id.difference:
|
||||
cash_register = self.cash_register_id.sudo() if sudo else self.cash_register_id
|
||||
cash_register.pos_session_id = False
|
||||
cash_register.unlink()
|
||||
self.write({'state': 'closed'})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
@@ -657,12 +664,13 @@ class PosSession(models.Model):
|
||||
invoice_receivable_vals[commercial_partner].append(self._get_invoice_receivable_vals(account_id, amounts['amount'], amounts['amount_converted'], partner=commercial_partner))
|
||||
for commercial_partner, vals in invoice_receivable_vals.items():
|
||||
account_id = commercial_partner.property_account_receivable_id.id
|
||||
receivable_line = MoveLine.create(vals)
|
||||
if (not receivable_line.reconciled):
|
||||
if account_id not in invoice_receivable_lines:
|
||||
invoice_receivable_lines[account_id] = receivable_line
|
||||
else:
|
||||
invoice_receivable_lines[account_id] |= receivable_line
|
||||
receivable_lines = MoveLine.create(vals)
|
||||
for receivable_line in receivable_lines:
|
||||
if (not receivable_line.reconciled):
|
||||
if account_id not in invoice_receivable_lines:
|
||||
invoice_receivable_lines[account_id] = receivable_line
|
||||
else:
|
||||
invoice_receivable_lines[account_id] |= receivable_line
|
||||
|
||||
data.update({'invoice_receivable_lines': invoice_receivable_lines})
|
||||
return data
|
||||
|
||||
@@ -117,7 +117,7 @@ class StockPicking(models.Model):
|
||||
'product_id': line.product_id.id,
|
||||
'name': lot.lot_name,
|
||||
})
|
||||
quant = existing_lot.quant_ids.filtered(lambda q: q.quantity > 0.0 or q.location_id.parent_path.startswith(move.location_id.parent_path))[-1:]
|
||||
quant = existing_lot.quant_ids.filtered(lambda q: q.quantity > 0.0 and q.location_id.parent_path.startswith(move.location_id.parent_path))[-1:]
|
||||
ml_vals.update({
|
||||
'lot_id': existing_lot.id,
|
||||
'location_id': quant.location_id.id or move.location_id.id
|
||||
|
||||
@@ -1696,6 +1696,9 @@ td {
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -1717,7 +1720,7 @@ td {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
border: none !important;
|
||||
font-size: 13px !important;
|
||||
font-size: 14px !important;
|
||||
width: 266px !important;
|
||||
}
|
||||
}
|
||||
@@ -2973,10 +2976,6 @@ td {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.order-receipt-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.order-receipt {
|
||||
color: white;
|
||||
font-size: medium;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
top: 0;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
font-size: 28px;
|
||||
font-size: 27px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ flectra.define('point_of_sale.Chrome', function(require) {
|
||||
|
||||
// 3. Save the screen to the order.
|
||||
// - This screen is shown when the order is selected.
|
||||
if (!(component.prototype instanceof IndependentToOrderScreen)) {
|
||||
if (!(component.prototype instanceof IndependentToOrderScreen) && name !== "ReprintReceiptScreen") {
|
||||
this._setScreenData(name, props);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,9 @@ flectra.define('point_of_sale.AbstractReceiptScreen', function (require) {
|
||||
return await this._printWeb();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* https://stackoverflow.com/questions/21285902/printing-a-part-of-webpage-with-javascript
|
||||
*/
|
||||
async _printWeb() {
|
||||
try {
|
||||
const isPrinted = document.execCommand('print', false, null);
|
||||
if (!isPrinted) window.print();
|
||||
window.print();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
|
||||
@@ -10,7 +10,11 @@ flectra.define('point_of_sale.ClientDetailsEdit', function(require) {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.intFields = ['country_id', 'state_id', 'property_product_pricelist'];
|
||||
this.changes = {};
|
||||
const partner = this.props.partner;
|
||||
this.changes = {
|
||||
'country_id': partner.country_id && partner.country_id[0],
|
||||
'state_id': partner.state_id && partner.state_id[0],
|
||||
};
|
||||
}
|
||||
mounted() {
|
||||
this.env.bus.on('save-customer', this, this.saveChanges);
|
||||
|
||||
+1
-9
@@ -18,15 +18,7 @@ flectra.define('point_of_sale.ReprintReceiptButton', function (require) {
|
||||
const order = this.orderManagementContext.selectedOrder;
|
||||
if (!order) return;
|
||||
|
||||
if (this.env.pos.proxy.printer) {
|
||||
const receiptHtml = this.props.hiddenOrderReceipt.el.outerHTML;
|
||||
const printResult = await this.env.pos.proxy.printer.print_receipt(receiptHtml);
|
||||
if (!printResult.successful) {
|
||||
this.showTempScreen('ReprintReceiptScreen', { order: order });
|
||||
}
|
||||
} else {
|
||||
this.showTempScreen('ReprintReceiptScreen', { order: order });
|
||||
}
|
||||
this.showScreen('ReprintReceiptScreen', { order: order });
|
||||
}
|
||||
}
|
||||
ReprintReceiptButton.template = 'ReprintReceiptButton';
|
||||
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
flectra.define('point_of_sale.OrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useContext, useRef } = owl.hooks;
|
||||
const { useContext } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
@@ -27,7 +27,6 @@ flectra.define('point_of_sale.OrderManagementScreen', function (require) {
|
||||
OrderFetcher.setComponent(this);
|
||||
OrderFetcher.setConfigId(this.env.pos.config_id);
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
this.receiptRef = useRef('order-receipt');
|
||||
}
|
||||
mounted() {
|
||||
OrderFetcher.on('update', this, this.render);
|
||||
@@ -77,9 +76,9 @@ flectra.define('point_of_sale.OrderManagementScreen', function (require) {
|
||||
OrderFetcher.fetch();
|
||||
}
|
||||
_onClickOrder({ detail: clickedOrder }) {
|
||||
if (!clickedOrder || clickedOrder.locked) {
|
||||
let currentPOSOrder = this.env.pos.get_order();
|
||||
if (currentPOSOrder && (!clickedOrder || clickedOrder.locked)) {
|
||||
this.orderManagementContext.selectedOrder = clickedOrder;
|
||||
let currentPOSOrder = this.env.pos.get_order();
|
||||
if (clickedOrder.attributes.client){
|
||||
currentPOSOrder.set_client(clickedOrder.attributes.client);
|
||||
}
|
||||
|
||||
+14
-5
@@ -6,12 +6,21 @@ flectra.define('point_of_sale.ReprintReceiptScreen', function (require) {
|
||||
|
||||
const ReprintReceiptScreen = (AbstractReceiptScreen) => {
|
||||
class ReprintReceiptScreen extends AbstractReceiptScreen {
|
||||
confirm() {
|
||||
this.props.resolve({ confirmed: true, payload: null });
|
||||
this.trigger('close-temp-screen');
|
||||
mounted() {
|
||||
this.printReceipt();
|
||||
}
|
||||
tryReprint() {
|
||||
this._printReceipt();
|
||||
confirm() {
|
||||
this.showScreen('OrderManagementScreen');
|
||||
}
|
||||
async printReceipt() {
|
||||
if(this.env.pos.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
|
||||
let result = await this._printReceipt();
|
||||
if(result)
|
||||
this.showScreen('OrderManagementScreen');
|
||||
}
|
||||
}
|
||||
async tryReprint() {
|
||||
await this._printReceipt();
|
||||
}
|
||||
}
|
||||
ReprintReceiptScreen.template = 'ReprintReceiptScreen';
|
||||
|
||||
@@ -31,6 +31,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
|
||||
onChangeOrder(this._onPrevOrder, this._onNewOrder);
|
||||
useErrorHandlers();
|
||||
this.payment_interface = null;
|
||||
this.error = false;
|
||||
this.payment_methods_from_config = this.env.pos.payment_methods.filter(method => this.env.pos.config.payment_method_ids.includes(method.id));
|
||||
}
|
||||
get currentOrder() {
|
||||
@@ -75,7 +76,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
|
||||
}
|
||||
_updateSelectedPaymentline() {
|
||||
if (this.paymentLines.every((line) => line.paid)) {
|
||||
this.currentOrder.add_paymentline(this.env.pos.payment_methods[0]);
|
||||
this.currentOrder.add_paymentline(this.payment_methods_from_config[0]);
|
||||
}
|
||||
if (!this.selectedPaymentLine) return; // do nothing if no selected payment line
|
||||
// disable changing amount on paymentlines with running or done payments on a payment terminal
|
||||
@@ -177,6 +178,8 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
|
||||
syncedOrderBackendIds = await this.env.pos.push_single_order(this.currentOrder);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code == 700)
|
||||
this.error = true;
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
} else {
|
||||
@@ -217,7 +220,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
|
||||
}
|
||||
}
|
||||
get nextScreen() {
|
||||
return 'ReceiptScreen';
|
||||
return !this.error? 'ReceiptScreen' : 'ProductScreen';
|
||||
}
|
||||
async _isOrderValid(isForceValidate) {
|
||||
if (this.currentOrder.get_orderlines().length === 0) {
|
||||
|
||||
@@ -63,6 +63,9 @@ flectra.define('point_of_sale.ReceiptScreen', function (require) {
|
||||
get nextScreen() {
|
||||
return { name: 'ProductScreen' };
|
||||
}
|
||||
whenClosing() {
|
||||
this.orderDone();
|
||||
}
|
||||
/**
|
||||
* This function is called outside the rendering call stack. This way,
|
||||
* we don't block the displaying of ReceiptScreen when it is mounted; additionally,
|
||||
@@ -72,7 +75,7 @@ flectra.define('point_of_sale.ReceiptScreen', function (require) {
|
||||
if (this._shouldAutoPrint()) {
|
||||
await this.printReceipt();
|
||||
if (this.currentOrder._printed && this._shouldCloseImmediately()) {
|
||||
this.orderDone();
|
||||
this.whenClosing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ flectra.define('point_of_sale.custom_hooks', function (require) {
|
||||
error.data.debug ||
|
||||
this.env._t('The server encountered an error while receiving your order.'),
|
||||
});
|
||||
} else if (error.code === 700) {
|
||||
// Fiscal module errors
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Fiscal data module error'),
|
||||
body:
|
||||
error.data.error.status ||
|
||||
this.env._t('The fiscal data module encountered an error while receiving your order.'),
|
||||
});
|
||||
} else {
|
||||
// ???
|
||||
await this.showPopup('ErrorPopup', {
|
||||
|
||||
@@ -2735,7 +2735,7 @@ exports.Order = Backbone.Model.extend({
|
||||
amount_paid: this.get_total_paid() - this.get_change(),
|
||||
amount_total: this.get_total_with_tax(),
|
||||
amount_tax: this.get_total_tax(),
|
||||
amount_return: this.amount_return ? this.amount_return : this.get_change(),
|
||||
amount_return: this.get_change(),
|
||||
lines: orderLines,
|
||||
statement_ids: paymentLines,
|
||||
pos_session_id: this.pos_session_id,
|
||||
|
||||
+1
-4
@@ -9,7 +9,7 @@
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
|
||||
<t t-component="cb.component" t-key="cb.name" hiddenOrderReceipt="receiptRef"/>
|
||||
<t t-component="cb.component" t-key="cb.name" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="subpads">
|
||||
@@ -26,9 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<MobileOrderManagementScreen t-else="" />
|
||||
<div class="order-receipt-hidden">
|
||||
<OrderReceipt t-if="orderManagementContext.selectedOrder" order="orderManagementContext.selectedOrder" t-ref="order-receipt" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</ul>
|
||||
<t t-if="props.line.get_lot_lines()">
|
||||
<ul class="info-list">
|
||||
<t t-foreach="props.line.get_lot_lines()" t-as="lot">
|
||||
<t t-foreach="props.line.get_lot_lines()" t-as="lot" t-key="lot.cid">
|
||||
<li>
|
||||
SN <t t-esc="lot.attributes['lot_name']"/>
|
||||
</li>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<t t-raw="receipt.header_html" />
|
||||
</t>
|
||||
<t t-if="!receipt.header_html and receipt.header">
|
||||
<div><t t-esc="receipt.header" /></div>
|
||||
<div style="white-space:pre-line"><t t-esc="receipt.header" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.cashier">
|
||||
<div class="cashier">
|
||||
@@ -93,7 +93,7 @@
|
||||
<t t-if="line.pack_lot_lines">
|
||||
<div class="pos-receipt-left-padding">
|
||||
<ul>
|
||||
<t t-foreach="line.pack_lot_lines" t-as="lot">
|
||||
<t t-foreach="line.pack_lot_lines" t-as="lot" t-key="lot.cid">
|
||||
<li>
|
||||
SN <t t-esc="lot.attributes['lot_name']"/>
|
||||
</li>
|
||||
|
||||
@@ -405,6 +405,74 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
# I close the session to generate the journal entries
|
||||
self.pos_config.current_session_id.action_pos_session_closing_control()
|
||||
|
||||
def test_order_to_picking02(self):
|
||||
""" This test is similar to test_order_to_picking except that this time, the product is tracked and its
|
||||
location is a sublocation of the main warehouse
|
||||
"""
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'SuperProduct',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
'available_in_pos': True,
|
||||
})
|
||||
wh_location = self.company_data['default_warehouse'].lot_stock_id
|
||||
shelf1_location = self.env['stock.location'].create({
|
||||
'name': 'shelf1',
|
||||
'usage': 'internal',
|
||||
'location_id': wh_location.id,
|
||||
})
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'name': 'SuperLot',
|
||||
'product_id': product.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
qty = 2
|
||||
self.env['stock.quant']._update_available_quantity(product, shelf1_location, qty, lot_id=lot)
|
||||
|
||||
self.pos_config.open_session_cb()
|
||||
self.pos_config.current_session_id.update_stock_at_closing = False
|
||||
|
||||
untax, atax = self.compute_tax(product, 1.15, 1)
|
||||
|
||||
for i in range(qty):
|
||||
pos_order = self.PosOrder.create({
|
||||
'company_id': self.env.company.id,
|
||||
'session_id': self.pos_config.current_session_id.id,
|
||||
'pricelist_id': self.partner1.property_product_pricelist.id,
|
||||
'partner_id': self.partner1.id,
|
||||
'lines': [(0, 0, {
|
||||
'name': "OL/0001",
|
||||
'product_id': product.id,
|
||||
'price_unit': untax + atax,
|
||||
'discount': 0.0,
|
||||
'qty': 1.0,
|
||||
'tax_ids': [(6, 0, product.taxes_id.ids)],
|
||||
'price_subtotal': untax,
|
||||
'price_subtotal_incl': untax + atax,
|
||||
'pack_lot_ids': [[0, 0, {'lot_name': lot.name}]],
|
||||
})],
|
||||
'amount_tax': atax,
|
||||
'amount_total': untax + atax,
|
||||
'amount_paid': 0,
|
||||
'amount_return': 0,
|
||||
})
|
||||
|
||||
context_make_payment = {
|
||||
"active_ids": [pos_order.id],
|
||||
"active_id": pos_order.id,
|
||||
}
|
||||
pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': untax + atax,
|
||||
})
|
||||
context_payment = {'active_id': pos_order.id}
|
||||
pos_make_payment.with_context(context_payment).check()
|
||||
|
||||
self.assertEqual(pos_order.state, 'paid')
|
||||
self.assertEqual(pos_order.picking_ids[0].move_line_ids.lot_id, lot)
|
||||
self.assertEqual(pos_order.picking_ids[0].move_line_ids.location_id, shelf1_location)
|
||||
|
||||
self.pos_config.current_session_id.action_pos_session_closing_control()
|
||||
|
||||
def test_order_to_invoice(self):
|
||||
|
||||
self.pos_config.open_session_cb(check_coa=False)
|
||||
|
||||
@@ -7,7 +7,7 @@ from flectra.http import request
|
||||
|
||||
class PortalRating(http.Controller):
|
||||
|
||||
@http.route(['/website/rating/comment'], type='json', auth="user", method=['POST'], website=True)
|
||||
@http.route(['/website/rating/comment'], type='json', auth="user", methods=['POST'], website=True)
|
||||
def publish_rating_comment(self, rating_id, publisher_comment):
|
||||
rating = request.env['rating.rating'].search([('id', '=', int(rating_id))])
|
||||
if not rating:
|
||||
|
||||
@@ -10,6 +10,9 @@ flectra.define('pos_restaurant.BillScreen', function (require) {
|
||||
this.props.resolve({ confirmed: true, payload: null });
|
||||
this.trigger('close-temp-screen');
|
||||
}
|
||||
whenClosing() {
|
||||
this.confirm();
|
||||
}
|
||||
}
|
||||
BillScreen.template = 'BillScreen';
|
||||
return BillScreen;
|
||||
|
||||
@@ -136,8 +136,7 @@ flectra.define('pos_restaurant.TipScreen', function (require) {
|
||||
async _printWeb(receipt) {
|
||||
try {
|
||||
$(this.el).find('.pos-receipt-container').html(receipt);
|
||||
const isPrinted = document.execCommand('print', false, null);
|
||||
if (!isPrinted) window.print();
|
||||
window.print();
|
||||
} catch (err) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Printing is not supported on some browsers'),
|
||||
|
||||
@@ -142,12 +142,12 @@
|
||||
<field string="Attributes" name="attribute_line_ids" groups="product.group_product_variant"/>
|
||||
<field name="pricelist_id" context="{'pricelist': self}" filter_domain="[]" groups="product.group_product_pricelist"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<separator/>
|
||||
<filter string="Warnings" name="activities_exception"
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
|
||||
<filter string="Assigned to" name="user" context="{'group_by': 'user_id'}"/>
|
||||
|
||||
@@ -248,7 +248,9 @@ class PurchaseOrder(models.Model):
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'state' in init_values and self.state == 'purchase':
|
||||
return self.env.ref('purchase.mt_rfq_approved')
|
||||
if init_values['state'] == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_approved')
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'done':
|
||||
@@ -1045,8 +1047,9 @@ class PurchaseOrderLine(models.Model):
|
||||
|
||||
# If not seller, use the standard price. It needs a proper currency conversion.
|
||||
if not seller:
|
||||
po_line_uom = self.product_uom or self.product_id.uom_po_id
|
||||
price_unit = self.env['account.tax']._fix_tax_included_price_company(
|
||||
self.product_id.uom_id._compute_price(self.product_id.standard_price, self.product_id.uom_po_id),
|
||||
self.product_id.uom_id._compute_price(self.product_id.standard_price, po_line_uom),
|
||||
self.product_id.supplier_taxes_id,
|
||||
self.taxes_id,
|
||||
self.company_id,
|
||||
@@ -1059,9 +1062,6 @@ class PurchaseOrderLine(models.Model):
|
||||
self.date_order or fields.Date.today(),
|
||||
)
|
||||
|
||||
if self.product_uom:
|
||||
price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom)
|
||||
|
||||
self.price_unit = price_unit
|
||||
return
|
||||
|
||||
|
||||
@@ -170,3 +170,36 @@ class TestPurchase(AccountTestInvoicingCommon):
|
||||
'<p> partner_a modified receipt dates for the following products:</p><p> \xa0 - product_a from 2020-06-06 to %s </p><p> \xa0 - product_b from 2020-06-06 to %s </p>' % (fields.Date.today(), fields.Date.today()),
|
||||
activity.note,
|
||||
)
|
||||
|
||||
def test_with_different_uom(self):
|
||||
""" This test ensures that the unit price is correctly computed"""
|
||||
uom_units = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_unit')
|
||||
uom_dozens = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_dozen')
|
||||
uom_pairs = self.env['uom.uom'].create({
|
||||
'name': 'Pairs',
|
||||
'category_id': uom_units.category_id.id,
|
||||
'uom_type': 'bigger',
|
||||
'factor_inv': 2,
|
||||
'rounding': 1,
|
||||
})
|
||||
product_data = {
|
||||
'name': 'SuperProduct',
|
||||
'type': 'consu',
|
||||
'uom_id': uom_units.id,
|
||||
'uom_po_id': uom_pairs.id,
|
||||
'standard_price': 100
|
||||
}
|
||||
product_01 = self.env['product.product'].create(product_data)
|
||||
product_02 = self.env['product.product'].create(product_data)
|
||||
|
||||
po_form = Form(self.env['purchase.order'])
|
||||
po_form.partner_id = self.partner_a
|
||||
with po_form.order_line.new() as po_line:
|
||||
po_line.product_id = product_01
|
||||
with po_form.order_line.new() as po_line:
|
||||
po_line.product_id = product_02
|
||||
po_line.product_uom = uom_dozens
|
||||
po = po_form.save()
|
||||
|
||||
self.assertEqual(po.order_line[0].price_unit, 200)
|
||||
self.assertEqual(po.order_line[1].price_unit, 1200)
|
||||
@@ -447,12 +447,12 @@
|
||||
<filter name="order_date" string="Order Date" date="date_order"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Warnings" name="activities_exception"
|
||||
domain="[('activity_exception_decoration', '!=', False)]"/>
|
||||
|
||||
@@ -259,12 +259,12 @@
|
||||
<filter string="Done" name="done" domain="[('state', '=', 'done')]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Purchase Representative" name="representative" domain="[]" context="{'group_by': 'user_id'}"/>
|
||||
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
|
||||
|
||||
@@ -269,12 +269,12 @@
|
||||
<filter name="filter_create_date" date="create_date"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Customer" name="partner" domain="[]" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
|
||||
|
||||
@@ -140,7 +140,8 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# create the sale lines in batch
|
||||
new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create)
|
||||
new_sale_lines._onchange_discount()
|
||||
for sol in new_sale_lines:
|
||||
sol._onchange_discount()
|
||||
|
||||
# build result map by replacing index with newly created record of sale.order.line
|
||||
result = {}
|
||||
@@ -205,12 +206,16 @@ class AccountMoveLine(models.Model):
|
||||
amount = (self.credit or 0.0) - (self.debit or 0.0)
|
||||
|
||||
if self.product_id.expense_policy == 'sales_price':
|
||||
return self.product_id.with_context(
|
||||
product = self.product_id.with_context(
|
||||
partner=order.partner_id.id,
|
||||
date_order=order.date_order,
|
||||
pricelist=order.pricelist_id.id,
|
||||
uom=self.product_uom_id.id
|
||||
).price
|
||||
uom=self.product_uom_id.id,
|
||||
quantity=unit_amount
|
||||
)
|
||||
if order.pricelist_id.discount_policy == 'with_discount':
|
||||
return product.price
|
||||
return product.lst_price
|
||||
|
||||
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_is_zero(unit_amount, precision_digits=uom_precision_digits):
|
||||
|
||||
@@ -138,8 +138,22 @@ class SaleOrder(models.Model):
|
||||
""", (list(value),))
|
||||
so_ids = self.env.cr.fetchone()[0] or []
|
||||
return [('id', 'in', so_ids)]
|
||||
if operator == '=' and not value:
|
||||
return [('order_line.invoice_lines', '=', False)]
|
||||
elif operator == '=' and not value:
|
||||
# special case for [('invoice_ids', '=', False)], i.e. "Invoices is not set"
|
||||
#
|
||||
# We cannot just search [('order_line.invoice_lines', '=', False)]
|
||||
# because it returns orders with uninvoiced lines, which is not
|
||||
# same "Invoices is not set" (some lines may have invoices and some
|
||||
# doesn't)
|
||||
#
|
||||
# A solution is making inverted search first ("orders with invoiced
|
||||
# lines") and then invert results ("get all other orders")
|
||||
#
|
||||
# Domain below returns subset of ('order_line.invoice_lines', '!=', False)
|
||||
order_ids = self._search([
|
||||
('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund'))
|
||||
])
|
||||
return [('id', 'not in', order_ids)]
|
||||
return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)]
|
||||
|
||||
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
|
||||
@@ -297,6 +311,7 @@ class SaleOrder(models.Model):
|
||||
""" For service and consumable, we only take the min dates. This method is extended in sale_stock to
|
||||
take the picking_policy of SO into account.
|
||||
"""
|
||||
self.mapped("order_line") # Prefetch indication
|
||||
for order in self:
|
||||
dates_list = []
|
||||
for line in order.order_line.filtered(lambda x: x.state != 'cancel' and not x._is_delivery() and not x.display_type):
|
||||
|
||||
@@ -168,42 +168,42 @@
|
||||
<record id="account_invoice_rule_see_personal" model="ir.rule">
|
||||
<field name="name">Personal Invoices</field>
|
||||
<field name="model_id" ref="model_account_move"/>
|
||||
<field name="domain_force">[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')), '|', ('invoice_user_id', '=', user.id), ('invoice_user_id', '=', False)]</field>
|
||||
<field name="domain_force">[('move_type', 'in', ('out_invoice', 'out_refund')), '|', ('invoice_user_id', '=', user.id), ('invoice_user_id', '=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_rule_see_all" model="ir.rule">
|
||||
<field name="name">All Invoices</field>
|
||||
<field name="model_id" ref="model_account_move"/>
|
||||
<field name="domain_force">[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]</field>
|
||||
<field name="domain_force">[('move_type', 'in', ('out_invoice', 'out_refund'))]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_line_rule_see_personal" model="ir.rule">
|
||||
<field name="name">Personal Invoice Lines</field>
|
||||
<field name="model_id" ref="model_account_move_line"/>
|
||||
<field name="domain_force">[('move_id.move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')), '|', ('move_id.invoice_user_id', '=', user.id), ('move_id.invoice_user_id', '=', False)]</field>
|
||||
<field name="domain_force">[('move_id.move_type', 'in', ('out_invoice', 'out_refund')), '|', ('move_id.invoice_user_id', '=', user.id), ('move_id.invoice_user_id', '=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_line_rule_see_all" model="ir.rule">
|
||||
<field name="name">All Invoice Lines</field>
|
||||
<field name="model_id" ref="model_account_move_line"/>
|
||||
<field name="domain_force">[('move_id.move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]</field>
|
||||
<field name="domain_force">[('move_id.move_type', 'in', ('out_invoice', 'out_refund'))]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_send_rule_see_personal" model="ir.rule">
|
||||
<field name="name">Personal Invoice Send and Print</field>
|
||||
<field name="model_id" ref="account.model_account_invoice_send"/>
|
||||
<field name="domain_force">[('invoice_ids.move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')), '|', ('invoice_ids.invoice_user_id', '=', user.id), ('invoice_ids.invoice_user_id', '=', False)]</field>
|
||||
<field name="domain_force">[('invoice_ids.move_type', 'in', ('out_invoice', 'out_refund')), '|', ('invoice_ids.invoice_user_id', '=', user.id), ('invoice_ids.invoice_user_id', '=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_send_rule_see_all" model="ir.rule">
|
||||
<field name="name">All Invoice Send and Print</field>
|
||||
<field name="model_id" ref="account.model_account_invoice_send"/>
|
||||
<field name="domain_force">[('invoice_ids.move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]</field>
|
||||
<field name="domain_force">[('invoice_ids.move_type', 'in', ('out_invoice', 'out_refund'))]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -66,12 +66,37 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
'default_journal_id': cls.company_data['default_journal_sale'].id,
|
||||
}
|
||||
|
||||
def _check_order_search(self, orders, domain, expected_result):
|
||||
domain += [('id', 'in', orders.ids)]
|
||||
result = self.env['sale.order'].search(domain)
|
||||
self.assertEqual(result, expected_result, "Unexpected result on search orders")
|
||||
|
||||
def test_search_invoice_ids(self):
|
||||
"""Test searching on computed fields invoice_ids"""
|
||||
|
||||
# Make qty zero to have a line without invoices
|
||||
self.sol_prod_order.product_uom_qty = 0
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# Tests before creating an invoice
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '!=', False)], self.env['sale.order'])
|
||||
|
||||
# Create invoice
|
||||
moves = self.sale_order._create_invoices()
|
||||
|
||||
# Tests after creating the invoice
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', 'in', moves.ids)], self.sale_order)
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '!=', False)], self.sale_order)
|
||||
|
||||
def test_downpayment(self):
|
||||
""" Test invoice with a way of downpayment and check downpayment's SO line is created
|
||||
and also check a total amount of invoice is equal to a respective sale order's total amount
|
||||
"""
|
||||
# Confirm the SO
|
||||
self.sale_order.action_confirm()
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||||
# Let's do an invoice for a deposit of 100
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
'advance_payment_method': 'fixed',
|
||||
@@ -79,6 +104,7 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||||
})
|
||||
payment.create_invoices()
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||||
|
||||
self.assertEqual(len(self.sale_order.invoice_ids), 1, 'Invoice should be created for the SO')
|
||||
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment)
|
||||
@@ -161,7 +187,9 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
'advance_payment_method': 'delivered'
|
||||
})
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||||
payment.create_invoices()
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||||
invoice = self.sale_order.invoice_ids[0]
|
||||
invoice.action_post()
|
||||
|
||||
|
||||
@@ -746,12 +746,12 @@
|
||||
-->
|
||||
<filter string="My Orders" domain="[('user_id', '=', uid)]" name="my_sale_orders_filter"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Salesperson" name="salesperson" domain="[]" context="{'group_by': 'user_id'}"/>
|
||||
<filter name="customer" string="Customer" domain="[]" context="{'group_by': 'partner_id'}"/>
|
||||
|
||||
@@ -23,9 +23,10 @@ class CouponProgram(models.Model):
|
||||
'name': _('Sales Orders'),
|
||||
'view_mode': 'tree,form',
|
||||
'res_model': 'sale.order',
|
||||
'search_view_id': [self.env.ref('sale.sale_order_view_search_inherit_quotation').id],
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('id', 'in', orders.ids), ('state', 'not in', ('draft', 'sent', 'cancel'))],
|
||||
'context': dict(self._context, create=False)
|
||||
'domain': [('id', 'in', orders.ids)],
|
||||
'context': dict(self._context, create=False),
|
||||
}
|
||||
|
||||
def _check_promo_code(self, order, coupon_code):
|
||||
|
||||
@@ -11,8 +11,8 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
so_line = self.sale_line_ids and self.sale_line_ids[-1] or False
|
||||
if so_line:
|
||||
bom = so_line.product_id.product_tmpl_id.bom_ids and so_line.product_id.product_tmpl_id.bom_ids[0]
|
||||
if bom.type == 'phantom':
|
||||
bom = so_line.product_id.product_tmpl_id.bom_ids.filtered(lambda b: not b.company_id or b.company_id == so_line.company_id)[:1]
|
||||
if bom and bom.type == 'phantom':
|
||||
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in so_line.invoice_lines if x.move_id.state == 'posted'])
|
||||
moves = so_line.move_ids
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
|
||||
|
||||
@@ -155,14 +157,13 @@ class ProductionLot(models.Model):
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_sale_order_ids(self):
|
||||
sale_orders = defaultdict(lambda: self.env['sale.order'])
|
||||
for move_line in self.env['stock.move.line'].search([('lot_id', 'in', self.ids), ('state', '=', 'done')]):
|
||||
move = move_line.move_id
|
||||
if move.picking_id.location_dest_id.usage == 'customer' and move.sale_line_id.order_id:
|
||||
sale_orders[move_line.lot_id.id] |= move.sale_line_id.order_id
|
||||
for lot in self:
|
||||
stock_moves = self.env['stock.move.line'].search([
|
||||
('lot_id', '=', lot.id),
|
||||
('state', '=', 'done')
|
||||
]).mapped('move_id')
|
||||
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
|
||||
lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done')
|
||||
lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id')
|
||||
lot.sale_order_ids = sale_orders[lot.id]
|
||||
lot.sale_order_count = len(lot.sale_order_ids)
|
||||
|
||||
def action_view_so(self):
|
||||
|
||||
@@ -87,7 +87,7 @@ class Project(models.Model):
|
||||
employees = self.env['account.analytic.line'].read_group([('task_id', 'in', tasks.ids), ('non_allow_billable', '=', False)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
|
||||
dict_project_employee = defaultdict(list)
|
||||
for line in employees:
|
||||
dict_project_employee[line['project_id'][0]] += [line['employee_id'][0]]
|
||||
dict_project_employee[line['project_id'][0]] += [line['employee_id'][0]] if line['employee_id'] else []
|
||||
for project in projects:
|
||||
project.warning_employee_rate = any(x not in project.sale_line_employee_ids.employee_id.ids for x in dict_project_employee[project.id])
|
||||
|
||||
@@ -232,7 +232,7 @@ class ProjectTask(models.Model):
|
||||
|
||||
# TODO: [XBO] remove me in master
|
||||
non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheets linked to this task will not be billed.")
|
||||
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so')
|
||||
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so', compute_sudo=True)
|
||||
remaining_hours_available = fields.Boolean(related="sale_line_id.remaining_hours_available")
|
||||
|
||||
@api.depends('sale_line_id', 'timesheet_ids', 'timesheet_ids.unit_amount')
|
||||
|
||||
@@ -3,7 +3,6 @@ import babel.dates
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import itertools
|
||||
import json
|
||||
from flectra.osv import expression
|
||||
|
||||
from flectra import fields, _, models
|
||||
from flectra.osv import expression
|
||||
@@ -22,9 +21,7 @@ class Project(models.Model):
|
||||
|
||||
def _qweb_prepare_qcontext(self, view_id, domain):
|
||||
values = super()._qweb_prepare_qcontext(view_id, domain)
|
||||
project_ids = self.env.context.get('active_ids')
|
||||
if project_ids:
|
||||
domain = expression.AND([[('id', 'in', project_ids)], domain])
|
||||
|
||||
projects = self.search(domain)
|
||||
values.update(projects._plan_prepare_values())
|
||||
values['actions'] = projects._plan_prepare_actions(values)
|
||||
@@ -32,14 +29,8 @@ class Project(models.Model):
|
||||
return values
|
||||
|
||||
def _plan_get_employee_ids(self):
|
||||
user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id'])
|
||||
user_ids = [user_id['user_id'][0] for user_id in user_ids]
|
||||
employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids'])
|
||||
# flatten the list of list
|
||||
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
|
||||
|
||||
aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
|
||||
employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
|
||||
employee_ids = list(map(lambda x: x['employee_id'][0], aal_employee_ids))
|
||||
return employee_ids
|
||||
|
||||
def _plan_prepare_values(self):
|
||||
|
||||
@@ -176,8 +176,8 @@ class StockMove(models.Model):
|
||||
next_serial = fields.Char('First SN')
|
||||
next_serial_count = fields.Integer('Number of SN')
|
||||
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Original Reordering Rule', check_company=True)
|
||||
forecast_availability = fields.Float('Forecast Availability', compute='_compute_forecast_information', digits='Product Unit of Measure')
|
||||
forecast_expected_date = fields.Datetime('Forecasted Expected date', compute='_compute_forecast_information')
|
||||
forecast_availability = fields.Float('Forecast Availability', compute='_compute_forecast_information', digits='Product Unit of Measure', compute_sudo=True)
|
||||
forecast_expected_date = fields.Datetime('Forecasted Expected date', compute='_compute_forecast_information', compute_sudo=True)
|
||||
lot_ids = fields.Many2many('stock.production.lot', compute='_compute_lot_ids', inverse='_set_lot_ids', string='Serial Numbers', readonly=False)
|
||||
|
||||
@api.onchange('product_id', 'picking_type_id')
|
||||
@@ -656,6 +656,19 @@ class StockMove(models.Model):
|
||||
self._generate_serial_numbers()
|
||||
return self.action_show_details()
|
||||
|
||||
def action_clear_lines_show_details(self):
|
||||
""" Unlink `self.move_line_ids` before returning `self.action_show_details`.
|
||||
Useful for if a user creates too many SNs by accident via action_assign_serial_show_details
|
||||
since there's no way to undo the action.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.picking_type_id.show_reserved:
|
||||
move_lines = self.move_line_ids
|
||||
else:
|
||||
move_lines = self.move_line_nosuggest_ids
|
||||
move_lines.unlink()
|
||||
return self.action_show_details()
|
||||
|
||||
def action_assign_serial(self):
|
||||
""" Opens a wizard to assign SN's name on each move lines.
|
||||
"""
|
||||
|
||||
@@ -668,7 +668,7 @@ class Picking(models.Model):
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('picking_type_id') and self.state != 'draft':
|
||||
if vals.get('picking_type_id') and any(picking.state != 'draft' for picking in self):
|
||||
raise UserError(_("Changing the operation type of this record is forbidden at this point."))
|
||||
# set partner as a follower and unfollow old partner
|
||||
if vals.get('partner_id'):
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 900.84 584.93"><defs><style>.cls-1,.cls-16,.cls-17,.cls-19,.cls-2,.cls-3,.cls-5,.cls-6{fill:none;stroke-miterlimit:10;}.cls-1,.cls-2,.cls-3{stroke:#eaeaea;}.cls-2{stroke-width:1.03px;}.cls-3{stroke-width:1.02px;}.cls-4{fill:url(#linear-gradient);}.cls-5{stroke:#2496f6;stroke-width:3.54px;}.cls-6{stroke:#827882;stroke-width:2px;}.cls-21,.cls-7{fill:#827882;}.cls-10,.cls-8,.cls-9{fill:#282828;}.cls-10,.cls-15,.cls-21,.cls-9{font-size:20px;}.cls-20,.cls-21,.cls-9{font-family:GothamMedium, Gotham Medium;}.cls-10{font-family:Gotham-Book, Gotham Book;}.cls-11{letter-spacing:-0.01em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:-0.01em;}.cls-14{fill:#00a09d;}.cls-15,.cls-18{fill:#b79cb0;}.cls-15{font-family:GothamBold, Gotham Bold;}.cls-16,.cls-19{stroke:#00a09d;}.cls-16,.cls-17{stroke-width:4px;}.cls-17{stroke:#b79cb0;}.cls-19{stroke-width:2.26px;}.cls-20{font-size:18px;fill:#fff;}</style><linearGradient id="linear-gradient" x1="344.19" y1="510.33" x2="344.19" y2="98.95" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="0.48" stop-color="#2496f6" stop-opacity="0.27"/></linearGradient></defs><g id="Layer_3" data-name="Layer 3"><polyline class="cls-1" points="725.53 60.15 725.53 537.44 29.58 537.44 29.58 60.15"/><line class="cls-2" x1="29.58" y1="474.17" x2="766.26" y2="474.17"/><line class="cls-2" x1="29.58" y1="410.9" x2="766.26" y2="410.9"/><line class="cls-2" x1="29.58" y1="347.63" x2="766.26" y2="347.63"/><line class="cls-2" x1="29.58" y1="284.36" x2="766.26" y2="284.36"/><line class="cls-2" x1="29.58" y1="221.1" x2="766.26" y2="221.1"/><line class="cls-2" x1="29.58" y1="157.83" x2="766.26" y2="157.83"/><line class="cls-2" x1="29.58" y1="94.56" x2="766.26" y2="94.56"/><line class="cls-3" x1="662.26" y1="60.15" x2="662.26" y2="537.44"/><line class="cls-3" x1="598.99" y1="60.15" x2="598.99" y2="537.44"/><line class="cls-3" x1="535.72" y1="60.15" x2="535.72" y2="537.44"/><line class="cls-3" x1="472.45" y1="60.15" x2="472.45" y2="537.44"/><line class="cls-3" x1="409.19" y1="60.15" x2="409.19" y2="537.44"/><line class="cls-3" x1="345.92" y1="60.15" x2="345.92" y2="537.44"/><line class="cls-3" x1="282.65" y1="60.15" x2="282.65" y2="537.44"/><line class="cls-3" x1="219.38" y1="60.15" x2="219.38" y2="537.44"/><line class="cls-3" x1="156.11" y1="60.15" x2="156.11" y2="537.44"/><line class="cls-3" x1="92.85" y1="60.15" x2="92.85" y2="537.44"/><polyline class="cls-4" points="29.88 298 69.47 347.63 94.04 347.63 119.44 397.27 156.11 397.27 156.11 91.28 175.38 91.28 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67 658.81 537.44 29.58 537.44 29.88 338.89"/><polyline class="cls-5" points="29.6 298.66 69.47 347.63 94.04 347.63 119.44 400.98 156.11 400.98 156.11 94.56 175.38 94.56 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67"/><polyline class="cls-6" points="29.58 67.33 29.58 537.44 759.08 537.44"/><polygon class="cls-7" points="24.59 68.78 29.58 60.15 34.56 68.78 24.59 68.78"/><polygon class="cls-7" points="757.62 542.42 766.26 537.44 757.62 532.45 757.62 542.42"/><path class="cls-8" d="M36.24,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,36.24,94.11Z"/><ellipse class="cls-8" cx="41.24" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M49.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,49.58,94.11Z"/><path class="cls-8" d="M56.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,56.25,94.11Z"/><path class="cls-8" d="M62.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,62.91,94.11Z"/><path class="cls-8" d="M69.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,69.58,94.11Z"/><path class="cls-8" d="M76.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,76.25,94.11Z"/><path class="cls-8" d="M82.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,82.91,94.11Z"/><path class="cls-8" d="M89.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,89.58,94.11Z"/><path class="cls-8" d="M96.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,96.25,94.11Z"/><path class="cls-8" d="M102.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,102.91,94.11Z"/><path class="cls-8" d="M109.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,109.58,94.11Z"/><path class="cls-8" d="M116.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,116.25,94.11Z"/><path class="cls-8" d="M122.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,122.91,94.11Z"/><path class="cls-8" d="M129.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,129.58,94.11Z"/><ellipse class="cls-8" cx="134.58" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M142.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,142.91,94.Line truncated
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 900.84 584.93"><defs><style>.cls-1,.cls-16,.cls-17,.cls-19,.cls-2,.cls-3,.cls-5,.cls-6{fill:none;stroke-miterlimit:10;}.cls-1,.cls-2,.cls-3{stroke:#eaeaea;}.cls-2{stroke-width:1.03px;}.cls-3{stroke-width:1.02px;}.cls-4{fill:url(#linear-gradient);}.cls-5{stroke:#875a7b;stroke-width:3.54px;}.cls-6{stroke:#827882;stroke-width:2px;}.cls-21,.cls-7{fill:#827882;}.cls-10,.cls-8,.cls-9{fill:#282828;}.cls-10,.cls-15,.cls-21,.cls-9{font-size:20px;}.cls-20,.cls-21,.cls-9{font-family:GothamMedium, Gotham Medium;}.cls-10{font-family:Gotham-Book, Gotham Book;}.cls-11{letter-spacing:-0.01em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:-0.01em;}.cls-14{fill:#00a09d;}.cls-15,.cls-18{fill:#b79cb0;}.cls-15{font-family:GothamBold, Gotham Bold;}.cls-16,.cls-19{stroke:#00a09d;}.cls-16,.cls-17{stroke-width:4px;}.cls-17{stroke:#b79cb0;}.cls-19{stroke-width:2.26px;}.cls-20{font-size:18px;fill:#fff;}</style><linearGradient id="linear-gradient" x1="344.19" y1="510.33" x2="344.19" y2="98.95" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="0.48" stop-color="#875a7b" stop-opacity="0.27"/></linearGradient></defs><g id="Layer_3" data-name="Layer 3"><polyline class="cls-1" points="725.53 60.15 725.53 537.44 29.58 537.44 29.58 60.15"/><line class="cls-2" x1="29.58" y1="474.17" x2="766.26" y2="474.17"/><line class="cls-2" x1="29.58" y1="410.9" x2="766.26" y2="410.9"/><line class="cls-2" x1="29.58" y1="347.63" x2="766.26" y2="347.63"/><line class="cls-2" x1="29.58" y1="284.36" x2="766.26" y2="284.36"/><line class="cls-2" x1="29.58" y1="221.1" x2="766.26" y2="221.1"/><line class="cls-2" x1="29.58" y1="157.83" x2="766.26" y2="157.83"/><line class="cls-2" x1="29.58" y1="94.56" x2="766.26" y2="94.56"/><line class="cls-3" x1="662.26" y1="60.15" x2="662.26" y2="537.44"/><line class="cls-3" x1="598.99" y1="60.15" x2="598.99" y2="537.44"/><line class="cls-3" x1="535.72" y1="60.15" x2="535.72" y2="537.44"/><line class="cls-3" x1="472.45" y1="60.15" x2="472.45" y2="537.44"/><line class="cls-3" x1="409.19" y1="60.15" x2="409.19" y2="537.44"/><line class="cls-3" x1="345.92" y1="60.15" x2="345.92" y2="537.44"/><line class="cls-3" x1="282.65" y1="60.15" x2="282.65" y2="537.44"/><line class="cls-3" x1="219.38" y1="60.15" x2="219.38" y2="537.44"/><line class="cls-3" x1="156.11" y1="60.15" x2="156.11" y2="537.44"/><line class="cls-3" x1="92.85" y1="60.15" x2="92.85" y2="537.44"/><polyline class="cls-4" points="29.88 298 69.47 347.63 94.04 347.63 119.44 397.27 156.11 397.27 156.11 91.28 175.38 91.28 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67 658.81 537.44 29.58 537.44 29.88 338.89"/><polyline class="cls-5" points="29.6 298.66 69.47 347.63 94.04 347.63 119.44 400.98 156.11 400.98 156.11 94.56 175.38 94.56 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67"/><polyline class="cls-6" points="29.58 67.33 29.58 537.44 759.08 537.44"/><polygon class="cls-7" points="24.59 68.78 29.58 60.15 34.56 68.78 24.59 68.78"/><polygon class="cls-7" points="757.62 542.42 766.26 537.44 757.62 532.45 757.62 542.42"/><path class="cls-8" d="M36.24,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,36.24,94.11Z"/><ellipse class="cls-8" cx="41.24" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M49.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,49.58,94.11Z"/><path class="cls-8" d="M56.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,56.25,94.11Z"/><path class="cls-8" d="M62.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,62.91,94.11Z"/><path class="cls-8" d="M69.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,69.58,94.11Z"/><path class="cls-8" d="M76.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,76.25,94.11Z"/><path class="cls-8" d="M82.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,82.91,94.11Z"/><path class="cls-8" d="M89.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,89.58,94.11Z"/><path class="cls-8" d="M96.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,96.25,94.11Z"/><path class="cls-8" d="M102.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,102.91,94.11Z"/><path class="cls-8" d="M109.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,109.58,94.11Z"/><path class="cls-8" d="M116.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,116.25,94.11Z"/><path class="cls-8" d="M122.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,122.91,94.11Z"/><path class="cls-8" d="M129.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,129.58,94.11Z"/><ellipse class="cls-8" cx="134.58" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M142.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,142.91,94.Line truncated
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -164,6 +164,12 @@
|
||||
title="Assign Serial Numbers">
|
||||
<span>Assign Serial Numbers</span>
|
||||
</button>
|
||||
<button name="action_clear_lines_show_details" type="object"
|
||||
class="btn-link"
|
||||
title="Clear Lines"
|
||||
attrs="{'invisible': [('display_assign_serial', '=', False)]}">
|
||||
<span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user