Compare commits

..

1 Commits

Author SHA1 Message Date
hparfr
955cda6957 has_discount collides with account_cash_discount
rename here because it's easier
2023-04-12 12:15:00 +02:00
222 changed files with 1769 additions and 4591 deletions

View File

@@ -1,10 +1,10 @@
# Copyright 2017-2024 Akretion France (http://www.akretion.com/)
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Bank Reconciliation Report",
"version": "14.0.2.0.0",
"version": "14.0.1.0.0",
"license": "AGPL-3",
"author": "Akretion",
"website": "https://github.com/akretion/odoo-usability",

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-23 10:04+0000\n"
"PO-Revision-Date: 2024-10-23 10:04+0000\n"
"POT-Creation-Date: 2023-01-13 10:31+0000\n"
"PO-Revision-Date: 2023-01-13 10:31+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -17,7 +17,6 @@ msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Amount"
msgstr "Montant"
@@ -28,12 +27,6 @@ msgstr "Montant"
msgid "Balance %s:"
msgstr "Solde %s :"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Bank Balance:"
msgstr "Solde bancaire :"
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__journal_ids
msgid "Bank Journals"
@@ -56,7 +49,7 @@ msgstr "Rapport de rapprochement bancaire"
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model,name:account_bank_reconciliation_summary_xlsx.model_bank_reconciliation_report_wizard
msgid "Bank Reconciliation Report Wizard"
msgstr "Assistant de rapport de rapprochement bancaire"
msgstr "Assistant rapport de rapprochement bancaire"
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.actions.report,name:account_bank_reconciliation_summary_xlsx.bank_reconciliation_xlsx
@@ -103,36 +96,17 @@ msgstr "Créé le"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Currency"
msgstr "Devise"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__date
#, python-format
msgid "Date"
msgstr "Date"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Description"
msgstr "Description"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Difference:"
msgstr "Écart :"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__display_name
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_report_bank_reconciliation_xlsx__display_name
msgid "Display Name"
msgstr "Nom affiché"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields.selection,name:account_bank_reconciliation_summary_xlsx.selection__bank_reconciliation_report_wizard__move_state__draft_posted
@@ -149,13 +123,13 @@ msgstr "Écritures"
#. module: account_bank_reconciliation_summary_xlsx
#: model_terms:ir.ui.view,arch_db:account_bank_reconciliation_summary_xlsx.bank_reconciliation_report_wizard_form
msgid "Export XLSX"
msgstr "Exporter en XLSX"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Generated from Odoo on %s by %s"
msgstr "Généré par Odoo le %s par %s"
msgid "Generated on %s"
msgstr "Généré le %s"
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__id
@@ -169,18 +143,6 @@ msgstr ""
msgid "Journal"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Journal Entry"
msgstr "Pièce"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Justification:"
msgstr "Justification :"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
@@ -191,17 +153,23 @@ msgstr "Libellé"
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard____last_update
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_report_bank_reconciliation_xlsx____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
msgstr ""
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "Move Number"
msgstr "Numéro de pièce"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
@@ -211,23 +179,10 @@ msgstr "Aucun journal de banque sélectionné."
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid "None"
msgstr "Aucun"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid ""
"On bank journal %(journal)s which is configured with currency "
"%(journal_currency)s, the account %(account)s must be configured with the "
"same currency (current account currency: %(account_currency)s)."
msgstr ""
"Sur le journal de banque %(journal)s qui est configuré avec la devise "
"%(journal_currency)s, le compte %(account)s doit être configuré avec la même"
" devise (devise actuelle du compte : %(account_currency)s)."
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
@@ -256,13 +211,3 @@ msgstr "Sous-total :"
#, python-format
msgid "TOTAL:"
msgstr "TOTAL :"
#. module: account_bank_reconciliation_summary_xlsx
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
#, python-format
msgid ""
"The are %(count)s journal items in account %(account)s that have a currency "
"other than %(currency)s or where currency is not set."
msgstr ""
"Il y a %(count)s écritures comptables dans le compte %(account)s qui ont une"
" devise autre que %(currency)s ou dont la devise n'est pas définie."

View File

@@ -1,4 +1,4 @@
# Copyright 2017-2024 Akretion France (http://www.akretion.com/)
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@@ -14,19 +14,32 @@ class BankReconciliationXlsx(models.AbstractModel):
_description = "Bank Reconciliation XLSX Report"
_inherit = "report.report_xlsx.abstract"
def _prepare_payment_move_lines(self, jdi, account, unreconciled_only=True):
domain = [
("company_id", "=", jdi['company'].id),
("account_id", "=", account.id),
("journal_id", "=", jdi['journal'].id),
("date", "<=", jdi['wizard'].date),
]
if jdi['wizard'].move_state == 'posted':
def _domain_add_move_state(self, wizard, domain):
if wizard.move_state == 'posted':
domain.append(('parent_state', '=', 'posted'))
elif jdi['wizard'].move_state == 'draft_posted':
elif wizard.move_state == 'draft_posted':
domain.append(('parent_state', 'in', ('draft', 'posted')))
def _get_account_balance(self, account, wizard):
domain = [
('account_id', '=', account.id),
('date', '<=', wizard.date),
('company_id', '=', wizard.company_id.id),
]
self._domain_add_move_state(wizard, domain)
res_rg = self.env['account.move.line'].read_group(domain, ['balance:sum'], [])
account_bal = res_rg and res_rg[0].get('balance', 0.0) or 0.0
return account_bal
def _prepare_payment_move_lines(self, journal, account, wizard, unreconciled_only=True):
domain = [
("company_id", "=", wizard.company_id.id),
("account_id", "=", account.id),
("journal_id", "=", journal.id),
("date", "<=", wizard.date),
]
if unreconciled_only:
limit_datetime_naive = datetime.combine(jdi['wizard'].date, datetime.max.time())
limit_datetime_naive = datetime.combine(wizard.date, datetime.max.time())
tz = pytz.timezone(self.env.user.tz)
limit_datetime_aware = tz.localize(limit_datetime_naive)
limit_datetime_utc = limit_datetime_aware.astimezone(pytz.utc)
@@ -34,6 +47,7 @@ class BankReconciliationXlsx(models.AbstractModel):
domain += [
'|', ('full_reconcile_id', '=', False),
('full_reconcile_id.create_date', '>', limit_datetime)]
self._domain_add_move_state(wizard, domain)
mlines = self.env["account.move.line"].search(domain)
res = []
for mline in mlines:
@@ -46,18 +60,13 @@ class BankReconciliationXlsx(models.AbstractModel):
):
cpart.append(line.account_id.code)
counterpart = " ,".join(cpart)
if jdi['currency'] == mline.currency_id:
amount = mline.amount_currency
else:
amount = mline.currency_id._convert(
mline.amount_currency, jdi['currency'], jdi['company'], mline.date)
res.append(
{
"date": mline.date,
"ref": move.ref or "",
"label": mline.name,
"partner": mline.partner_id.display_name or "",
"amount": amount,
"amount": mline.balance,
"move_name": move.name,
"counterpart": counterpart,
}
@@ -65,31 +74,27 @@ class BankReconciliationXlsx(models.AbstractModel):
return res
def _write_move_lines_block(self, jdi, row, account, add2total=True):
# Returns row
# For suspense lines, it may not add any cells if there are no suspense lines
# => in this case, it doesn't increment row
# If it adds cells, it returns row + 2 to add 2 empty rows at the end
sheet = jdi['sheet']
style = jdi['style']
style_suffix = not add2total and '_warn' or ''
subtotal = 0.0
mlines = self._prepare_payment_move_lines(jdi, account)
mlines = self._prepare_payment_move_lines(jdi['journal'], account, jdi['wizard'])
if mlines or add2total:
sheet.write(row, 0, '%s %s' % (account.name, account.code), style[f"title{style_suffix}"])
sheet.write(row, 1, "", style[f"title{style_suffix}"])
sheet.write(row, 0, '%s %s' % (account.name, account.code), style['title' + style_suffix])
sheet.write(row, 1, "", style['title' + style_suffix])
if not mlines:
if add2total:
sheet.write(row, 2, _("None"), style['none'])
else:
return row
return
else:
row += 1
col_labels = [
_("Date"),
_("Partner"),
_("Amount"),
_("Journal Entry"),
_("Move Number"),
_("Counter-part"),
_("Ref."),
_("Label"),
@@ -103,7 +108,7 @@ class BankReconciliationXlsx(models.AbstractModel):
for mline in mlines:
sheet.write(row, 0, mline["date"], style['regular_date'])
sheet.write(row, 1, mline["partner"], style['regular'])
sheet.write(row, 2, mline["amount"], style[jdi['currency']])
sheet.write(row, 2, mline["amount"], style['currency'])
sheet.write(row, 3, mline["move_name"], style['regular'])
sheet.write(row, 4, mline["counterpart"], style['regular'])
sheet.write(row, 5, mline["ref"], style['regular'])
@@ -113,20 +118,18 @@ class BankReconciliationXlsx(models.AbstractModel):
end_line = row
for col in range(1):
sheet.write(row, col, "", style[f"title{style_suffix}"])
sheet.write(row, 1, _("Sub-total:") + ' ', style[f"title_right{style_suffix}"])
sheet.write(row, col, "", style['title' + style_suffix])
sheet.write(row, 1, _("Sub-total:") + ' ', style['title_right' + style_suffix])
formula = f"=SUM({jdi['total_col']}{start_line}:{jdi['total_col']}{end_line})"
sheet.write_formula(row, 2, formula, style[f"{jdi['currency']}_bg{style_suffix}"], subtotal)
formula = '=SUM(%s%d:%s%d)' % (
jdi['total_col'], start_line, jdi['total_col'], end_line)
sheet.write_formula(row, 2, formula, style['currency_bg' + style_suffix], subtotal)
if add2total:
jdi['total'] += subtotal
jdi['total_formula'] += f"+{jdi['total_col']}{row + 1}"
return row + 2
jdi['total_formula'] += '+%s%d' % (jdi['total_col'], row + 1)
return row
def generate_xlsx_report(self, workbook, data, wizard):
lang = self.env.user.lang
self = self.with_context(lang=lang)
wizard = wizard.with_context(lang=lang)
if not wizard.journal_ids:
raise UserError(_("No bank journal selected."))
date_dt = wizard.date
@@ -134,21 +137,15 @@ class BankReconciliationXlsx(models.AbstractModel):
style = self._get_style(workbook, company)
move_state_label = dict(
wizard.fields_get('move_state', 'selection')['move_state']['selection'])
generated_on_label = _('Generated from Odoo on %s by %s') % (
format_datetime(self.env, datetime.utcnow()),
self.env.user.name)
generated_on_label = _('Generated on %s') % format_datetime(
self.env, datetime.utcnow())
for journal in wizard.journal_ids:
row = 0
sheet = workbook.add_worksheet(journal.code or journal.name)
bank_account = journal.default_account_id
jdi = {
'wizard': wizard,
'company': company,
'journal': journal,
'currency': journal.currency_id or company.currency_id,
'bank_account': bank_account,
'style': style,
'workbook': workbook,
'sheet': sheet,
'total': 0.0,
'total_formula': '=',
@@ -172,7 +169,7 @@ class BankReconciliationXlsx(models.AbstractModel):
sheet.set_column(6, 6, 60)
row += 3
sheet.write(row, 0, _("Company"), style['wizard_field'])
sheet.write(row, 1, company.display_name, style['wizard_value'])
sheet.write(row, 1, wizard.company_id.display_name, style['wizard_value'])
row += 1
sheet.write(row, 0, _("Date"), style['wizard_field'])
sheet.write(row, 1, date_dt, style['wizard_value_date'])
@@ -180,121 +177,51 @@ class BankReconciliationXlsx(models.AbstractModel):
sheet.write(row, 0, _("Journal"), style['wizard_field'])
sheet.write(row, 1, journal.display_name, style['wizard_value'])
row += 1
sheet.write(row, 0, _("Currency"), style['wizard_field'])
sheet.write(row, 1, jdi['currency'].name, style['wizard_value'])
row += 1
sheet.write(row, 0, _("Entries"), style['wizard_field'])
sheet.write(row, 1, move_state_label[wizard.move_state], style['wizard_value'])
# Setup check
if journal.currency_id and journal.currency_id != company.currency_id:
if journal.currency_id != bank_account.currency_id:
raise UserError(_(
"On bank journal %(journal)s which is configured with currency "
"%(journal_currency)s, the account %(account)s must be configured "
"with the same currency (current account currency: %(account_currency)s).",
journal=journal.display_name,
journal_currency=journal.currency_id.name,
account=bank_account.display_name,
account_currency=bank_account.currency_id.name or _('None')))
bad_line_count = self.env['account.move.line'].search_count([
('company_id', '=', company.id),
('journal_id', '=', journal.id),
('account_id', '=', bank_account.id),
('currency_id', '!=', jdi['currency'].id),
])
if bad_line_count:
raise UserError(_(
"The are %(count)s journal items in account %(account)s "
"that have a currency other than %(currency)s or where "
"currency is not set.",
count=bad_line_count,
account=bank_account.display_name,
currency=jdi['currency'].name))
# 1) Show balance of bank account
row += 3
bank_account = journal.default_account_id
for col in range(1):
sheet.write(row, col, "", style['title'])
sheet.write(row, 1, _("Balance %s:") % bank_account.code + ' ', style['title_right'])
if wizard.move_state == 'posted':
domain = [('parent_state', '=', 'posted')]
else:
# by default, the native method _get_journal_bank_account_balance()
# has ('parent_state', '!=', 'cancel')
domain = None
account_bal, nb_lines = journal._get_journal_bank_account_balance(domain=domain)
account_bal = self._get_account_balance(bank_account, wizard)
sheet.write(row, 2, account_bal, style[f"{jdi['currency']}_bg"])
sheet.write(row, 2, account_bal, style['currency_bg'])
jdi['total'] += account_bal
jdi['total_formula'] += f"{jdi['total_col']}{row + 1}"
jdi['total_formula'] += '%s%d' % (jdi['total_col'], row + 1)
row += 2
# 2) Show payment lines IN (debit)
debit_account = journal.payment_debit_account_id
row = self._write_move_lines_block(jdi, row, debit_account)
row += 2
# 3) Show payment lines OUT (credit)
credit_account = journal.payment_credit_account_id
row = self._write_move_lines_block(jdi, row, credit_account)
row += 2
for col in range(1):
sheet.write(row, col, "", style['title'])
sheet.write(row, 1, _("TOTAL:") + ' ', style['title_right'])
sheet.write_formula(
row, 2, jdi['total_formula'], style[f"{jdi['currency']}_bg"], jdi['total'])
total_row = row
row += 2
row, 2, jdi['total_formula'], style['currency_bg'], jdi['total'])
row += 3
# 4) Show suspense account lines
row = self._write_move_lines_block(
jdi, row, journal.suspense_account_id, add2total=False)
# Static cells
for col in range(1):
sheet.write(row, col, "", style['title'])
sheet.write(row, 1, _("Bank Balance:") + ' ', style['title_right'])
sheet.write(row, 2, 0, style[f"{jdi['currency']}_bg_manual"])
bank_bal_row = row
row += 2
for col in range(1):
sheet.write(row, col, "", style['title'])
sheet.write(row, 1, _("Difference:") + ' ', style['title_right'])
sheet.write_formula(
row, 2, f"={jdi['total_col']}{total_row + 1}-{jdi['total_col']}{bank_bal_row + 1}",
style[f"{jdi['currency']}_bg"], jdi['total'])
row += 2
for col in range(1):
sheet.write(row, col, "", style['title'])
sheet.write(row, 1, _("Justification:") + ' ', style['title_right'])
justif_lines = 6
sheet.write_formula(
row, 2, f"=SUM({jdi['total_col']}{row+3}:{jdi['total_col']}{row+3+justif_lines-1})",
style[f"{jdi['currency']}_bg"], 0)
row += 1
col_labels = [
_("Date"),
_("Description"),
_("Amount"),
]
col = 0
for col_label in col_labels:
sheet.write(row, col, col_label, style['col_header'])
col += 1
for x in range(justif_lines):
row += 1
sheet.write(row, 0, "", style['regular_date'])
sheet.write(row, 1, "", style['regular'])
sheet.write(row, 2, "", style[jdi['currency']])
def _get_style(self, workbook, company):
style = {}
font_size = 10
light_grey = "#eeeeee"
light_blue = "#e0edff"
title_blue = "#e6e6fa"
subtotal_orange = "#ffcc00"
subtotal_warn = "#ffff99"
amount_manual = "#ffeeab"
title_warn = "#ff9999"
subtotal_warn = "#ffff99"
light_purple = "#ffdeff"
lang_code = self.env.user.lang
lang = False
if lang_code:
@@ -322,7 +249,7 @@ class BankReconciliationXlsx(models.AbstractModel):
)
title_style = {
"bold": True,
"bg_color": light_blue,
"bg_color": title_blue,
"font_size": font_size,
"align": "left",
}
@@ -330,7 +257,7 @@ class BankReconciliationXlsx(models.AbstractModel):
style['title'] = workbook.add_format(dict(title_style))
style['wizard_field'] = workbook.add_format(dict(title_style, bg_color=light_grey))
wizard_value_style = {
"bg_color": light_blue,
"bg_color": light_purple,
"bold": True,
"font_size": font_size,
"align": "left",
@@ -347,29 +274,23 @@ class BankReconciliationXlsx(models.AbstractModel):
dict(title_style, align="left", bg_color=title_warn))
style['title_right_warn'] = workbook.add_format(
dict(title_style, align="right", bg_color=title_warn))
style['regular'] = workbook.add_format({"font_size": font_size, "border": 1})
style['regular'] = workbook.add_format({"font_size": font_size})
if "%" in xls_date_format:
# fallback
xls_date_format = "yyyy-mm-dd"
style['regular_date'] = workbook.add_format(
{"num_format": xls_date_format, "font_size": font_size, "align": "left", "border": 1}
{"num_format": xls_date_format, "font_size": font_size, "align": "left"}
)
for currency in self.env['res.currency'].search([]):
symbol = currency.symbol or currency.name
decimals = '0' * currency.decimal_places
if currency.position == 'before':
cur_format = f"{symbol} #,##0.{decimals}"
else:
cur_format = f"#,##0.{decimals} {symbol}"
# It seems that Excel replaces automatically the decimal
# and thousand separator by those of the language under which
# Excel runs
currency_style = {"num_format": cur_format, "font_size": font_size}
style[currency] = workbook.add_format(dict(currency_style, border=1))
style[f'{currency}_bg'] = workbook.add_format(
dict(currency_style, bg_color=subtotal_orange))
style[f'{currency}_bg_warn'] = workbook.add_format(
dict(currency_style, bg_color=subtotal_warn))
style[f'{currency}_bg_manual'] = workbook.add_format(
dict(currency_style, bg_color=amount_manual))
cur_format = "#,##0.00 %s" % (
company.currency_id.symbol or company.currency_id.name
)
# It seems that Excel replaces automatically the decimal
# and thousand separator by those of the language under which
# Excel runs
currency_style = {"num_format": cur_format, "font_size": font_size}
style['currency'] = workbook.add_format(currency_style)
style['currency_bg'] = workbook.add_format(
dict(currency_style, bg_color=subtotal_orange))
style['currency_bg_warn'] = workbook.add_format(
dict(currency_style, bg_color=subtotal_warn))
return style

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2024 Akretion France (http://www.akretion.com/)
Copyright 2017-2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
@@ -12,6 +12,7 @@
<field name="report_type">xlsx</field>
<field name="report_name">bank.reconciliation.xlsx</field>
<field name="report_file">bank.reconciliation.xlsx</field>
<!-- print_report_name doesn't work here... -->
<field name="print_report_name">'bank_reconciliation-%s' % (object.date)</field>
</record>
</odoo>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2024 Akretion France (http://www.akretion.com/)
Copyright 2017-2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2018-2024 Akretion France (http://www.akretion.com/)
Copyright 2018-2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
@@ -9,7 +9,7 @@
<record id="account_journal_dashboard_kanban_view" model="ir.ui.view">
<field
name="name"
>bank_reconciliation_summary.account_journal_dashboard</field>
>bank_reconciliation_summarry.account_journal_dashboard</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view" />
<field name="arch" type="xml">

View File

@@ -1,4 +1,4 @@
# Copyright 2017-2024 Akretion France (http://www.akretion.com/)
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2024 Akretion France (http://www.akretion.com/)
Copyright 2017-2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->

View File

@@ -1 +1,2 @@
from . import models
from . import account_invoice
from . import account_invoice_report

View File

@@ -4,7 +4,7 @@
{
'name': 'Account Invoice Margin',
'version': '14.0.1.0.0',
'version': '12.0.1.0.0',
'category': 'Invoicing Management',
'license': 'AGPL-3',
'summary': 'Copy standard price on invoice line and compute margins',
@@ -15,10 +15,10 @@ This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'website': 'http://www.akretion.com',
'depends': ['account'],
'data': [
'views/account_move.xml',
'account_invoice_view.xml',
],
'installable': True,
'installable': False,
}

View File

@@ -0,0 +1,152 @@
# Copyright 2015-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
import odoo.addons.decimal_precision as dp
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
standard_price_company_currency = fields.Float(
string='Cost Price in Company Currency', readonly=True,
digits=dp.get_precision('Product Price'),
help="Cost price in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
string='Cost Price in Invoice Currency', readonly=True,
compute='_compute_margin', store=True,
digits=dp.get_precision('Product Price'),
help="Cost price in invoice currency in the unit of measure "
"of the invoice line")
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'standard_price_company_currency', 'invoice_id.currency_id',
'invoice_id.type', 'invoice_id.company_id',
'invoice_id.date_invoice', 'quantity', 'price_subtotal')
def _compute_margin(self):
for il in self:
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
inv = il.invoice_id
if inv and inv.type in ('out_invoice', 'out_refund'):
# it works in _get_current_rate
# even if we set date = False in context
# standard_price_inv_cur is in the UoM of the invoice line
date = inv._get_currency_rate_date() or\
fields.Date.context_today(self)
company = inv.company_id
company_currency = company.currency_id
standard_price_inv_cur =\
company_currency._convert(
il.standard_price_company_currency,
inv.currency_id, company, date)
margin_inv_cur =\
il.price_subtotal - il.quantity * standard_price_inv_cur
margin_comp_cur = inv.currency_id._convert(
margin_inv_cur, company_currency, company, date)
if il.price_subtotal:
margin_rate = 100 * margin_inv_cur / il.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if inv.type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
il.standard_price_invoice_currency = standard_price_inv_cur
il.margin_invoice_currency = margin_inv_cur
il.margin_company_currency = margin_comp_cur
il.margin_rate = margin_rate
# We want to copy standard_price on invoice line for customer
# invoice/refunds. We can't do that via on_change of product_id,
# because it is not always played when invoice is created from code
# => we inherit write/create
# We write standard_price_company_currency even on supplier invoice/refunds
# because we don't have access to the 'type' of the invoice
@api.model
def create(self, vals):
if vals.get('product_id'):
pp = self.env['product.product'].browse(vals['product_id'])
std_price = pp.standard_price
inv_uom_id = vals.get('uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super(AccountInvoiceLine, self).create(vals)
def write(self, vals):
if not vals:
vals = {}
if 'product_id' in vals or 'uom_id' in vals:
for il in self:
if 'product_id' in vals:
if vals.get('product_id'):
pp = self.env['product.product'].browse(
vals['product_id'])
else:
pp = False
else:
pp = il.product_id or False
# uom_id is NOT a required field
if 'uom_id' in vals:
if vals.get('uom_id'):
inv_uom = self.env['uom.uom'].browse(
vals['uom_id'])
else:
inv_uom = False
else:
inv_uom = il.uom_id or False
std_price = 0.0
if pp:
std_price = pp.standard_price
if inv_uom and inv_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
il.write({'standard_price_company_currency': std_price})
return super(AccountInvoiceLine, self).write(vals)
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='company_currency_id')
@api.depends(
'type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
res = self.env['account.invoice.line'].read_group(
[('invoice_id', 'in', self.ids)],
['invoice_id', 'margin_invoice_currency',
'margin_company_currency'],
['invoice_id'])
for re in res:
if re['invoice_id']:
inv = self.browse(re['invoice_id'][0])
if inv.type in ('out_invoice', 'out_refund'):
inv.margin_invoice_currency = re['margin_invoice_currency']
inv.margin_company_currency = re['margin_company_currency']

View File

@@ -0,0 +1,60 @@
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# why digits=0 ??? Why is it like that in the native "account" module
user_currency_margin = fields.Float(
string="Margin", compute='_compute_user_currency_margin', digits=0)
_depends = {
'account.invoice': [
'account_id', 'amount_total_company_signed',
'commercial_partner_id', 'company_id',
'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
'journal_id', 'number', 'partner_bank_id', 'partner_id',
'payment_term_id', 'residual', 'state', 'type', 'user_id',
],
'account.invoice.line': [
'account_id', 'invoice_id', 'price_subtotal', 'product_id',
'quantity', 'uom_id', 'account_analytic_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.depends('currency_id', 'date', 'margin')
def _compute_user_currency_margin(self):
user_currency = self.env.user.company_id.currency_id
currency_rate = self.env['res.currency.rate'].search([
('rate', '=', 1),
'|',
('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False)], limit=1)
base_currency = currency_rate.currency_id
for record in self:
date = record.date or fields.Date.today()
company = record.company_id
record.user_currency_margin = base_currency._convert(
record.margin, user_currency, company, date)
# TODO check for refunds
def _sub_select(self):
select_str = super(AccountInvoiceReport, self)._sub_select()
select_str += ", SUM(ail.margin_company_currency) AS margin"
return select_str
def _select(self):
select_str = super(AccountInvoiceReport, self)._select()
select_str += ", sub.margin AS margin"
return select_str

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015-2017 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_invoice_line_form" model="ir.ui.view">
<field name="name">margin.account.invoice.line.form</field>
<field name="model">account.invoice.line</field>
<field name="inherit_id" ref="account.view_invoice_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='analytic_tag_ids']/.." position="inside">
<field name="standard_price_company_currency"
string="Cost Price in Comp. Cur."
groups="base.group_no_one"/>
<field name="standard_price_invoice_currency"
string="Cost Price in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_invoice_currency"
string="Margin in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur."
groups="base.group_no_one"/>
<label for="margin_rate" groups="base.group_no_one"/>
<div name="margin_rate" groups="base.group_no_one">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="invoice_form" model="ir.ui.view">
<field name="name">margin.account.invoice.form</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<field name="move_id" position="after">
<field name="margin_invoice_currency"
string="Margin in Inv. Cur." groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur." groups="base.group_no_one"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,2 +0,0 @@
from . import account_move
from . import account_invoice_report

View File

@@ -1,36 +0,0 @@
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# added margin_company_currency on account.move.line
_depends = {
'account.move': [
'name', 'state', 'move_type', 'partner_id', 'invoice_user_id', 'fiscal_position_id',
'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'partner_bank_id',
],
'account.move.line': [
'quantity', 'price_subtotal', 'amount_residual', 'balance', 'amount_currency',
'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id',
'journal_id', 'company_id', 'currency_id', 'partner_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.model
def _select(self):
select_str = super()._select()
select_str += ", line.margin_company_currency * currency_table.rate AS margin"
return select_str

View File

@@ -1,155 +0,0 @@
# Copyright 2015-2021 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
standard_price_company_currency = fields.Float(
string='Unit Cost Price in Company Currency', readonly=True,
digits='Product Price',
help="Unit Cost price in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
string='Unit Cost Price in Invoice Currency',
compute='_compute_margin', store=True, digits='Product Price',
help="Unit Cost price in invoice currency in the unit of measure "
"of the invoice line.")
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency', store=True,
compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency', store=True,
compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'standard_price_company_currency', 'move_id.currency_id',
'move_id.move_type', 'move_id.company_id',
'move_id.invoice_date', 'quantity', 'price_subtotal')
def _compute_margin(self):
for ml in self:
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
move = ml.move_id
if move.move_type and move.move_type in ('out_invoice', 'out_refund'):
# it works in _get_current_rate
# even if we set date = False in context
# standard_price_inv_cur is in the UoM of the invoice line
date = move.date or fields.Date.context_today(self)
company = move.company_id
company_currency = company.currency_id
standard_price_inv_cur =\
company_currency._convert(
ml.standard_price_company_currency,
ml.currency_id, company, date)
margin_inv_cur =\
ml.price_subtotal - ml.quantity * standard_price_inv_cur
margin_comp_cur = move.currency_id._convert(
margin_inv_cur, company_currency, company, date)
if ml.price_subtotal:
margin_rate = 100 * margin_inv_cur / ml.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if move.move_type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
ml.standard_price_invoice_currency = standard_price_inv_cur
ml.margin_invoice_currency = margin_inv_cur
ml.margin_company_currency = margin_comp_cur
ml.margin_rate = margin_rate
# We want to copy standard_price on invoice line for customer
# invoice/refunds. We can't do that via on_change of product_id,
# because it is not always played when invoice is created from code
# => we inherit write/create
# We write standard_price_company_currency even on supplier invoice/refunds
# because we don't have access to the 'type' of the invoice
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('product_id') and not vals.get('display_type'):
pp = self.env['product.product'].browse(vals['product_id'])
std_price = pp.standard_price
inv_uom_id = vals.get('product_uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super().create(vals_list)
def write(self, vals):
if not vals:
vals = {}
if 'product_id' in vals or 'product_uom_id' in vals:
for il in self:
if 'product_id' in vals:
if vals.get('product_id'):
pp = self.env['product.product'].browse(
vals['product_id'])
else:
pp = False
else:
pp = il.product_id or False
# uom_id is NOT a required field
if 'product_uom_id' in vals:
if vals.get('product_uom_id'):
inv_uom = self.env['uom.uom'].browse(
vals['product_uom_id'])
else:
inv_uom = False
else:
inv_uom = il.uom_id or False
std_price = 0.0
if pp:
std_price = pp.standard_price
if inv_uom and inv_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
il.write({'standard_price_company_currency': std_price})
return super().write(vals)
class AccountMove(models.Model):
_inherit = 'account.move'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True,
currency_field='company_currency_id')
@api.depends(
'move_type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
rg_res = self.env['account.move.line'].read_group(
[
('move_id', 'in', self.ids),
('display_type', '=', False),
('exclude_from_invoice_tab', '=', False),
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
],
['move_id', 'margin_invoice_currency:sum', 'margin_company_currency:sum'],
['move_id'])
mapped_data = dict([(x['move_id'][0], {
'margin_invoice_currency': x['margin_invoice_currency'],
'margin_company_currency': x['margin_company_currency'],
}) for x in rg_res])
for move in self:
move.margin_invoice_currency = mapped_data.get(move.id, {}).get('margin_invoice_currency')
move.margin_company_currency = mapped_data.get(move.id, {}).get('margin_company_currency')

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015-2024 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_form" model="ir.ui.view">
<field name="name">margin.account.move.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<group name="sale_info_group" position="inside">
<field name="margin_invoice_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</group>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='price_total']" position="after">
<field name="standard_price_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_rate" optional="hide" string="Margin Rate (%)" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</xpath>
<xpath expr="//field[@name='invoice_line_ids']/form//field[@name='price_total']/.." position="inside">
<field name="standard_price_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="standard_price_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<label for="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<div name="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="view_invoice_tree" model="ir.ui.view">
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="arch" type="xml">
<field name="amount_residual_signed" position="after">
<field name="margin_company_currency" optional="hide" sum="1" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund')" string="Margin"/>
</field>
</field>
</record>
</odoo>

View File

@@ -11,8 +11,7 @@
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<button name="button_draft" position="before">
<button name="prepare_update_wizard" type="object" string="Update Invoice" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '=', 'entry')]}"/>
<button name="prepare_update_wizard" type="object" string="Update Entry" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '!=', 'entry')]}"/>
<button name="prepare_update_wizard" type="object" string="Update Invoice" states="posted" groups="account.group_account_invoice"/>
</button>
</field>
</record>

View File

@@ -21,7 +21,6 @@ class AccountMoveUpdate(models.TransientModel):
invoice_payment_term_id = fields.Many2one(
'account.payment.term', string='Payment Term')
ref = fields.Char(string='Reference') # field label is customized in the view
invoice_date = fields.Date()
invoice_origin = fields.Char(string='Source Document')
partner_bank_id = fields.Many2one(
'res.partner.bank', string='Bank Account')
@@ -31,7 +30,7 @@ class AccountMoveUpdate(models.TransientModel):
@api.model
def _simple_fields2update(self):
'''List boolean, date, datetime, char, text fields'''
return ['ref', 'invoice_origin', 'invoice_date']
return ['ref', 'invoice_origin']
@api.model
def _m2o_fields2update(self):
@@ -227,12 +226,6 @@ class AccountMoveUpdate(models.TransientModel):
inv.message_post(body=_(
'Non-legal fields of invoice updated via the Invoice Update '
'wizard.'))
# Purge existing PDF
report = self.env.ref("account.account_invoices")
attachment = report.retrieve_attachment(inv)
# attachment may be None
if attachment:
attachment.unlink()
return True

View File

@@ -15,15 +15,13 @@
<field name="move_type" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field string="Bill Date" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="invoice_date"/>
<field string="Supplier Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/>
<field string="Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/>
<field string="Customer Reference" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}" name="ref"/>
<field string="Ref" attrs="{'invisible': [('move_type', '!=', 'entry')]}" name="ref"/>
<field name="invoice_origin" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
<field name="invoice_origin"/>
<!-- update of payment term is broken -->
<!-- <field name="invoice_payment_term_id" widget="selection"/>-->
<field name="partner_bank_id" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
<field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
<field name="partner_bank_id"/>
<field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}"/>
</group>
<group name="lines">
<field name="line_ids" nolabel="1" widget="section_and_note_one2many">
@@ -32,8 +30,8 @@
<field name="display_type" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="name"/>
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)]}"/>
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)]}"/>
<field name="analytic_account_id" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
</tree>

View File

@@ -1,25 +0,0 @@
# Copyright 2024 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Account Payment Line Manual Account',
'version': '14.0.1.0.0',
'category': 'Accounting',
'license': 'AGPL-3',
'summary': 'Ability to select the account on payment lines without journal item',
'description': """
With this module, when you manually create a payment line that is not linked to a journal item, you can select an account (by default, it is set to the payable/receivable account of the partner) : this account will be used as the counter part of the outbound/inbound payment account configured on the bank journal. It covers special needs of a few companies that use SEPA credit transfer for the same partner in different accounting scenarios.
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'maintainers': ['alexis-via'],
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['account_payment_order'],
'data': [
"views/account_payment_line.xml",
],
'installable': True,
}

View File

@@ -1,2 +0,0 @@
from . import account_payment_line
from . import account_payment_order

View File

@@ -1,38 +0,0 @@
# Copyright 2024 Akretion France (https://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountPaymentLine(models.Model):
_inherit = "account.payment.line"
account_id = fields.Many2one(
'account.account',
compute="_compute_account_id", store=True, readonly=False, check_company=True,
domain="[('company_id', '=', company_id), ('deprecated', '=', False)]")
analytic_account_id = fields.Many2one(
'account.analytic.account', string='Analytic Account',
domain="[('company_id', 'in', [False, company_id])]",
check_company=True)
@api.depends('move_line_id', 'partner_id')
def _compute_account_id(self):
for line in self:
account_id = False
if not line.move_line_id and line.partner_id:
partner = line.partner_id.with_company(line.order_id.company_id.id)
if line.order_id.payment_type == "inbound":
account_id = partner.property_account_receivable_id.id
else:
account_id = partner.property_account_payable_id.id
line.account_id = account_id
# take info account account_id for grouping
def payment_line_hashcode(self):
hashcode = super().payment_line_hashcode()
account_str = str(self.account_id.id or False)
analytic_account_str = str(self.analytic_account_id.id or False)
hashcode = '-'.join([hashcode, account_str, analytic_account_str])
return hashcode

View File

@@ -1,18 +0,0 @@
# Copyright 2024 Akretion France (https://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
class AccountPaymentOrder(models.Model):
_inherit = "account.payment.order"
def _prepare_move_line_partner_account(self, bank_line):
vals = super()._prepare_move_line_partner_account(bank_line)
if not bank_line.payment_line_ids[0].move_line_id:
vals.update({
'account_id': bank_line.payment_line_ids[0].account_id.id,
'analytic_account_id': bank_line.payment_line_ids[0].analytic_account_id.id or False,
})
return vals

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_payment_line_form" model="ir.ui.view">
<field name="model">account.payment.line</field>
<field name="inherit_id" ref="account_payment_order.account_payment_line_form"/>
<field name="arch" type="xml">
<field name="company_id" position="after">
<field name="account_id" attrs="{'invisible': [('move_line_id', '!=', False)], 'required': [('move_line_id', '=', False)]}"/>
<field name="analytic_account_id" attrs="{'invisible': [('move_line_id', '!=', False)]}" groups="analytic.group_analytic_accounting"/>
</field>
</field>
</record>
<record id="account_payment_line_tree" model="ir.ui.view">
<field name="model">account.payment.line</field>
<field name="inherit_id" ref="account_payment_order.account_payment_line_tree"/>
<field name="arch" type="xml">
<field name="move_line_id" position="after">
<field name="account_id" optional="hide"/>
<field name="analytic_account_id" optional="hide" groups="analytic.group_analytic_accounting"/>
</field>
</field>
</record>
</odoo>

View File

@@ -32,7 +32,7 @@ This modules adds the following functions:
* A wizard to mark several invoices as sent at once (forward from v8)
* Default date for Account Move Reversal is now D+1 instead of today
* Track more fields on invoice (see details in account.py)
* Add boolean fields `has_discount` and `has_attachment` on invoice
* Add boolean fields `has_line_discount` and `has_attachment` on invoice
* Add button "Delete line qty = 0" on supplier invoice
* Cut name_get() of invoice if too long
* A script for if Odoo screws up invoice attachment filename

View File

@@ -30,7 +30,6 @@
'views/product.xml',
'views/res_config_settings.xml',
'views/res_company.xml',
'views/res_partner.xml',
'views/account_report.xml',
'views/account_reconcile_model.xml',
'wizard/account_invoice_mark_sent_view.xml',

View File

@@ -296,11 +296,16 @@ msgstr "Possède une pièce jointe"
#. module: account_usability
#: model:ir.model.fields,field_description:account_usability.field_account_bank_statement_line__has_discount
#: model:ir.model.fields,field_description:account_usability.field_account_move__has_discount
#: model:ir.model.fields,field_description:account_usability.field_account_payment__has_discount
msgid "Has Discount"
msgstr "A une réduction"
#. module: account_usability
#: model:ir.model.fields,field_description:account_usability.field_account_move__has_line_discount
msgid "Has Line Discount"
msgstr "Contient une réduction"
#. module: account_usability
#: model:ir.model.fields,field_description:account_usability.field_account_bank_statement__hide_bank_statement_balance
#: model:ir.model.fields,field_description:account_usability.field_account_journal__hide_bank_statement_balance

View File

@@ -43,6 +43,13 @@ class AccountBankStatement(models.Model):
res.append((statement.id, name))
return res
def button_reopen(self):
self = self.with_context(skip_undo_reconciliation=True)
return super().button_reopen()
def button_undo_reconciliation(self):
self.line_ids.button_undo_reconciliation()
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
@@ -89,3 +96,9 @@ class AccountBankStatementLine(models.Model):
'res_id': self.move_id.id,
})
return action
def button_undo_reconciliation(self):
if self._context.get("skip_undo_reconciliation"):
return
else:
return super().button_undo_reconciliation()

View File

@@ -6,12 +6,10 @@ from datetime import timedelta
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import float_is_zero
from odoo.tools.misc import format_date
from odoo.tools.safe_eval import safe_eval, time
from collections import defaultdict
_logger = logging.getLogger(__name__)
@@ -30,8 +28,8 @@ class AccountMove(models.Model):
fiscal_position_id = fields.Many2one(tracking=True)
amount_total = fields.Monetary(tracking=True)
# for invoice report
has_discount = fields.Boolean(
compute='_compute_has_discount', readonly=True)
has_line_discount = fields.Boolean(
compute='_compute_has_line_discount', readonly=True)
# has_attachment is useful for those who use attachment to archive
# supplier invoices. It allows them to find supplier invoices
# that don't have any attachment
@@ -42,77 +40,16 @@ class AccountMove(models.Model):
compute="_compute_sales_dates", readonly=True,
help="This information appear on invoice qweb report "
"(you may use it for your own report)")
# There is a native "blocked" field (bool) on account.move.line
# We want to have that field on invoices to improve usability
# while keeping compatibility with the standard Odoo datamodel
blocked = fields.Boolean(
compute="_compute_blocked",
inverse="_inverse_blocked",
store=True,
string="Dispute",
tracking=True,
)
# Having amounts in invoice currency can be useful in tree view of invoices
# We add those fields with optional="hide"
amount_untaxed_invoice_currency_signed = fields.Monetary(
compute="_compute_amount_invoice_currency_signed", store=True,
string="Untaxed Amount Invoice Currency Signed")
amount_tax_invoice_currency_signed = fields.Monetary(
compute="_compute_amount_invoice_currency_signed", store=True,
string="Tax Invoice Currency Signed")
amount_total_invoice_currency_signed = fields.Monetary(
compute="_compute_amount_invoice_currency_signed", store=True,
string="Total Invoice Currency Signed")
amount_residual_invoice_currency_signed = fields.Monetary(
compute="_compute_amount_invoice_currency_signed", store=True,
string="Amount Due Invoice Currency Signed")
# Field search_account_id is just for search view
search_account_id = fields.Many2one(related='line_ids.account_id')
@api.depends('amount_untaxed', 'amount_tax', 'amount_total', 'amount_residual', 'move_type')
def _compute_amount_invoice_currency_signed(self):
for move in self:
amount_untaxed_invoice_currency_signed = move.amount_untaxed
amount_tax_invoice_currency_signed = move.amount_tax
amount_total_invoice_currency_signed = move.amount_total
amount_residual_invoice_currency_signed = move.amount_residual
if move.move_type in ('out_refund', 'in_refund'):
amount_untaxed_invoice_currency_signed *= -1
amount_tax_invoice_currency_signed *= -1
amount_total_invoice_currency_signed *= -1
amount_residual_invoice_currency_signed *= -1
move.amount_untaxed_invoice_currency_signed = amount_untaxed_invoice_currency_signed
move.amount_tax_invoice_currency_signed = amount_tax_invoice_currency_signed
move.amount_total_invoice_currency_signed = amount_total_invoice_currency_signed
move.amount_residual_invoice_currency_signed = amount_residual_invoice_currency_signed
@api.depends("line_ids", "line_ids.blocked")
def _compute_blocked(self):
for move in self:
move.blocked = any(
[
l.blocked
for l in move.line_ids
if l.account_id.internal_type in ("payable", "receivable")
]
)
def _inverse_blocked(self):
for move in self:
for line in move.line_ids.filtered(
lambda l: l.account_id.internal_type in ("payable", "receivable")
):
line.blocked = move.blocked
def _compute_has_discount(self):
def _compute_has_line_discount(self):
prec = self.env['decimal.precision'].precision_get('Discount')
for inv in self:
has_discount = False
has_line_discount = False
for line in inv.invoice_line_ids:
if not line.display_type and not float_is_zero(line.discount, precision_digits=prec):
has_discount = True
has_line_discount = True
break
inv.has_discount = has_discount
inv.has_line_discount = has_line_discount
def _compute_has_attachment(self):
iao = self.env['ir.attachment']
@@ -175,16 +112,6 @@ class AccountMove(models.Model):
# self.invalidate_cache()
# return res
def _reverse_moves(self, default_values_list=None, cancel=False):
reverse_moves = super()._reverse_moves(
default_values_list=default_values_list, cancel=cancel)
# In the simple scenario 1 invoice -> 1 refund, we add a message in the chatter
# of the invoice and in the chatter of the refund
if len(self) == 1 and len(reverse_moves) == 1:
self.message_post(body=_("A reverse journal entry <a href=# data-oe-model=account.move data-oe-id=%d>%s</a> has been generated.") % (reverse_moves.id, reverse_moves.display_name))
reverse_moves.message_post(body=_("This journal entry has been generated as the reverse of <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>.") % (self.id, self.display_name))
return reverse_moves
def delete_lines_qty_zero(self):
lines = self.env['account.move.line'].search([
('display_type', '=', False),
@@ -258,14 +185,33 @@ class AccountMove(models.Model):
move.suitable_journal_ids = self.env['account.journal'].search(domain)
def button_draft(self):
# Delete attached pdf invoice
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
for report_xmlid in ('account.account_invoices', 'account.account_invoices_without_payment'):
report = self.env.ref(report_xmlid)
attach = report.retrieve_attachment(move)
if attach:
attach.unlink()
super().button_draft()
# Delete attached pdf invoice
try:
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice')
except IndexError:
report_invoice = False
if report_invoice and report_invoice.attachment:
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
# The pb is that the filename is dynamic and related to move.state
# in v12, the feature was native and they used that kind of code:
# with invoice.env.do_in_draft():
# invoice.number, invoice.state = invoice.move_name, 'open'
# attachment = self.env.ref('account.account_invoices').retrieve_attachment(invoice)
# But do_in_draft() doesn't exists in v14
# If you know how we could do that, please update the code below
attachment = self.env['ir.attachment'].search([
('name', '=', self._get_invoice_attachment_name()),
('res_id', '=', move.id),
('res_model', '=', self._name),
('type', '=', 'binary'),
], limit=1)
if attachment:
attachment.unlink()
def _get_invoice_attachment_name(self):
self.ensure_one()
return '%s.pdf' % (self.name and self.name.replace('/', '_') or 'INV')
def _get_accounting_date(self, invoice_date, has_tax):
# On vendor bills/refunds, we want date = invoice_date unless
@@ -279,40 +225,6 @@ class AccountMove(models.Model):
date = invoice_date
return date
# I don't use account_invoice_supplier_ref_unique because it adds
# a field supplier_invoice_number on account.move instead of using the native field
# cf https://github.com/OCA/account-invoicing/issues/1484
# So I take inspiration from the code of account_invoice_supplier_ref_unique
# but I use the native "ref" field
@api.constrains("ref", "partner_id")
def _check_in_invoice_ref_unique_insensitive(self):
for move in self:
if move.ref and move.is_purchase_document(
include_receipts=True
):
in_invoice_same_ref = self.search(
[
("commercial_partner_id", "=", move.commercial_partner_id.id),
("move_type", "in", ("in_invoice", "in_refund")),
("company_id", "=", move.company_id.id),
("ref", "=ilike", move.ref),
("id", "!=", move.id),
],
limit=1,
)
if in_invoice_same_ref:
raise ValidationError(
_(
"An invoice already exists in Odoo with the same "
"bill reference '%s' for the same supplier '%s': %s."
)
% (
in_invoice_same_ref.ref,
in_invoice_same_ref.partner_id.display_name,
in_invoice_same_ref.display_name,
)
)
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'

View File

@@ -16,6 +16,14 @@
<button name="button_reopen" position="attributes">
<attribute name="confirm">Are you sure ? Don't do 'Reset to New' if you just want to modify the bank journal entry of an existing statement line.</attribute>
</button>
<button name="button_reopen" position="after">
<button
name="button_undo_reconciliation"
type="object"
confirm="Are you sure to unreconcile all the entries of the bank statement?"
states="open"
string="Unreconcile All"/>
</button>
<xpath expr="//field[@name='line_ids']/tree/button[@name='button_undo_reconciliation']" position="after">
<field name="move_id" invisible="1"/>
<button name="show_account_move" type="object"

View File

@@ -47,16 +47,6 @@
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='product_id']" position="after">
<field name="product_barcode" optional="hide"/>
</xpath>
<field name="auto_post" position="before">
<field name="blocked"/>
</field>
<div role="alert" position="after">
<div id="warn_blocked" groups="account.group_account_invoice,account.group_account_readonly"
class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('move_type', 'not in', ('in_invoice', 'in_refund', 'out_invoice', 'out_refund')), ('blocked', '=', False)]}">
This <field name="move_type"/> is marked as <b>disputed</b>.
</div>
</div>
</field>
</record>
@@ -67,14 +57,6 @@
<field name="amount_residual_signed" position="attributes">
<attribute name="optional">show</attribute>
</field>
<field name="amount_untaxed_signed" position="before">
<!-- No sum="1" on the invoice currency fields, because it doesn't make sense
to add amounts in different currencies -->
<field name="amount_untaxed_invoice_currency_signed" string="Tax Excluded Inv. Cur." optional="hide"/>
<field name="amount_tax_invoice_currency_signed" string="Tax Inv. Cur." optional="hide"/>
<field name="amount_total_invoice_currency_signed" string="Total Inv. Cur." optional="hide"/>
<field name="amount_residual_invoice_currency_signed" string="Amount Due Inv. Cur." optional="hide"/>
</field>
</field>
</record>
@@ -89,8 +71,6 @@
<filter name="sent" string="Sent" domain="[('is_move_sent', '=', True), ('move_type', 'in', ('out_invoice', 'out_refund'))]"/>
<separator/>
<filter name="no_attachment" string="Missing Attachment" domain="[('has_attachment', '=', False)]"/>
<separator/>
<filter name="dispute" string="Dispute" domain="[('blocked', '=', True)]"/>
</filter>
<filter name="salesperson" position="before">
<filter name="commercial_partner_groupby" string="Commercial Partner" context="{'group_by': 'commercial_partner_id'}"/>
@@ -101,33 +81,6 @@
</field>
</record>
<record id="view_account_move_filter" model="ir.ui.view">
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_account_move_filter"/>
<field name="arch" type="xml">
<field name="journal_id" position="after">
<field name="search_account_id"/>
</field>
</field>
</record>
<record id="view_move_line_form" model="ir.ui.view">
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_form"/>
<field name="arch" type="xml">
<!-- The field 'blocked' is alone in it's block
We don't want to display an empty block, so we put the attrs on the group
The drawback of this is that, if someone added a field in that group,
he won't see the field when internal_type is not payable/receivable -->
<xpath expr="//field[@name='blocked']/.." position="attributes">
<attribute name="attrs">{'invisible': [('account_internal_type', 'not in', ('payable', 'receivable'))]}</attribute>
</xpath>
<field name="company_id" position="after">
<field name="account_internal_type" invisible="1"/>
</field>
</field>
</record>
<record id="view_move_line_tree" model="ir.ui.view">
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_tree"/>
@@ -173,18 +126,6 @@
</field>
</record>
<record id="view_move_line_pivot" model="ir.ui.view">
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_pivot"/>
<field name="arch" type="xml">
<!-- By default, date is split by month... but if you've been using Odoo for several years,
the pivot table becomes very big by default: so we split by year -->
<field name="date" position="attributes">
<attribute name="interval">year</attribute>
</field>
</field>
</record>
<record id="account.action_move_journal_line" model="ir.actions.act_window">
<field name="context">{'default_move_type': 'entry', 'view_no_maturity': True}</field>
<!-- Remove 'search_default_misc_filter': 1 -->

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_partner_property_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_holder_name']" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -15,7 +15,6 @@ class ResPartnerPhone(models.Model):
_name = 'res.partner.phone'
_order = 'partner_id, type'
_phone_name_sequence = 8
_phone_name_fields = ["phone"]
_inherit = ['phone.validation.mixin']
_description = 'Multiple emails and phones for partners'
@@ -74,7 +73,7 @@ class ResPartnerPhone(models.Model):
if self._context.get('callerid'):
name = pphone.partner_id.display_name
else:
name = '%s (%s)' % (pphone.phone, pphone.partner_id.name)
name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name)
else:
name = pphone.phone
res.append((pphone.id, name))
@@ -105,7 +104,6 @@ class ResPartnerPhone(models.Model):
class ResPartner(models.Model):
_inherit = 'res.partner'
_phone_name_fields = []
# in v10, we are supposed to have in DB E.164 format
# with the current implementation, we have:
@@ -149,8 +147,6 @@ class ResPartner(models.Model):
if vals.get(partner_field):
vals['phone_ids'].append(
(0, 0, {'type': type, partner_phone_field: vals[partner_field]}))
if partner_field in vals:
vals.pop(partner_field)
@api.model
def create(self, vals):
@@ -159,6 +155,7 @@ class ResPartner(models.Model):
self._update_create_vals(vals, '1_email_primary', 'email', 'email')
self._update_create_vals(vals, '3_phone_primary', 'phone', 'phone')
self._update_create_vals(vals, '5_mobile_primary', 'mobile', 'phone')
# self._update_create_vals(vals, '7_fax_primary', 'fax', 'phone')
return super().create(vals)
def _update_write_vals(
@@ -181,16 +178,16 @@ class ResPartner(models.Model):
else:
if pphone:
vals['phone_ids'].append((2, pphone.id))
vals.pop(partner_field)
def write(self, vals):
if 'phone_ids' not in vals:
for rec in self:
cvals = dict(vals, phone_ids=[])
rec._update_write_vals(cvals, '1_email_primary', 'email', 'email')
rec._update_write_vals(cvals, '3_phone_primary', 'phone', 'phone')
rec._update_write_vals(cvals, '5_mobile_primary', 'mobile', 'phone')
super(ResPartner, rec).write(cvals)
vals['phone_ids'] = []
rec._update_write_vals(vals, '1_email_primary', 'email', 'email')
rec._update_write_vals(vals, '3_phone_primary', 'phone', 'phone')
rec._update_write_vals(vals, '5_mobile_primary', 'mobile', 'phone')
rec._update_write_vals(vals, '7_fax_primary', 'fax', 'phone')
super(ResPartner, rec).write(vals)
return True
else:
return super().write(vals)

View File

@@ -161,12 +161,6 @@
<field name="name" position="attributes">
<attribute name="filter_domain">['|', '|', ('display_name', 'ilike', self), ('ref', '=ilike', self + '%'), ('phone_ids.email', 'ilike', self)]</attribute>
</field>
<field name="email" position="attributes">
<attribute name="filter_domain">[('phone_ids.email', 'ilike', self)]</attribute>
</field>
<field name="phone" position="attributes">
<attribute name="filter_domain">[('phone_ids.phone', 'ilike', self)]</attribute>
</field>
</field>
</record>

View File

@@ -10,7 +10,7 @@
'summary': 'Better usability in base module',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['web'],
'depends': ['base'],
'data': [
'security/group.xml',
'security/ir.model.access.csv',
@@ -21,7 +21,6 @@
'views/ir_module.xml',
'views/ir_sequence.xml',
'views/ir_property.xml',
'views/assets.xml',
],
'installable': True,
}

View File

@@ -1,13 +0,0 @@
diff --git a/odoo/tools/mimetypes.py b/odoo/tools/mimetypes.py
index d104198a4ae..6eeabcc63a3 100644
--- a/odoo/tools/mimetypes.py
+++ b/odoo/tools/mimetypes.py
@@ -123,7 +123,7 @@ _mime_mappings = (
_Entry('image/png', [b'\x89PNG\r\n\x1A\n'], []),
_Entry('image/gif', [b'GIF87a', b'GIF89a'], []),
_Entry('image/bmp', [b'BM'], []),
- _Entry('image/svg+xml', [b'<'], [
+ _Entry('application/xml', [b'<'], [
_check_svg,
]),
_Entry('image/x-icon', [b'\x00\x00\x01\x00'], []),

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
"POT-Creation-Date: 2021-07-01 10:02+0000\n"
"PO-Revision-Date: 2021-07-01 10:02+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -15,18 +15,6 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "%s with a capital of"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "APE:"
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_bank
msgid "Bank Accounts"
@@ -37,22 +25,11 @@ msgstr ""
msgid "Bank Name"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "Capital:"
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_company
msgid "Companies"
msgstr ""
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Company"
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner
msgid "Contact"
@@ -71,7 +48,6 @@ msgid "Customer Number:"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
@@ -89,24 +65,12 @@ msgstr ""
msgid "E-mail:"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "EORI:"
msgstr ""
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Field"
msgstr ""
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Group By"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__id
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id
#: model:ir.model.fields,field_description:base_usability.field_ir_model__id
#: model:ir.model.fields,field_description:base_usability.field_res_company__id
@@ -123,7 +87,6 @@ msgid "Installable"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
@@ -176,11 +139,6 @@ msgstr ""
msgid "Partner Tags"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__print_report_name
msgid "Printed Report Name"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
#: model:ir.model.fields,field_description:base_usability.field_res_users__ref
@@ -188,14 +146,8 @@ msgid "Reference"
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_ir_actions_report
msgid "Report Action"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "SIRET:"
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Search Countries"
msgstr ""
#. module: base_usability
@@ -217,14 +169,6 @@ msgstr ""
msgid "Tel:"
msgstr ""
#. module: base_usability
#: model:ir.model.fields,help:base_usability.field_ir_actions_report__print_report_name
msgid ""
"This is the filename of the report going to download. Keep empty to not "
"change the report filename. You can use a python expression with the "
"'object' and 'time' variables."
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_users
msgid "Users"

View File

@@ -6,27 +6,15 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
"Last-Translator: \n"
"POT-Creation-Date: 2021-07-01 10:02+0000\n"
"PO-Revision-Date: 2021-07-01 12:15+0200\n"
"Last-Translator: Alexis de Lattre <alexis@via.ecp.fr>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "%s with a capital of"
msgstr "%s au capital de"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "APE:"
msgstr "APE :"
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_bank
msgid "Bank Accounts"
@@ -37,22 +25,11 @@ msgstr "Comptes bancaires"
msgid "Bank Name"
msgstr "Nom de la banque"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "Capital:"
msgstr "Capital : "
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Company"
msgstr "Société"
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner
msgid "Contact"
@@ -65,13 +42,11 @@ msgstr "Devise"
#. module: base_usability
#: code:addons/base_usability/models/res_partner.py:0
#: code:addons/base_usability/models/res_partner.py:0
#, python-format
msgid "Customer Number:"
msgstr "N° client :"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
@@ -89,24 +64,12 @@ msgstr "Nom affiché"
msgid "E-mail:"
msgstr "E-mail :"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "EORI:"
msgstr "EORI :"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Field"
msgstr "Champ"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Group By"
msgstr "Grouper par"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__id
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id
#: model:ir.model.fields,field_description:base_usability.field_ir_model__id
#: model:ir.model.fields,field_description:base_usability.field_res_company__id
@@ -115,7 +78,7 @@ msgstr "Grouper par"
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__id
#: model:ir.model.fields,field_description:base_usability.field_res_users__id
msgid "ID"
msgstr ""
msgstr "ID"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.view_module_filter
@@ -123,7 +86,6 @@ msgid "Installable"
msgstr "Installable"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
@@ -174,12 +136,7 @@ msgstr "Personne (utilisé pour cacher des entrées de menu natifs)"
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_category
msgid "Partner Tags"
msgstr "Étiquettes contact"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__print_report_name
msgid "Printed Report Name"
msgstr "Nom du rapport imprimé"
msgstr "Étiquettes du partenaire"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
@@ -188,18 +145,11 @@ msgid "Reference"
msgstr "Référence"
#. module: base_usability
#: model:ir.model,name:base_usability.model_ir_actions_report
msgid "Report Action"
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Search Countries"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "SIRET:"
msgstr "SIRET :"
#. module: base_usability
#: code:addons/base_usability/models/res_partner.py:0
#: code:addons/base_usability/models/res_partner.py:0
#, python-format
msgid "Supplier Number:"
@@ -208,7 +158,7 @@ msgstr "N° fournisseur :"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__name
msgid "Tag Name"
msgstr "Libellé de l'étiquette"
msgstr "Nom de l'étiquette"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
@@ -217,14 +167,6 @@ msgstr "Libellé de l'étiquette"
msgid "Tel:"
msgstr "Tél :"
#. module: base_usability
#: model:ir.model.fields,help:base_usability.field_ir_actions_report__print_report_name
msgid ""
"This is the filename of the report going to download. Keep empty to not "
"change the report filename. You can use a python expression with the "
"'object' and 'time' variables."
msgstr ""
#. module: base_usability
#: model:ir.model,name:base_usability.model_res_users
msgid "Users"

View File

@@ -3,7 +3,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, _
from odoo.tools.misc import format_amount
class ResCompany(models.Model):
@@ -40,82 +39,32 @@ class ResCompany(models.Model):
'value': self.phone,
# http://www.fileformat.info/info/unicode/char/1f4de/index.htm
'icon': '\U0001F4DE',
'label': _('Tel:'),
},
'label': _('Tel:')},
'email': {
'value': self.email,
# http://www.fileformat.info/info/unicode/char/2709/index.htm
'icon': '\u2709',
'label': _('E-mail:'),
},
'label': _('E-mail:')},
'website': {
'value': self.website,
'icon': '\U0001f310',
'label': _('Website:'),
},
'label': _('Website:')},
'vat': {
'value': self.vat,
'label': _('VAT:'),
},
'ape': {
'value': hasattr(self, 'ape') and self.ape or False,
'label': _('APE:'),
},
'siret': {
'value': hasattr(self, 'siret') and self.siret or False,
'label': _('SIRET:'),
},
'siren': {
'value': hasattr(self, 'siren') and self.siren or False,
'label': _('SIREN:'),
},
'rcs_siren': {
'value': hasattr(self, 'siren') and self.siren and self.company_registry and f"{self.company_registry} {self.siren}",
'label': 'RCS',
},
'eori': {
'value': self._get_eori(),
'label': _('EORI:'),
},
'capital': {
# 'capital_amount' added by base_company_extension
'value': hasattr(self, 'capital_amount') and self.capital_amount and format_amount(self.env, self.capital_amount, self.currency_id) or False,
'label': _('Capital:'),
}
'label': _('VAT:')},
}
# 'legal_type' added by base_company_extension
if hasattr(self, 'legal_type') and self.legal_type:
options['capital']['label'] = _('%s with a capital of') % self.legal_type
return options
def _get_eori(self):
eori = False
if self.partner_id.country_id.code == 'FR' and hasattr(self, 'siret') and self.siret:
# Currently migrating from EORI-SIRET to EORI-SIREN :
# https://www.pwcavocats.com/fr/ealertes/ealertes-france/2023/avril/reforme-numero-eori-siren-siret.html
# But, for the moment, we continue to use EORI-SIRET
eori = f'FR{self.siret}'
return eori
def _report_company_legal_name(self):
'''Method inherited in the module base_company_extension'''
self.ensure_one()
return self.name
def _report_header_line_details(self):
"""This method is designed to be inherited"""
# I decided not to put email in the default header because only a few very small
# companies have a generic company email address
line_details = [['phone', 'website', 'rcs_siren', 'capital'], ['vat', 'siret', 'eori', 'ape']]
return line_details
# for reports
def _display_report_header(
self, line_details=None, icon=True, line_separator=' - '):
self, line_details=[['phone', 'website'], ['vat']],
icon=True, line_separator=' - '):
self.ensure_one()
if line_details is None:
line_details = self._report_header_line_details()
res = ''
address = self.partner_id._display_address(without_company=True)
address = address.replace('\n', ' - ')

View File

@@ -125,20 +125,6 @@ class ResPartner(models.Model):
'label': _('Supplier Number:'),
},
}
if hasattr(self, 'siren'):
options['siren'] = {
'value': self.siren,
'label': _("SIREN:"),
}
if hasattr(self, 'siret'):
if hasattr(self, 'siren'): # l10n_fr_siret is installed
siret = self.siren and self.nic and self.siret or False
else:
siret = self.siret
options['siret'] = {
'value': siret,
'label': _("SIRET:"),
}
res = []
for detail in details:
if options.get(detail) and options[detail]['value']:

View File

@@ -1,10 +0,0 @@
.o_form_view {
.o_address_format {
.o_address_state {
margin-right: 0;
}
.o_address_zip {
margin-right: 2%;
}
}
}

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template
id="assets_backend"
name="web_sheet_full_width"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link
rel="stylesheet"
href="/base_usability/static/src/scss/form_view.scss"
/>
</xpath>
</template>
</odoo>

View File

@@ -20,21 +20,6 @@
<div attrs="{'invisible': [('same_vat_partner_id', '=', False)]}" position="attributes">
<attribute name="class">alert alert-warning</attribute>
</div>
<!-- Native order: city / state_id / zip. New order: zip / city / state_id -->
<xpath expr="//div[hasclass('o_address_format')]/field[@name='city']" position="before">
<field name="zip" position="move"/>
</xpath>
</field>
</record>
<record id="res_partner_view_form_private" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.res_partner_view_form_private"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_holder_name']" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</xpath>
</field>
</record>

View File

@@ -16,10 +16,6 @@
<field name="bank_name" position="after">
<field name="bank_id"/>
</field>
<field name="acc_holder_name" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</field>
</field>
</record>
</odoo>

View File

@@ -1,28 +0,0 @@
diff --git a/addons/web/static/src/js/control_panel/custom_filter_item.js b/addons/web/static/src/js/control_panel/custom_filter_item.js
index 6682b660b78..b2ff23344d9 100644
--- a/addons/web/static/src/js/control_panel/custom_filter_item.js
+++ b/addons/web/static/src/js/control_panel/custom_filter_item.js
@@ -180,6 +180,10 @@ odoo.define('web.CustomFilterItem', function (require) {
[field.name, '>=', domainValue[0]],
[field.name, '<=', domainValue[1]]
);
+ } else if (operator.symbol === 'startswith') {
+ domainArray.push([field.name, '=ilike', domainValue[0] + '%']);
+ } else if (operator.symbol === 'endswith') {
+ domainArray.push([field.name, '=ilike', '%' + domainValue[0]]);
} else {
domainArray.push([field.name, operator.symbol, domainValue[0]]);
}
diff --git a/addons/web/static/src/js/control_panel/search_utils.js b/addons/web/static/src/js/control_panel/search_utils.js
index 8fce5b23ef6..d240d2e1fb2 100644
--- a/addons/web/static/src/js/control_panel/search_utils.js
+++ b/addons/web/static/src/js/control_panel/search_utils.js
@@ -18,6 +18,8 @@ odoo.define('web.searchUtils', function (require) {
char: [
{ symbol: "ilike", description: _lt("contains") },
{ symbol: "not ilike", description: _lt("doesn't contain") },
+ { symbol: "startswith", description: _lt("starts with") },
+ { symbol: "endswith", description: _lt("ends with") },
{ symbol: "=", description: _lt("is equal to") },
{ symbol: "!=", description: _lt("is not equal to") },
{ symbol: "!=", description: _lt("is set"), value: false },

View File

@@ -1,16 +0,0 @@
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index e4c1b187169..ccd8110478f 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -516,7 +516,10 @@ var FormRenderer = BasicRenderer.extend({
* @returns {integer}
*/
_renderButtonBoxNbButtons: function () {
- return [2, 2, 2, 4][config.device.size_class] || 7;
+ /* AKRETION HACK 24/04/2024
+ * show 14 buttons before adding 'More' dropdown list (instead of 7 by default
+ */
+ return [2, 2, 2, 4][config.device.size_class] || 14;
},
/**
* Do not render a field widget if it is always invisible.

View File

@@ -22,7 +22,6 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'depends': ['delivery'],
'data': [
'views/stock_picking.xml',
'views/product_packaging.xml',
],
'installable': True,
}

View File

@@ -1,4 +1 @@
from . import product_packaging
from . import stock_picking
from . import stock_move
from . import stock_quant_package

View File

@@ -1,31 +0,0 @@
# Copyright 2018-2021 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ProductPackaging(models.Model):
_inherit = 'product.packaging'
# product.packaging is defined in the 'product' module and enhanced in the 'delivery' module
# I used to make the improvements on the datamodel of product.packaging in the OCA module
# 'stock_packaging_usability_pp' from OCA/stock-logistics-tracking,
# but I eventually figured out that the feature provided by 'stock_packaging_usability_pp'
# was native in the 'delivery' module via the wizard choose.delivery.package.
# So I stopped using 'stock_packaging_usability_pp' and I moved the datamodel changes
# here in the module 'delivery_usability'
name = fields.Char(translate=True)
weight = fields.Float(digits="Stock Weight", string="Empty Package Weight")
active = fields.Boolean(default=True)
# packaging_type is important, in particular for pallets for which
# we need a special implementation to enter the height
packaging_type = fields.Selection(
[
("unit", "Unit"),
("pack", "Pack"),
("box", "Box"),
("pallet", "Pallet"),
],
string="Type",
)

View File

@@ -1,23 +0,0 @@
# Copyright 2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class StockMove(models.Model):
_inherit = "stock.move"
# Fixing bug https://github.com/odoo/odoo/issues/34702
@api.depends("product_id", "product_uom_qty", "product_uom")
def _cal_move_weight(self):
weight_uom_categ = self.env.ref("uom.product_uom_categ_kgm")
kg_uom = self.env.ref("uom.product_uom_kgm")
for move in self:
if move.product_id.uom_id.category_id == weight_uom_categ:
move.weight = move.product_id.uom_id._compute_quantity(
move.product_qty, kg_uom
)
else:
move.weight = move.product_qty * move.product_id.weight

View File

@@ -1,68 +0,0 @@
# Copyright 2019-2024 Akretion France (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo.tools import float_is_zero
class StockQuantPackage(models.Model):
_inherit = "stock.quant.package"
# These 2 fields are defined in the 'delivery' module but they forgot
# the decimal precision
shipping_weight = fields.Float(digits="Stock Weight")
weight = fields.Float(digits="Stock Weight")
# Fixing bug https://github.com/odoo/odoo/issues/34702
# and take into account the weight of the packaging
# WARNING: this method _compute_weight() is also inherited by the OCA module
# base_delivery_carrier_label so if you use that module, you should copy
# that piece of code in a custom module that depend on delivery_usability
# and base_delivery_carrier_label
def _compute_weight(self):
smlo = self.env["stock.move.line"]
weight_uom_categ = self.env.ref("uom.product_uom_categ_kgm")
kg_uom = self.env.ref("uom.product_uom_kgm")
weight_prec = self.env['decimal.precision'].precision_get('Stock Weight')
for package in self:
# if the weight of the package has been measured,
# it is written in shipping_weight
if not float_is_zero(package.shipping_weight, precision_digits=weight_prec):
weight = package.shipping_weight
# otherwise, we compute the theorical weight from the weight of the products
# and the weight of the packaging
# Since Odoo v11, consu products don't create quants, so I can't loop
# on pack.quant_ids to get all the items inside a package: I have to
# get the picking, then loop on the stock.move.line of that picking
# linked to that package
else:
weight = 0.0
# the package can be seen in a return
# So I get the picking of it's first appearance
domain = [
("result_package_id", "=", package.id),
("product_id", "!=", False),
]
first_move_line = smlo.search(
domain + [('picking_id', '!=', False)], limit=1, order='id')
if first_move_line:
picking_id = first_move_line.picking_id.id
current_picking_move_line_ids = smlo.search(
domain + [("picking_id", "=", picking_id)])
for ml in current_picking_move_line_ids:
if ml.product_uom_id.category_id == weight_uom_categ:
weight += ml.product_uom_id._compute_quantity(
ml.qty_done, kg_uom
)
else:
weight += (
ml.product_uom_id._compute_quantity(
ml.qty_done, ml.product_id.uom_id
)
* ml.product_id.weight
)
if package.packaging_id:
weight += package.packaging_id.weight
package.weight = weight

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2019-2024 Akretion France (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<!-- I don't know why the form view of product.packaging in the delivery
module has "<field name="inherit_id" eval="False"/>"
instead of a standard inherit of product.product_packaging_form_view -->
<record id="product_packaging_delivery_form" model="ir.ui.view">
<field name="name">stock_packaging_usability_pp.product.packaging.form</field>
<field name="model">product.packaging</field>
<field name="inherit_id" ref="delivery.product_packaging_delivery_form" />
<field name="arch" type="xml">
<field name="package_carrier_type" position="after">
<field name="packaging_type" />
<field name="active" invisible="1" />
</field>
<label for="max_weight" position="before">
<label for="weight" />
<div class="o_row" name="weight">
<field name="weight" />
<span><field name="weight_uom_name" /></span>
</div>
</label>
<label for="name" position="before">
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
</label>
</field>
</record>
<record id="product_packaging_delivery_tree" model="ir.ui.view">
<field name="name">stock_packaging_usability_pp.product.packaging.tree</field>
<field name="model">product.packaging</field>
<field name="inherit_id" ref="delivery.product_packaging_delivery_tree" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="packaging_type" optional="show" />
</field>
<field name="max_weight" position="before">
<field name="weight" optional="show" />
</field>
</field>
</record>
<!-- There is no native serch view for product.packaging -->
<record id="product_packaging_search" model="ir.ui.view">
<field name="name">product.packaging.search</field>
<field name="model">product.packaging</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<separator />
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<group name="groupby">
<filter
name="packaging_type_groupby"
string="Packaging Type"
context="{'group_by': 'packaging_type'}"
/>
</group>
</search>
</field>
</record>
</odoo>

View File

@@ -1 +1,4 @@
# -*- coding: utf-8 -*-
from . import intrastat_product_type
from .post_install import set_intrastat_type_on_products

View File

@@ -4,7 +4,7 @@
{
'name': 'Intrastat Product Type',
'version': '14.0.1.0.0',
'version': '12.0.1.0.0',
'category': 'Accounting',
'license': 'AGPL-3',
'summary': 'Adds a special field Intrastat Type on Products',
@@ -19,8 +19,9 @@ This module adds a field *Intrastat Type* on the Product Form with 2 possible op
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'website': 'http://www.akretion.com',
'depends': ['intrastat_product', 'l10n_fr_intrastat_service'],
'data': ['product_view.xml'],
'installable': True,
'post_init_hook': 'set_intrastat_type_on_products',
'installable': False,
}

View File

@@ -1,4 +1,5 @@
# Copyright 2016-2023 Akretion (http://www.akretion.com)
# -*- coding: utf-8 -*-
# Copyright 2016-2019 Akretion (http://www.akretion.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
# @author Alexis de Lattre <alexis.delattre@akretion.com>
@@ -13,36 +14,50 @@ class ProductTemplate(models.Model):
intrastat_type = fields.Selection([
('product', 'Product'),
('service', 'Service'),
],
compute='_compute_intrastat_type', readonly=False, store=True,
required=True, string='Intrastat Type',
], string='Intrastat Type', default='product', required=True,
help="Type of product used for the intrastat declarations. "
"For example, you can configure a product with "
"'Product Type' = 'Consumable' and 'Intrastat Type' = 'Service'.")
@api.multi
@api.constrains('type', 'intrastat_type')
def check_intrastat_type(self):
for pt in self:
if pt.intrastat_type == 'product' and pt.type == 'service':
raise ValidationError(_(
"On the product '%s', you cannot set Product Type to "
"'Service' and Intrastat Type to 'Product'.")
% pt.display_name)
"On the product %s, you cannot set Product Type to "
"'Service' and Intrastat Type to 'Product'.") % pt.name)
if pt.intrastat_type == 'service' and pt.type == 'product':
raise ValidationError(_(
"On the product '%s', you cannot set Intrastat Type to "
"On the product %s, you cannot set Intrastat Type to "
"'Service' and Product Type to 'Stockable product' "
"(but you can set Product Type to 'Consumable' or "
"'Service').") % pt.display_name)
"'Service').") % pt.name)
@api.depends('type')
def _compute_intrastat_type(self):
for pt in self:
if pt.type in ('product', 'consu'):
intrastat_type = 'product'
else:
intrastat_type = 'service'
pt.intrastat_type = intrastat_type
@api.onchange('type')
def intrastat_type_onchange(self):
if self.type in ('product', 'consu'):
self.intrastat_type = 'product'
elif self.type == 'service':
self.intrastat_type = 'service'
@api.model
def create(self, vals):
if vals.get('type'):
if not vals.get('intrastat_type'):
if vals['type'] in ('product', 'consu'):
vals['intrastat_type'] = 'product'
elif vals['type'] == 'service':
vals['intrastat_type'] = 'service'
elif (
vals.get('intrastat_type') == 'product' and
vals['type'] == 'service'):
# usefull because intrastat_type = 'product' by default and
# wizards in other modules that don't depend on this module
# (e.g. sale_rental) may create a product with only
# {'type': 'service'} and no 'intrastat_type'
vals['intrastat_type'] = 'service'
return super(ProductTemplate, self).create(vals)
class L10nFrIntrastatServiceDeclaration(models.Model):

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2019 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def set_intrastat_type_on_products(cr, registry):
cr.execute(
"UPDATE product_template SET intrastat_type='service' "
"WHERE type='service'")
return

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016-2023 Akretion France (http://www.akretion.com/)
Copyright 2016-2019 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->

View File

@@ -20,10 +20,10 @@ This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'website': 'http://www.akretion.com',
'depends': ['mass_mailing', 'link_tracker_usability'],
'data': [
'views/link_tracker.xml',
# 'views/link_tracker.xml',
],
'installable': True,
'installable': False,
}

View File

@@ -1 +0,0 @@
from . import mailing_mailing

View File

@@ -1,14 +0,0 @@
# Copyright 2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MassMailing(models.Model):
_inherit = 'mailing.mailing'
def action_view_clicked(self):
action = super().action_view_clicked()
action["view_mode"] = "tree,form"
return action

View File

@@ -8,13 +8,39 @@
<odoo>
<record id="link_tracker_click_view_tree" model="ir.ui.view">
<record id="view_link_tracker_click_tree" model="ir.ui.view">
<field name="name">mm.usability.link.tracker.click.tree</field>
<field name="model">link.tracker.click</field>
<field name="inherit_id" ref="mass_mailing.link_tracker_click_view_tree"/>
<field name="inherit_id" ref="link_tracker.view_link_tracker_click_tree"/>
<field name="arch" type="xml">
<field name="mass_mailing_id" position="after">
<field name="mailing_trace_id" optional="hide"/>
<field name="country_id" position="after">
<field name="mass_mailing_id"/>
<field name="mail_stat_recipient"/>
</field>
</field>
</record>
<record id="view_link_tracker_click_form" model="ir.ui.view">
<field name="name">mm.usability.link.tracker.click.form</field>
<field name="model">link.tracker.click</field>
<field name="inherit_id" ref="link_tracker.view_link_tracker_click_form"/>
<field name="arch" type="xml">
<field name="country_id" position="after">
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="mail_stat_id"/>
<field name="mail_stat_recipient"/>
</field>
</field>
</record>
<record id="link_tracker_click_search" model="ir.ui.view">
<field name="name">mm.usability.link.tracker.click.search</field>
<field name="model">link.tracker.click</field>
<field name="inherit_id" ref="link_tracker_usability.link_tracker_click_search"/>
<field name="arch" type="xml">
<field name="link_id" position="after">
<field name="mail_stat_recipient"/>
</field>
</field>
</record>

View File

@@ -1,10 +1,10 @@
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'MRP Average Cost',
'version': '14.0.1.0.0',
'version': '12.0.1.0.0', # WARNING: we'll probably not port this module to v14, because part of its feature is now provided by the module mrp_account
'category': 'Manufactuing',
'license': 'AGPL-3',
'summary': 'Update standard_price upon validation of a manufacturing order',
@@ -12,22 +12,22 @@
MRP Average Cost
================
I initially developped this module for Odoo 12.0, when the module mrp_account didn't exist, so Odoo didn't support the update of the standard cost of a manufactured product.
By default, the official stock module updates the standard_price of a product that has costing_method = 'average' when validating an incoming picking. But the official 'mrp' module doesn't do that when you validate a manufactuging order.
In the mrp_account module, you must use workcenters to take the labor costs into account. This module aims at encoding theorical labor costs on the BOM and using it to compute the cost of the finished product.
This module adds this feature : when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM.
With this module, when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM plus the extra cost defined of the BOM.
Together with this module, I recommend the use of my module product_usability, available in the same branch, which contains a backport of the model product.price.history from v8 to v7.
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['mrp_account'],
'website': 'http://www.akretion.com',
'depends': ['mrp'],
'data': [
'security/mrp_average_cost_security.xml',
'security/ir.model.access.csv',
'data/mrp_data.xml',
'views/mrp_view.xml',
],
'installable': True,
'installable': False,
}

View File

@@ -1,9 +1,11 @@
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo.tools import float_compare
import odoo.addons.decimal_precision as dp
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
import logging
logger = logging.getLogger(__name__)
@@ -15,19 +17,23 @@ class MrpBomLabourLine(models.Model):
bom_id = fields.Many2one(
comodel_name='mrp.bom',
string='Bill of Material',
string='Labour Lines',
ondelete='cascade')
labour_time = fields.Float(
string='Labour Time',
required=True,
digits='Labour Hours',
digits=dp.get_precision('Labour Hours'),
help="Average labour time for the production of "
"items of the BOM, in hours.")
labour_cost_profile_id = fields.Many2one(
comodel_name='labour.cost.profile',
string='Labour Cost Profile',
required=True)
note = fields.Text()
note = fields.Text(
string='Note')
_sql_constraints = [(
'labour_time_positive',
@@ -38,26 +44,6 @@ class MrpBomLabourLine(models.Model):
class MrpBom(models.Model):
_inherit = 'mrp.bom'
labour_line_ids = fields.One2many(
'mrp.bom.labour.line', 'bom_id', string='Labour Lines')
total_labour_cost = fields.Float(
compute='_compute_total_labour_cost', digits='Product Price', store=True)
extra_cost = fields.Float(
tracking=True, digits='Product Price',
help="Extra cost for the production of the quantity of "
"items of the BOM, in company currency. "
"You can use this field to enter the cost of the consumables "
"that are used to produce the product but are not listed in "
"the BOM")
total_components_cost = fields.Float(
compute='_compute_total_cost', digits='Product Price')
total_cost = fields.Float(
compute='_compute_total_cost', digits='Product Price',
help="Total cost for the quantity and unit of measure of the bill of material. "
"Total Cost = Total Components Cost + Total Labour Cost + Extra Cost")
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency')
@api.depends(
'labour_line_ids.labour_time',
'labour_line_ids.labour_cost_profile_id.hour_cost')
@@ -84,77 +70,107 @@ class MrpBom(models.Model):
bom.total_components_cost = comp_cost
bom.total_cost = total_cost
labour_line_ids = fields.One2many(
'mrp.bom.labour.line', 'bom_id', string='Labour Lines')
total_labour_cost = fields.Float(
compute='_compute_total_labour_cost', readonly=True,
digits=dp.get_precision('Product Price'),
string="Total Labour Cost", store=True)
extra_cost = fields.Float(
string='Extra Cost', track_visibility='onchange',
digits=dp.get_precision('Product Price'),
help="Extra cost for the production of the quantity of "
"items of the BOM, in company currency. "
"You can use this field to enter the cost of the consumables "
"that are used to produce the product but are not listed in "
"the BOM")
total_components_cost = fields.Float(
compute='_compute_total_cost', readonly=True,
digits=dp.get_precision('Product Price'),
string='Total Components Cost')
total_cost = fields.Float(
compute='_compute_total_cost', readonly=True,
string='Total Cost',
digits=dp.get_precision('Product Price'),
help="Total Cost = Total Components Cost + "
"Total Labour Cost + Extra Cost")
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency')
@api.model
def _phantom_update_product_standard_price(self):
logger.info('Start to auto-update cost price from phantom boms')
logger.info('Start to auto-update cost price from phantom bom')
boms = self.search([('type', '=', 'phantom')])
boms.manual_update_product_standard_price()
logger.info('End of the auto-update cost price from phantom boms')
boms.with_context(
product_price_history_origin='Automatic update of Phantom BOMs')\
.manual_update_product_standard_price()
logger.info('End of the auto-update cost price from phantom bom')
return True
def manual_update_product_standard_price(self):
prec = self.env['decimal.precision'].precision_get(
if 'product_price_history_origin' not in self._context:
self = self.with_context(
product_price_history_origin='Manual update from BOM')
precision = self.env['decimal.precision'].precision_get(
'Product Price')
for bom in self:
if bom.product_id:
products = bom.product_id
else:
products = bom.product_tmpl_id.product_variant_ids
for product in products:
standard_price = product._compute_bom_price(bom)
if float_compare(product.standard_price, standard_price, precision_digits=prec):
product.write({'standard_price': standard_price})
logger.info(
'Cost price updated to %s on product %s',
standard_price, product.display_name)
wproduct = bom.product_id
if not wproduct:
wproduct = bom.product_tmpl_id
if float_compare(
wproduct.standard_price, bom.total_cost,
precision_digits=precision):
wproduct.with_context().write(
{'standard_price': bom.total_cost})
logger.info(
'Cost price updated to %s on product %s',
bom.total_cost, wproduct.display_name)
return True
class MrpBomLine(models.Model):
_inherit = 'mrp.bom.line'
standard_price = fields.Float(related='product_id.standard_price')
class ProductProduct(models.Model):
_inherit = 'product.product'
def _compute_bom_price(self, bom, boms_to_recompute=False):
# Native method of mrp_account
# WARNING dirty hack ; I hope it doesn't break too many things
self.ensure_one()
bom_cost_per_unit_in_product_uom = 0
qty_product_uom = bom.product_uom_id._compute_quantity(bom.product_qty, self.uom_id)
if qty_product_uom:
bom_cost_per_unit_in_product_uom = bom.total_cost / qty_product_uom
return bom_cost_per_unit_in_product_uom
standard_price = fields.Float(
related='product_id.standard_price',
readonly=True,
string='Standard Price')
class LabourCostProfile(models.Model):
_name = 'labour.cost.profile'
_inherit = ['mail.thread', 'mail.activity.mixin']
_inherit = ['mail.thread']
_description = 'Labour Cost Profile'
name = fields.Char(
string='Name',
required=True,
tracking=True)
track_visibility='onchange')
hour_cost = fields.Float(
string='Cost per Hour',
required=True,
digits='Product Price',
tracking=True,
digits=dp.get_precision('Product Price'),
track_visibility='onchange',
help="Labour cost per hour per person in company currency")
company_id = fields.Many2one(
comodel_name='res.company', required=True,
default=lambda self: self.env.company)
comodel_name='res.company',
string='Company',
required=True,
default=lambda self: self.env['res.company']._company_default_get())
company_currency_id = fields.Many2one(
related='company_id.currency_id', store=True, string='Company Currency')
related='company_id.currency_id',
readonly=True,
store=True,
string='Company Currency')
@api.depends('name', 'hour_cost', 'company_currency_id.symbol')
def name_get(self):
res = []
for record in self:
res.append((record.id, '%s (%s %s)' % (
res.append((record.id, u'%s (%s %s)' % (
record.name, record.hour_cost,
record.company_currency_id.symbol)))
return res
@@ -163,27 +179,93 @@ class LabourCostProfile(models.Model):
class MrpProduction(models.Model):
_inherit = 'mrp.production'
unit_cost = fields.Float(
string='Unit Cost', readonly=True,
digits=dp.get_precision('Product Price'),
help="This cost per unit in the unit of measure of the product "
"in company currency takes into account "
"the cost of the raw materials and the labour cost defined on"
"the BOM.")
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency')
# extra_cost is a native field of mrp_account
# we convert it to a computed field
# extra_cost is per unit in the UoM of mrp.production (product_uom_id)
extra_cost = fields.Float(
compute='_compute_extra_cost', store=True, readonly=False,
help="For a regular production order, it takes into account the labor cost "
"and the extra cost defined on the bill of material.")
related='company_id.currency_id', readonly=True,
string='Company Currency')
# Strategy for v14 : we write labor costs and bom's extra cost on the native field extra_cost
# of mrp.production => it is automatically added by the code of mrp_account
def compute_order_unit_cost(self):
self.ensure_one()
mo_total_price = 0.0 # In the UoM of the M0
labor_cost_per_unit = 0.0 # In the UoM of the product
extra_cost_per_unit = 0.0 # In the UoM of the product
subcontract_cost_per_unit = 0.0
# I read the raw materials MO, not on BOM, in order to make
# it work with the "dynamic" BOMs (few raw material are auto-added
# on the fly on MO)
prec = self.env['decimal.precision'].precision_get(
'Product Unit of Measure')
for raw_smove in self.move_raw_ids:
# I don't filter on state, in order to make it work with
# partial productions
# For partial productions, mo.product_qty is not updated
# so we compute with fully qty and we compute with all raw
# materials (consumed or not), so it gives a good price
# per unit at the end
raw_price = raw_smove.product_id.standard_price
raw_material_cost = raw_price * raw_smove.product_qty
logger.info(
'MO %s product %s: raw_material_cost=%s',
self.name, raw_smove.product_id.display_name,
raw_material_cost)
mo_total_price += raw_material_cost
if self.bom_id:
bom = self.bom_id
# if not bom.total_labour_cost:
# raise orm.except_orm(
# _('Error:'),
# _("Total Labor Cost is 0 on bill of material '%s'.")
# % bom.name)
if float_is_zero(bom.product_qty, precision_digits=prec):
raise UserError(_(
"Missing Product Quantity on bill of material '%s'.")
% bom.display_name)
bom_qty_product_uom = bom.product_uom_id._compute_quantity(
bom.product_qty, bom.product_tmpl_id.uom_id)
assert bom_qty_product_uom > 0, 'BoM qty should be positive'
labor_cost_per_unit = bom.total_labour_cost / bom_qty_product_uom
extra_cost_per_unit = bom.extra_cost / bom_qty_product_uom
if bom.type == 'subcontract':
one_finished_move = self.env['stock.move'].search([
('production_id', '=', self.id),
('product_id', '=', self.product_id.id),
('move_dest_ids', '!=', False)], limit=1)
if one_finished_move:
subcontract_cost_per_unit = one_finished_move.move_dest_ids[0].price_unit
# mo_standard_price and labor_cost_per_unit are
# in the UoM of the product (not of the MO/BOM)
mo_qty_product_uom = self.product_uom_id._compute_quantity(
self.product_qty, self.product_id.uom_id)
assert mo_qty_product_uom > 0, 'MO qty should be positive'
mo_standard_price = mo_total_price / mo_qty_product_uom
logger.info(
'MO %s: labor_cost_per_unit=%s extra_cost_per_unit=%s '
'subcontract_cost_per_unit=%s',
self.name, labor_cost_per_unit, extra_cost_per_unit,
subcontract_cost_per_unit)
mo_standard_price += labor_cost_per_unit
mo_standard_price += extra_cost_per_unit
mo_standard_price += subcontract_cost_per_unit
return mo_standard_price
@api.depends('bom_id', 'product_id')
def _compute_extra_cost(self):
for prod in self:
bom = prod.bom_id
if bom and bom.type == 'normal' and bom.product_uom_id.category_id == prod.product_uom_id.category_id:
extra_cost_bom_qty_uom = bom.extra_cost + bom.total_labour_cost
extra_cost_per_unit_in_prod_uom = 0
qty_prod_uom = bom.product_uom_id._compute_quantity(bom.product_qty, prod.product_uom_id)
if qty_prod_uom:
extra_cost_per_unit_in_prod_uom = extra_cost_bom_qty_uom / qty_prod_uom
prod.extra_cost = extra_cost_per_unit_in_prod_uom
def post_inventory(self):
'''This is the method where _action_done() is called on finished move
So we write on 'price_unit' of the finished move and THEN we call
super() which will call _action_done() which itself calls
product_price_update_before_done()'''
for order in self:
if order.product_id.cost_method == 'average':
unit_cost = order.compute_order_unit_cost()
order.write({'unit_cost': unit_cost})
logger.info('MO %s: unit_cost=%s', order.name, unit_cost)
order.move_finished_ids.filtered(
lambda x: x.product_id == order.product_id).write({
'price_unit': unit_cost})
return super(MrpProduction, self).post_inventory()

View File

@@ -4,7 +4,7 @@
<record id="labour_cost_profile_rule" model="ir.rule">
<field name="name">Labour Cost Profile multi-company</field>
<field name="model_id" ref="model_labour_cost_profile"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</odoo>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016-2024 Akretion (http://www.akretion.com/)
Copyright (C) 2016-2019 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
@@ -13,25 +13,23 @@
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='miscellaneous']/group" position="inside">
<group name="costs">
<field name="total_components_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<field name="total_labour_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<field name="extra_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<label for="total_cost"/>
<div>
<field name="total_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"
class="oe_inline"/>
<button type="object" name="manual_update_product_standard_price"
string="Update Cost Price of Product" class="oe_link"/>
</div>
<field name="company_currency_id" invisible="1"/>
</group>
</xpath>
<field name="picking_type_id" position="after">
<field name="total_components_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<field name="total_labour_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<field name="extra_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"/>
<label for="total_cost"/>
<div>
<field name="total_cost" widget="monetary"
options="{'currency_field': 'company_currency_id'}"
class="oe_inline"/>
<button type="object" name="manual_update_product_standard_price"
string="Update Cost Price of Product" class="oe_link"/>
</div>
<field name="company_currency_id" invisible="1"/>
</field>
<notebook position="inside">
<page string="Labour" name="labour_lines">
<group name="labour_lines_grp">
@@ -119,10 +117,10 @@
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
<field name="model">mrp.production</field>
<field name="arch" type="xml">
<xpath expr="//page[@name='miscellaneous']//field[@name='origin']/.." position="inside">
<field name="extra_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
<field name="availability" position="after">
<field name="unit_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}" attrs="{'invisible': [('state', '!=', 'done')]}"/>
<field name="company_currency_id" invisible="1"/>
</xpath>
</field>
</field>
</record>

View File

@@ -4,7 +4,7 @@
{
'name': 'MRP No Product Template Menu',
'version': '14.0.1.0.0',
'version': '12.0.1.0.0',
'category': 'Manufacturing',
'license': 'AGPL-3',
'summary': "Replace product.template menu entries by product.product menu",
@@ -22,9 +22,9 @@ This module has been written by Alexis de Lattre
from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'website': 'http://www.akretion.com',
'depends': ['mrp', 'sale_purchase_no_product_template_menu'],
'auto_install': True,
'data': ['mrp_view.xml'],
'installable': True,
'installable': False,
}

View File

@@ -1,18 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016-2024 Akretion France (http://www.akretion.com/)
Copyright 2016-2019 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="mrp.menu_mrp_product_form" model="ir.ui.menu">
<field name="action" ref="product.product_normal_action"/>
<record id="product_product_action_mrp" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form,kanban</field>
<field name="context">{'search_default_consumable': 1, 'default_type': 'product'}</field>
</record>
<record id="mrp.menu_mrp_product_form" model="ir.ui.menu">
<field name="action" ref="product_product_action_mrp"/>
</record>
<!-- we don't care about:
"search_default_consumable": 1 => not useful
"default_type": 'product' : stock_usability make it the default... no need to bother
-->
</odoo>

View File

@@ -1,20 +0,0 @@
# Copyright 2023 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'MRP Subcontracting Usability',
'version': '14.0.1.0.0',
'category': 'Manufacturing',
'license': 'AGPL-3',
'summary': 'Usability improvements on mrp_subcontracting',
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['mrp_subcontracting', 'stock_usability'],
'data': [
'views/mrp_bom.xml',
'views/stock_move.xml',
'views/stock_picking.xml',
],
'installable': True,
}

View File

@@ -1 +0,0 @@
from . import stock_move

View File

@@ -1,21 +0,0 @@
# Copyright 2024 Akretion France (https://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class StockMove(models.Model):
_inherit = 'stock.move'
subcontracting_production_id = fields.Many2one(
'mrp.production',
compute='_compute_subcontracting_production_id',
)
def _compute_subcontracting_production_id(self):
for move in self:
subcontracting_production_id = False
if move.is_subcontract and move.move_orig_ids.production_id:
subcontracting_production_id = move.move_orig_ids.production_id[-1:].id
move.subcontracting_production_id = subcontracting_production_id

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_mrp_bom_filter" model="ir.ui.view">
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.view_mrp_bom_filter"/>
<field name="arch" type="xml">
<filter name="phantom" position="after">
<filter name="subcontract" domain="[('type', '=', 'subcontract')]" string="Subcontracting"/>
</filter>
</field>
</record>
</odoo>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_form" model="ir.ui.view">
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_form"/>
<field name="arch" type="xml">
<group name="advanced" position="inside">
<field name="is_subcontract" readonly="1" attrs="{'invisible': [('is_subcontract', '=', False)]}" groups="mrp.group_mrp_manager"/>
<field name="subcontracting_production_id" attrs="{'invisible': [('is_subcontract', '=', False)]}" groups="mrp.group_mrp_manager"/>
</group>
</field>
</record>
</odoo>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_picking_form" model="ir.ui.view">
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='move_ids_without_package']/form/group" position="inside">
<field name="is_subcontract" readonly="1" attrs="{'invisible': [('is_subcontract', '=', False)]}" groups="mrp.group_mrp_manager"/>
<field name="subcontracting_production_id" attrs="{'invisible': [('is_subcontract', '=', False)]}" groups="mrp.group_mrp_manager"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -14,7 +14,6 @@
'data': [
'views/mrp_production.xml',
'views/product_template.xml',
'views/stock_move_line.xml',
# 'report/mrp_report.xml' # TODO
],
'installable': True,

View File

@@ -3,27 +3,12 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo import api, models
class MrpProduction(models.Model):
_inherit = 'mrp.production'
# Allow to change the destination location until 'Mark as done'.
# Native behavior: it is only possible to change it in draft state.
location_dest_id = fields.Many2one(states={
'draft': [('readonly', False)], # native
'confirmed': [('readonly', False)], # added
'progress': [('readonly', False)], # added
'to_close': [('readonly', False)], # added
}, tracking=True)
# Add field product_categ_id for reporting only
product_categ_id = fields.Many2one(related='product_id.categ_id', store=True)
lot_producing_id = fields.Many2one(tracking=True)
location_src_id = fields.Many2one(tracking=True)
location_dest_id = fields.Many2one(tracking=True)
bom_id = fields.Many2one(tracking=True)
# Method used by the report, inherited in this module
@api.model
def get_stock_move_sold_out_report(self, move):

View File

@@ -16,9 +16,8 @@
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_src_id']" position="replace"/>
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_dest_id']" position="replace"/>
<field name="bom_id" position="after">
<!-- no need to set readonly via attrs, the readonly status is in the field definition -->
<field name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" />
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}"/>
<field name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
</field>
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='date_deadline']" position="after">
<field name="date_start"/>
@@ -41,20 +40,4 @@
</field>
</record>
<!-- Menu Manufacturing > Reporting > Manufacturing orders -->
<record id="mrp.mrp_production_report" model="ir.actions.act_window">
<!-- Change order: pivot first instead of graph -->
<field name="view_mode">pivot,graph,form</field>
</record>
<record id="view_production_pivot" model="ir.ui.view">
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.view_production_pivot"/>
<field name="arch" type="xml">
<pivot position="inside">
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
</odoo>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_line_form" model="ir.ui.view">
<field name="name">mrp_usability.stock.move.line.form</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_move_line_form" />
<field name="arch" type="xml">
<field name="reference" position="before">
<field name="production_id" attrs="{'invisible': [('production_id', '=', False)]}"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1 +0,0 @@
from . import res_partner

View File

@@ -1,27 +0,0 @@
# Copyright 2014-2024 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Partner Product Shortcut',
'version': '14.0.1.0.0',
'category': 'Contact Management',
'license': 'AGPL-3',
'summary': 'Adds a shortcut on partner form to the products supplied by this partner',
'description': """
Partner Product Shortcut
========================
Adds a smartbutton on partner form to the products supplied by this partner.
This is an alternative to the OCA module `partner_supplierinfo_smartbutton <https://github.com/OCA/purchase-workflow/tree/14.0/partner_supplierinfo_smartbutton>`_ which adds a smartbutton on partner form to links to the related product.supplierinfo (and not to product.template like in this module).
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['product'],
'data': ['res_partner_view.xml'],
'installable': True,
}

View File

@@ -1,38 +0,0 @@
# Copyright 2014-2024 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, _
class ResPartner(models.Model):
_inherit = 'res.partner'
def _product_supplied_count(self):
for partner in self:
count = 0
sellers = self.env['product.supplierinfo'].search(
[('name', '=', partner.id)])
if sellers:
count = self.env['product.template'].search_count(
[('seller_ids', 'in', sellers.ids)])
partner.product_supplied_count = count
product_supplied_count = fields.Integer(
compute='_product_supplied_count', string="# of Products Supplied",
)
def show_supplied_products(self):
self.ensure_one()
sellers = self.env['product.supplierinfo'].search(
[('name', '=', self.id)])
ptemplates = self.env['product.template'].search(
[('seller_ids', 'in', sellers.ids)])
action = {
'name': _('Products'),
'type': "ir.actions.act_window",
"res_model": "product.template",
"view_mode": 'tree,kanban,form',
'domain': f"[('id', 'in', {ptemplates.ids})]",
}
return action

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2014-2024 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">add.shortcut.to.product.variants.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_inline oe_stat_button" type="object"
name="show_supplied_products"
icon="fa-shopping-basket"
attrs="{'invisible': [('product_supplied_count', '=', 0)]}">
<field string="Products Supplied" name="product_supplied_count"
widget="statinfo"/>
</button>
</div>
</field>
</record>
</odoo>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -31,11 +31,7 @@ Akretion:
"views/report_pos_order.xml",
"views/pos_category.xml",
"views/pos_session.xml",
"views/pos_order.xml",
"views/pos_config.xml",
"views/product.xml",
"views/pos_payment_method.xml",
"views/stock_warehouse.xml",
],
"installable": True,
}

View File

@@ -1,4 +1,3 @@
from . import product
from . import pos_category
from . import pos_config
from . import pos_payment_method

View File

@@ -1,12 +0,0 @@
# Copyright 2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class PosConfig(models.Model):
_inherit = 'pos.config'
_order = 'sequence, id'
sequence = fields.Integer(default=10)

View File

@@ -8,7 +8,5 @@ from odoo import fields, models
class PosPaymentMethod(models.Model):
_inherit = 'pos.payment.method'
_check_company_auto = True
_order = 'sequence, id'
cash_journal_id = fields.Many2one(check_company=True)
sequence = fields.Integer(default=10)

View File

@@ -1,15 +0,0 @@
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index 55aa635aa10..3ae56105b44 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -169,7 +169,9 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
}
}
async _finalizeValidation() {
- if ((this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer) {
+ //if ((this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer) {
+ // Always open cashbox (by default, Odoo only opens cashbox for cash payments)
+ if (this.env.pos.config.iface_cashdrawer) {
this.env.pos.proxy.printer.open_cashbox();
}

View File

@@ -1,35 +0,0 @@
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index 55aa635aa10..d91ea933a71 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -8,6 +8,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
const { useListener } = require('web.custom_hooks');
const Registries = require('point_of_sale.Registries');
const { onChangeOrder } = require('point_of_sale.custom_hooks');
+ const utils = require('web.utils');
class PaymentScreen extends PosComponent {
constructor() {
@@ -20,6 +21,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
useListener('send-payment-cancel', this._sendPaymentCancel);
useListener('send-payment-reverse', this._sendPaymentReverse);
useListener('send-force-done', this._sendForceDone);
+ useListener('validate-order', () => this.validateOrder(false));
this.lockedValidateOrder = useAsyncLockedMethod(this.validateOrder);
NumberBuffer.use(this._getNumberBufferConfig);
onChangeOrder(this._onPrevOrder, this._onNewOrder);
@@ -333,6 +335,14 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
if (isPaymentSuccessful) {
line.set_payment_status('done');
line.can_be_reversed = payment_terminal.supports_reversals;
+ // Automatically validate the order if, after an electronic payment,
+ // the current order is fully paid (BACKPORT FROM v16)
+ if (
+ this.currentOrder.is_paid() &&
+ utils.float_is_zero(this.currentOrder.get_due(), this.env.pos.currency.decimals)
+ ) {
+ this.trigger('validate-order');
+ }
} else {
line.set_payment_status('retry');
}

View File

@@ -1,69 +0,0 @@
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 828d0bcf0f4..fd4a9e464f5 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -332,14 +332,16 @@ class PosSession(models.Model):
self._create_picking_at_end_of_session()
# Users without any accounting rights won't be able to create the journal entry. If this
# case, switch to sudo for creation and posting.
- try:
- with self.env.cr.savepoint():
- self.with_company(self.company_id)._create_account_move()
- except AccessError as e:
- if sudo:
- self.sudo().with_company(self.company_id)._create_account_move()
- else:
- raise e
+ # AKRETION HACK 20/10/2023 disable savepoint() because I get some
+ # crash upon pos session closing
+ #try:
+ # with self.env.cr.savepoint():
+ # self.with_company(self.company_id)._create_account_move()
+ #except AccessError as e:
+ # if sudo:
+ self.sudo().with_company(self.company_id)._create_account_move()
+ # else:
+ # raise e
if self.move_id.line_ids:
# Set the uninvoiced orders' state to 'done'
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
@@ -506,6 +508,7 @@ class PosSession(models.Model):
sale_key = (
# account
line['income_account_id'],
+ line['income_analytic_account_id'],
# sign
-1 if line['amount'] < 0 else 1,
# for taxes
@@ -810,9 +813,14 @@ class PosSession(models.Model):
tax['account_id'] = tax_rep.account_id.id
date_order = order_line.order_id.date_order
taxes = [{'date_order': date_order, **tax} for tax in taxes]
+ # _get_product_analytic_accounts() is a method of the OCA module product_analytic
+ # from https://github.com/OCA/account-analytic
+ income_analytic_account = order_line.product_id.product_tmpl_id.with_company(
+ order_line.company_id)._get_product_analytic_accounts()['income']
return {
'date_order': order_line.order_id.date_order,
'income_account_id': get_income_account(order_line).id,
+ 'income_analytic_account_id': income_analytic_account and income_analytic_account.id or False,
'amount': order_line.price_subtotal,
'taxes': taxes,
'base_tags': tuple(tax_data['base_tags']),
@@ -860,7 +868,7 @@ class PosSession(models.Model):
return self._credit_amounts(partial_vals, amount, amount_converted)
def _get_sale_vals(self, key, amount, amount_converted):
- account_id, sign, tax_keys, base_tag_ids = key
+ account_id, analytic_account_id, sign, tax_keys, base_tag_ids = key
tax_ids = set(tax[0] for tax in tax_keys)
applied_taxes = self.env['account.tax'].browse(tax_ids)
title = 'Sales' if sign == 1 else 'Refund'
@@ -870,6 +878,7 @@ class PosSession(models.Model):
partial_vals = {
'name': name,
'account_id': account_id,
+ 'analytic_account_id': analytic_account_id,
'move_id': self.move_id.id,
'tax_ids': [(6, 0, tax_ids)],
'tax_tag_ids': [(6, 0, base_tag_ids)],

View File

@@ -1,22 +0,0 @@
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 86a7b44bcfb..06b46ba5645 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -514,16 +514,7 @@ exports.PosModel = Backbone.Model.extend({
fields: ['name', 'is_cash_count', 'use_payment_terminal'],
domain: function(self){return ['|',['active', '=', false], ['active', '=', true]]; },
loaded: function(self, payment_methods) {
- self.payment_methods = payment_methods.sort(function(a,b){
- // prefer cash payment_method to be first in the list
- if (a.is_cash_count && !b.is_cash_count) {
- return -1;
- } else if (!a.is_cash_count && b.is_cash_count) {
- return 1;
- } else {
- return a.id - b.id;
- }
- });
+ self.payment_methods = payment_methods;
self.payment_methods_by_id = {};
_.each(self.payment_methods, function(payment_method) {
self.payment_methods_by_id[payment_method.id] = payment_method;

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_pos_config_tree" model="ir.ui.view">
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_tree"/>
<field name="arch" type="xml">
<field name="name" position="before">
<field name="sequence" widget="handle"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_pos_pos_form" model="ir.ui.view">
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='lines']/form//field[@name='full_product_name']" position="before">
<field name="product_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="pos_payment_method_view_tree" model="ir.ui.view">
<field name="model">pos.payment.method</field>
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="before">
<field name="sequence" widget="handle"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_warehouse" model="ir.ui.view">
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse"/>
<field name="arch" type="xml">
<field name="out_type_id" position="after">
<field name="pos_type_id"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,50 +1,20 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * product_category_tax
# * product_category_tax
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Project-Id-Version: Odoo Server 8.0-20150727\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-16 20:34+0000\n"
"PO-Revision-Date: 2023-10-16 20:34+0000\n"
"Last-Translator: Alexis de Lattre <alexis.delattre@akretion.com>\n"
"POT-Creation-Date: 2015-12-04 17:16+0000\n"
"PO-Revision-Date: 2015-12-04 17:16+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: product_category_tax
#: model:ir.model,name:product_category_tax.model_product_categ_tax_mixin
msgid "Common code for taxes on product categories"
msgstr "Code commun pour les taxes sur les catégories d'articles"
#. module: product_category_tax
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin__display_name
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__display_name
#: model:ir.model.fields,field_description:product_category_tax.field_product_product__display_name
#: model:ir.model.fields,field_description:product_category_tax.field_product_template__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: product_category_tax
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin__id
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__id
#: model:ir.model.fields,field_description:product_category_tax.field_product_product__id
#: model:ir.model.fields,field_description:product_category_tax.field_product_template__id
msgid "ID"
msgstr "ID"
#. module: product_category_tax
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin____last_update
#: model:ir.model.fields,field_description:product_category_tax.field_product_category____last_update
#: model:ir.model.fields,field_description:product_category_tax.field_product_product____last_update
#: model:ir.model.fields,field_description:product_category_tax.field_product_template____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: product_category_tax
#: model:ir.model,name:product_category_tax.model_product_product
msgid "Product"
@@ -53,7 +23,7 @@ msgstr "Article"
#. module: product_category_tax
#: model:ir.model,name:product_category_tax.model_product_category
msgid "Product Category"
msgstr "Catégorie d'article"
msgstr "Catégorie d'articles"
#. module: product_category_tax
#: model:ir.model,name:product_category_tax.model_product_template
@@ -61,31 +31,23 @@ msgid "Product Template"
msgstr "Modèle d'article"
#. module: product_category_tax
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__purchase_tax_ids
#: field:product.category,purchase_tax_ids:0
msgid "Purchase Taxes"
msgstr "Taxes à l'achat"
msgstr "Taxes fournisseurs"
#. module: product_category_tax
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__sale_tax_ids
#: field:product.category,sale_tax_ids:0
msgid "Sale Taxes"
msgstr "Taxes à la vente"
#. module: product_category_tax
#: code:addons/product_category_tax/product.py:0
#: code:addons/product_category_tax/product.py:57
#, python-format
msgid ""
"The purchase taxes configured on the product '%s' are not the same as the "
"purchase taxes configured on it's related internal category '%s'."
msgstr ""
"Les taxes à l'achat configurées sur l'article '%s' ne sont pas les mêmes que "
"les taxes à l'achat configurées sur sa catégorie interne '%s'."
msgid "The purchase taxes configured on the product '%s' are not the same as the purchase taxes configured on it's related product category '%s'."
msgstr "Les taxes fournisseurs paramétrées sur l'article '%s' ne sont pas les mêmes que les taxes fournisseurs paramétrées sur la catégorie interne '%s'."
#. module: product_category_tax
#: code:addons/product_category_tax/product.py:0
#: code:addons/product_category_tax/product.py:49
#, python-format
msgid ""
"The sale taxes configured on the product '%s' are not the same as the sale "
"taxes configured on it's related internal category '%s'."
msgstr ""
"Les taxes à la vente configurées sur l'article '%s' ne sont pas les mêmes que "
"les taxes à la vente configurées sur sa catégorie interne '%s'."
msgid "The sale taxes configured on the product '%s' are not the same as the sale taxes configured on it's related product category '%s'."
msgstr "Les taxes à la vente paramétrées sur l'article '%s' ne sont pas les mêmes que les taxes à la vente paramétrées sur la catégorie interne '%s'."

View File

@@ -13,9 +13,7 @@ class ProductTemplate(models.Model):
('consu', 'Consumable'),
('service', 'Service'),
], string='Product Type', default='consu', required=True, tracking=True)
type = fields.Selection(
compute='_compute_type', store=True, string="Type", # native string = "Product Type"
default=False, required=False)
type = fields.Selection(compute='_compute_type', store=True, string="Type")
def _detailed_type_mapping(self):
return {}
@@ -25,16 +23,3 @@ class ProductTemplate(models.Model):
type_mapping = self._detailed_type_mapping()
for record in self:
record.type = type_mapping.get(record.detailed_type, record.detailed_type)
# to ensure compat with test and demo data
# It's not perfect, we still have a problem when installing the "stock"
# module while product_detailed_type_stock is not installed yet: it creates
# products with type = 'product' and with the inherit below, it sets detailed_type = 'product'
# but this value is only possible once product_detailed_type_stock is installed. Odoo says:
# ValueError: Wrong value for product.template.detailed_type: 'product'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('type') and vals['type'] != vals.get('detailed_type'):
vals['detailed_type'] = vals['type']
return super().create(vals_list)

View File

@@ -1 +0,0 @@
from . import test_detailed_type

View File

@@ -1,34 +0,0 @@
# Copyright 2024 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import SavepointCase
class TestProductDetailedType(SavepointCase):
def test_product_detailed_type(self):
p1 = self.env['product.product'].create({
'name': 'Test 1',
'detailed_type': 'service',
})
self.assertEqual(p1.type, 'service')
p2 = self.env['product.product'].create({
'name': 'Test 2',
'detailed_type': 'consu',
})
self.assertEqual(p2.type, 'consu')
def test_product_type_compat(self):
p1 = self.env['product.product'].create({
'name': 'Test 1',
'type': 'service',
})
self.assertEqual(p1.type, 'service')
self.assertEqual(p1.detailed_type, 'service')
p2 = self.env['product.product'].create({
'name': 'Test 1',
'type': 'consu',
})
self.assertEqual(p2.type, 'consu')
self.assertEqual(p2.detailed_type, 'consu')

View File

@@ -10,4 +10,4 @@ class ProductTemplate(models.Model):
detailed_type = fields.Selection(selection_add=[
('product', 'Storable Product')
], ondelete={'product': 'set default'}, default='product')
], ondelete={'product': 'set default'})

Some files were not shown because too many files have changed in this diff Show More