[PATCH] Upstream patch

This commit is contained in:
Parthiv Patel committed 2021-06-20 11:03:19 +00:00
1 parent 45655fd76d
commit 3e576b368d
155 files changed
+1673 -901

No files matched your search

+46 -22
View File
@@ -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):
+17 -6
View File
@@ -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
'''
+1
View File
@@ -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.
+28
View File
@@ -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>
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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)
+5 -4
View File
@@ -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)
+38 -37
View File
@@ -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
# ------------------------------------------------------------
+56
View File
@@ -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)
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
</xpath>
<xpath expr="//filter[@name='assigned_to_me']" position="replace">
<filter string="My Activities" name="assigned_to_me"
+5 -5
View File
@@ -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;
+6 -6
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By">
<filter string="Vehicle" name="vehicle" context="{'group_by': 'vehicle_id'}"/>
</group>
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+1 -1
View File
@@ -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')
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+9 -12
View File
@@ -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
+6 -6
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
]"/>
<group expand="0" string="Group By">
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
]"/>
<group expand="0" string="Group By">
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
+1 -1
View File
@@ -18,7 +18,7 @@
By setting an alias to a job position, emails sent to this address create applications automatically. You can even use multiple trackers to get statistics according to the source of the application: LinkedIn, Monster, Indeed, etc.
% set record = object.env['hr.job'].search([('alias_name', '!=', False)], limit=1)
% if record and record.alias_domain
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #2496f6; text-decoration: none;">Try sending an email</a>
<a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Responsible" name="responsible" domain="[]" context="{'group_by': 'user_id'}"/>
+2 -2
View File
@@ -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>
+132 -34
View File
@@ -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')
+9 -81
View File
@@ -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
+14 -8
View File
@@ -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 """
+5 -2
View File
@@ -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())
+22
View File
@@ -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"
);
});
});
});
});
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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>
+6 -2
View File
@@ -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
+3 -1
View File
@@ -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):
+3 -3
View File
@@ -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>
+3 -3
View File
@@ -483,12 +483,12 @@
domain="['|', ('delay_alert_date', '!=', False), '&amp;', ('date_deadline', '&lt;', current_date), ('state', '=', 'confirmed')]"/>
<separator/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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"
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
</group>
<group expand='0' string='Group by...'>
<filter string='Product' name="productgroup" context="{'group_by': 'product_id'}"/>
+1 -1
View File
@@ -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):
+5
View File
@@ -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
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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):
+1 -1
View File
@@ -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
+15 -7
View File
@@ -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
+1 -1
View File
@@ -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
+4 -5
View File
@@ -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;
}
+1 -1
View File
@@ -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);
@@ -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';
@@ -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);
}
@@ -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', {
+1 -1
View File
@@ -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,
@@ -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'),
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
]"/>
<separator/>
<filter string="Warnings" name="activities_exception"
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+5 -5
View File
@@ -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
+33
View File
@@ -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)
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+9 -4
View File
@@ -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):
+17 -2
View File
@@ -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):
+6 -6
View File
@@ -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>
+28
View File
@@ -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()
+3 -3
View File
@@ -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', '&lt;', context_today().strftime('%Y-%m-%d'))]"
domain="[('my_activity_date_deadline', '&lt;', 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', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
domain="[('my_activity_date_deadline', '&gt;', 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'}"/>
+3 -2
View File
@@ -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):
+2 -2
View File
@@ -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
+8 -7
View File
@@ -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):
+2 -2
View File
@@ -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):
+15 -2
View File
@@ -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.
"""
+1 -1
View File
@@ -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
View File
@@ -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

+6
View File
@@ -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