Compare commits
35 Commits
14.0-purch
...
14.0-produ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b8709a425 | ||
|
|
1d96cc4c83 | ||
|
|
2ff3ea6dcd | ||
|
|
a1f8ab32dd | ||
|
|
f3a6cfade6 | ||
|
|
b8203ae42a | ||
|
|
02d9dee32e | ||
|
|
04b363f8a6 | ||
|
|
f3b5f85fc9 | ||
|
|
d7f6a2b9f8 | ||
|
|
2a0b1342e7 | ||
|
|
db33820a57 | ||
|
|
4c067fef09 | ||
|
|
88452b7930 | ||
|
|
a6305eb581 | ||
|
|
d2a8b77c22 | ||
|
|
467ce47306 | ||
|
|
ff0bdc1b8d | ||
|
|
254a97edd3 | ||
|
|
162f0b7874 | ||
|
|
ee8bf2ea17 | ||
|
|
97c20fed73 | ||
|
|
856bca4ccf | ||
|
|
0d5346d856 | ||
|
|
940fe43614 | ||
|
|
4e68c48110 | ||
|
|
7b8c35a384 | ||
|
|
0bbda6f265 | ||
|
|
c7c5d9172b | ||
|
|
4655e6b739 | ||
|
|
8bd83b0975 | ||
|
|
9367f7006e | ||
|
|
29b8ebb779 | ||
|
|
c33835957d | ||
|
|
0fa2024dab |
@@ -1,10 +1,10 @@
|
||||
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
|
||||
# Copyright 2017-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": "Bank Reconciliation Report",
|
||||
"version": "14.0.1.0.0",
|
||||
"version": "14.0.2.0.0",
|
||||
"license": "AGPL-3",
|
||||
"author": "Akretion",
|
||||
"website": "https://github.com/akretion/odoo-usability",
|
||||
|
||||
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-01-13 10:31+0000\n"
|
||||
"PO-Revision-Date: 2023-01-13 10:31+0000\n"
|
||||
"POT-Creation-Date: 2024-10-23 10:04+0000\n"
|
||||
"PO-Revision-Date: 2024-10-23 10:04+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -17,6 +17,7 @@ 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"
|
||||
@@ -27,6 +28,12 @@ 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"
|
||||
@@ -49,7 +56,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 rapport de rapprochement bancaire"
|
||||
msgstr "Assistant de rapport de rapprochement bancaire"
|
||||
|
||||
#. module: account_bank_reconciliation_summary_xlsx
|
||||
#: model:ir.actions.report,name:account_bank_reconciliation_summary_xlsx.bank_reconciliation_xlsx
|
||||
@@ -96,17 +103,36 @@ 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 ""
|
||||
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 :"
|
||||
|
||||
#. 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 ""
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. 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
|
||||
@@ -123,13 +149,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 ""
|
||||
msgstr "Exporter en XLSX"
|
||||
|
||||
#. module: account_bank_reconciliation_summary_xlsx
|
||||
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
|
||||
#, python-format
|
||||
msgid "Generated on %s"
|
||||
msgstr "Généré le %s"
|
||||
msgid "Generated from Odoo on %s by %s"
|
||||
msgstr "Généré par Odoo le %s par %s"
|
||||
|
||||
#. module: account_bank_reconciliation_summary_xlsx
|
||||
#: model:ir.model.fields,field_description:account_bank_reconciliation_summary_xlsx.field_bank_reconciliation_report_wizard__id
|
||||
@@ -143,6 +169,18 @@ 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
|
||||
@@ -153,23 +191,17 @@ 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 ""
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. 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 ""
|
||||
msgstr "Dernière mise à jour par"
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. 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"
|
||||
msgstr "Dernière mise à jour le"
|
||||
|
||||
#. module: account_bank_reconciliation_summary_xlsx
|
||||
#: code:addons/account_bank_reconciliation_summary_xlsx/report/bank_reconciliation_xlsx.py:0
|
||||
@@ -179,10 +211,23 @@ 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
|
||||
@@ -211,3 +256,13 @@ 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."
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
|
||||
# Copyright 2017-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).
|
||||
|
||||
@@ -14,32 +14,19 @@ class BankReconciliationXlsx(models.AbstractModel):
|
||||
_description = "Bank Reconciliation XLSX Report"
|
||||
_inherit = "report.report_xlsx.abstract"
|
||||
|
||||
def _domain_add_move_state(self, wizard, domain):
|
||||
if wizard.move_state == 'posted':
|
||||
domain.append(('parent_state', '=', 'posted'))
|
||||
elif wizard.move_state == 'draft_posted':
|
||||
domain.append(('parent_state', 'in', ('draft', 'posted')))
|
||||
|
||||
def _get_account_balance(self, account, wizard):
|
||||
def _prepare_payment_move_lines(self, jdi, account, unreconciled_only=True):
|
||||
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),
|
||||
("company_id", "=", jdi['company'].id),
|
||||
("account_id", "=", account.id),
|
||||
("journal_id", "=", journal.id),
|
||||
("date", "<=", wizard.date),
|
||||
("journal_id", "=", jdi['journal'].id),
|
||||
("date", "<=", jdi['wizard'].date),
|
||||
]
|
||||
if jdi['wizard'].move_state == 'posted':
|
||||
domain.append(('parent_state', '=', 'posted'))
|
||||
elif jdi['wizard'].move_state == 'draft_posted':
|
||||
domain.append(('parent_state', 'in', ('draft', 'posted')))
|
||||
if unreconciled_only:
|
||||
limit_datetime_naive = datetime.combine(wizard.date, datetime.max.time())
|
||||
limit_datetime_naive = datetime.combine(jdi['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)
|
||||
@@ -47,7 +34,6 @@ 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:
|
||||
@@ -60,13 +46,18 @@ 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": mline.balance,
|
||||
"amount": amount,
|
||||
"move_name": move.name,
|
||||
"counterpart": counterpart,
|
||||
}
|
||||
@@ -74,27 +65,31 @@ 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['journal'], account, jdi['wizard'])
|
||||
mlines = self._prepare_payment_move_lines(jdi, account)
|
||||
if mlines or add2total:
|
||||
sheet.write(row, 0, '%s %s' % (account.name, account.code), style['title' + style_suffix])
|
||||
sheet.write(row, 1, "", style['title' + style_suffix])
|
||||
sheet.write(row, 0, '%s %s' % (account.name, account.code), style[f"title{style_suffix}"])
|
||||
sheet.write(row, 1, "", style[f"title{style_suffix}"])
|
||||
|
||||
if not mlines:
|
||||
if add2total:
|
||||
sheet.write(row, 2, _("None"), style['none'])
|
||||
else:
|
||||
return
|
||||
return row
|
||||
else:
|
||||
row += 1
|
||||
col_labels = [
|
||||
_("Date"),
|
||||
_("Partner"),
|
||||
_("Amount"),
|
||||
_("Move Number"),
|
||||
_("Journal Entry"),
|
||||
_("Counter-part"),
|
||||
_("Ref."),
|
||||
_("Label"),
|
||||
@@ -108,7 +103,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['currency'])
|
||||
sheet.write(row, 2, mline["amount"], style[jdi['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'])
|
||||
@@ -118,18 +113,20 @@ class BankReconciliationXlsx(models.AbstractModel):
|
||||
end_line = row
|
||||
|
||||
for col in range(1):
|
||||
sheet.write(row, col, "", style['title' + style_suffix])
|
||||
sheet.write(row, 1, _("Sub-total:") + ' ', style['title_right' + style_suffix])
|
||||
sheet.write(row, col, "", style[f"title{style_suffix}"])
|
||||
sheet.write(row, 1, _("Sub-total:") + ' ', style[f"title_right{style_suffix}"])
|
||||
|
||||
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)
|
||||
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)
|
||||
if add2total:
|
||||
jdi['total'] += subtotal
|
||||
jdi['total_formula'] += '+%s%d' % (jdi['total_col'], row + 1)
|
||||
return row
|
||||
jdi['total_formula'] += f"+{jdi['total_col']}{row + 1}"
|
||||
return row + 2
|
||||
|
||||
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
|
||||
@@ -137,15 +134,21 @@ 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 on %s') % format_datetime(
|
||||
self.env, datetime.utcnow())
|
||||
generated_on_label = _('Generated from Odoo on %s by %s') % (
|
||||
format_datetime(self.env, datetime.utcnow()),
|
||||
self.env.user.name)
|
||||
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': '=',
|
||||
@@ -169,7 +172,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, wizard.company_id.display_name, style['wizard_value'])
|
||||
sheet.write(row, 1, company.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'])
|
||||
@@ -177,51 +180,121 @@ 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'])
|
||||
account_bal = self._get_account_balance(bank_account, wizard)
|
||||
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)
|
||||
|
||||
sheet.write(row, 2, account_bal, style['currency_bg'])
|
||||
sheet.write(row, 2, account_bal, style[f"{jdi['currency']}_bg"])
|
||||
jdi['total'] += account_bal
|
||||
jdi['total_formula'] += '%s%d' % (jdi['total_col'], row + 1)
|
||||
jdi['total_formula'] += f"{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['currency_bg'], jdi['total'])
|
||||
row += 3
|
||||
row, 2, jdi['total_formula'], style[f"{jdi['currency']}_bg"], jdi['total'])
|
||||
total_row = row
|
||||
row += 2
|
||||
|
||||
# 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"
|
||||
title_blue = "#e6e6fa"
|
||||
light_blue = "#e0edff"
|
||||
subtotal_orange = "#ffcc00"
|
||||
title_warn = "#ff9999"
|
||||
subtotal_warn = "#ffff99"
|
||||
light_purple = "#ffdeff"
|
||||
amount_manual = "#ffeeab"
|
||||
title_warn = "#ff9999"
|
||||
lang_code = self.env.user.lang
|
||||
lang = False
|
||||
if lang_code:
|
||||
@@ -249,7 +322,7 @@ class BankReconciliationXlsx(models.AbstractModel):
|
||||
)
|
||||
title_style = {
|
||||
"bold": True,
|
||||
"bg_color": title_blue,
|
||||
"bg_color": light_blue,
|
||||
"font_size": font_size,
|
||||
"align": "left",
|
||||
}
|
||||
@@ -257,7 +330,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_purple,
|
||||
"bg_color": light_blue,
|
||||
"bold": True,
|
||||
"font_size": font_size,
|
||||
"align": "left",
|
||||
@@ -274,23 +347,29 @@ 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})
|
||||
style['regular'] = workbook.add_format({"font_size": font_size, "border": 1})
|
||||
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"}
|
||||
{"num_format": xls_date_format, "font_size": font_size, "align": "left", "border": 1}
|
||||
)
|
||||
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))
|
||||
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))
|
||||
return style
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2017-2023 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2017-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).
|
||||
-->
|
||||
@@ -12,7 +12,6 @@
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2017-2020 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2017-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).
|
||||
-->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2018-2023 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2018-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).
|
||||
-->
|
||||
@@ -9,7 +9,7 @@
|
||||
<record id="account_journal_dashboard_kanban_view" model="ir.ui.view">
|
||||
<field
|
||||
name="name"
|
||||
>bank_reconciliation_summarry.account_journal_dashboard</field>
|
||||
>bank_reconciliation_summary.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">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2017-2023 Akretion France (http://www.akretion.com/)
|
||||
# Copyright 2017-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).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2017-2023 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2017-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).
|
||||
-->
|
||||
|
||||
@@ -227,6 +227,12 @@ 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
|
||||
|
||||
|
||||
|
||||
1
account_payment_line_manual_account/__init__.py
Normal file
1
account_payment_line_manual_account/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
25
account_payment_line_manual_account/__manifest__.py
Normal file
25
account_payment_line_manual_account/__manifest__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# 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,
|
||||
}
|
||||
2
account_payment_line_manual_account/models/__init__.py
Normal file
2
account_payment_line_manual_account/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import account_payment_line
|
||||
from . import account_payment_order
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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>
|
||||
@@ -52,6 +52,39 @@ class AccountMove(models.Model):
|
||||
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):
|
||||
@@ -225,45 +258,14 @@ class AccountMove(models.Model):
|
||||
move.suitable_journal_ids = self.env['account.journal'].search(domain)
|
||||
|
||||
def button_draft(self):
|
||||
# Get report name before reset to draft because name can be different.
|
||||
report_names = self._get_invoice_attachment_name()
|
||||
super().button_draft()
|
||||
# Delete attached pdf invoice
|
||||
if report_names:
|
||||
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', 'in', report_names[move.id]),
|
||||
('res_id', '=', move.id),
|
||||
('res_model', '=', self._name),
|
||||
('type', '=', 'binary'),
|
||||
], limit=1)
|
||||
if attachment:
|
||||
attachment.unlink()
|
||||
|
||||
def _get_invoice_attachment_name(self):
|
||||
report_names = defaultdict(list)
|
||||
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')):
|
||||
report_names[move.id].append(safe_eval(report_invoice.print_report_name, {'object': self, 'time': time}))
|
||||
try:
|
||||
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
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')):
|
||||
report_names[move.id].append(safe_eval(report_invoice.print_report_name, {'object': self, 'time': time}))
|
||||
return report_names
|
||||
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()
|
||||
|
||||
def _get_accounting_date(self, invoice_date, has_tax):
|
||||
# On vendor bills/refunds, we want date = invoice_date unless
|
||||
|
||||
@@ -67,6 +67,14 @@
|
||||
<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>
|
||||
|
||||
@@ -93,6 +101,16 @@
|
||||
</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"/>
|
||||
|
||||
13
base_usability/fix-mimetypes_application_xml.diff
Normal file
13
base_usability/fix-mimetypes_application_xml.diff
Normal file
@@ -0,0 +1,13 @@
|
||||
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'], []),
|
||||
@@ -69,6 +69,10 @@ class ResCompany(models.Model):
|
||||
'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:'),
|
||||
@@ -102,7 +106,7 @@ class ResCompany(models.Model):
|
||||
"""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', 'capital'], ['vat', 'siret', 'eori', 'ape']]
|
||||
line_details = [['phone', 'website', 'rcs_siren', 'capital'], ['vat', 'siret', 'eori', 'ape']]
|
||||
return line_details
|
||||
|
||||
# for reports
|
||||
|
||||
16
base_usability/web-buttons_14_max_instead_of_7.diff
Normal file
16
base_usability/web-buttons_14_max_instead_of_7.diff
Normal file
@@ -0,0 +1,16 @@
|
||||
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.
|
||||
@@ -20,10 +20,10 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['mass_mailing', 'link_tracker_usability'],
|
||||
'data': [
|
||||
# 'views/link_tracker.xml',
|
||||
'views/link_tracker.xml',
|
||||
],
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
1
mass_mailing_usability/models/__init__.py
Normal file
1
mass_mailing_usability/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import mailing_mailing
|
||||
14
mass_mailing_usability/models/mailing_mailing.py
Normal file
14
mass_mailing_usability/models/mailing_mailing.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
@@ -8,39 +8,13 @@
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_link_tracker_click_tree" model="ir.ui.view">
|
||||
<record id="link_tracker_click_view_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="link_tracker.view_link_tracker_click_tree"/>
|
||||
<field name="inherit_id" ref="mass_mailing.link_tracker_click_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<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 name="mass_mailing_id" position="after">
|
||||
<field name="mailing_trace_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -22,7 +22,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['mrp'],
|
||||
'depends': ['mrp_account'],
|
||||
'data': [
|
||||
'security/mrp_average_cost_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
@@ -165,7 +165,9 @@ class MrpProduction(models.Model):
|
||||
|
||||
company_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id', string='Company Currency')
|
||||
# extra_cost is per unit in the UoM of the mrp.production (product_uom_id)
|
||||
# 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 "
|
||||
@@ -178,7 +180,7 @@ class MrpProduction(models.Model):
|
||||
def _compute_extra_cost(self):
|
||||
for prod in self:
|
||||
bom = prod.bom_id
|
||||
if bom and bom.type == 'normal':
|
||||
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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
'summary': 'Usability improvements on mrp_subcontracting',
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['mrp_subcontracting'],
|
||||
'depends': ['mrp_subcontracting', 'stock_usability'],
|
||||
'data': [
|
||||
'views/mrp_bom.xml',
|
||||
'views/stock_move.xml',
|
||||
'views/stock_picking.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
1
mrp_subcontracting_usability/models/__init__.py
Normal file
1
mrp_subcontracting_usability/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import stock_move
|
||||
21
mrp_subcontracting_usability/models/stock_move.py
Normal file
21
mrp_subcontracting_usability/models/stock_move.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 Akretion France (http://www.akretion.com/)
|
||||
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).
|
||||
-->
|
||||
|
||||
23
mrp_subcontracting_usability/views/stock_move.xml
Normal file
23
mrp_subcontracting_usability/views/stock_move.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
23
mrp_subcontracting_usability/views/stock_picking.xml
Normal file
23
mrp_subcontracting_usability/views/stock_picking.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
@@ -19,6 +19,10 @@ class MrpProduction(models.Model):
|
||||
}, 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
|
||||
|
||||
1
partner_products_shortcut/__init__.py
Normal file
1
partner_products_shortcut/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import res_partner
|
||||
27
partner_products_shortcut/__manifest__.py
Normal file
27
partner_products_shortcut/__manifest__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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,
|
||||
}
|
||||
38
partner_products_shortcut/res_partner.py
Normal file
38
partner_products_shortcut/res_partner.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
27
partner_products_shortcut/res_partner_view.xml
Normal file
27
partner_products_shortcut/res_partner_view.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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>
|
||||
BIN
partner_products_shortcut/static/description/icon.png
Normal file
BIN
partner_products_shortcut/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
15
pos_usability/pos-always_open_cashbox.diff
Normal file
15
pos_usability/pos-always_open_cashbox.diff
Normal file
@@ -0,0 +1,15 @@
|
||||
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();
|
||||
}
|
||||
|
||||
69
pos_usability/pos-product_analytic.diff
Normal file
69
pos_usability/pos-product_analytic.diff
Normal file
@@ -0,0 +1,69 @@
|
||||
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)],
|
||||
@@ -13,7 +13,9 @@ 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")
|
||||
type = fields.Selection(
|
||||
compute='_compute_type', store=True, string="Type", # native string = "Product Type"
|
||||
default=False, required=False)
|
||||
|
||||
def _detailed_type_mapping(self):
|
||||
return {}
|
||||
@@ -23,3 +25,16 @@ 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)
|
||||
|
||||
1
product_detailed_type/tests/__init__.py
Normal file
1
product_detailed_type/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_detailed_type
|
||||
34
product_detailed_type/tests/test_detailed_type.py
Normal file
34
product_detailed_type/tests/test_detailed_type.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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')
|
||||
1
product_editable_sequence/__init__.py
Normal file
1
product_editable_sequence/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
26
product_editable_sequence/__manifest__.py
Normal file
26
product_editable_sequence/__manifest__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2024 Akretion (https://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Product Editable Sequence",
|
||||
"summary": "Make s=the sequence mass editable with a default value of 100",
|
||||
"version": "14.0.1.0.0",
|
||||
"development_status": "Alpha",
|
||||
"category": "Uncategorized",
|
||||
"website": "www.akretion.com",
|
||||
"author": " Akretion",
|
||||
"license": "AGPL-3",
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
"bin": [],
|
||||
},
|
||||
"depends": [
|
||||
"product",
|
||||
],
|
||||
"data": [
|
||||
"views/product_template_views.xml",
|
||||
],
|
||||
"demo": [
|
||||
],
|
||||
}
|
||||
11
product_editable_sequence/models/product_template.py
Normal file
11
product_editable_sequence/models/product_template.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Copyright 2024 Akretion (https://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
sequence = fields.Integer(default=100)
|
||||
16
product_editable_sequence/views/product_template_views.xml
Normal file
16
product_editable_sequence/views/product_template_views.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="product_template_view_tree" model="ir.ui.view">
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_tree_view" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="sequence" position="attributes">
|
||||
<attribute name="widget"/>
|
||||
<attribute name="readonly"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -14,7 +14,9 @@ class ProductTemplate(models.Model):
|
||||
# in v10, that field was defined in procurement_suggest, but we will
|
||||
# probably not port procurement_suggest because it is native in v14
|
||||
seller_id = fields.Many2one(
|
||||
'res.partner', related='seller_ids.name', store=True,
|
||||
'res.partner',
|
||||
compute="_compute_seller_id",
|
||||
search="_search_seller_id",
|
||||
string='Main Supplier')
|
||||
|
||||
# in v14, I noticed that the tracking of the fields of product.template
|
||||
@@ -33,6 +35,19 @@ class ProductTemplate(models.Model):
|
||||
company_id = fields.Many2one(tracking=110)
|
||||
barcode_type = fields.Char(compute='_compute_template_barcode_type')
|
||||
|
||||
|
||||
def _search_seller_id(self, operator, value):
|
||||
# searching on the first line of a o2m is not that easy
|
||||
# So we search all potential matching products
|
||||
# Then we filter on the seller_id
|
||||
records = self.search([("seller_ids.partner_id", operator, value)])
|
||||
records = records.filtered_domain([("seller_id", operator, value)])
|
||||
return [("id", "in", records.ids)]
|
||||
|
||||
def _compute_seller_id(self):
|
||||
for record in self:
|
||||
record.seller_id = fields.first(record.seller_ids).name
|
||||
|
||||
@api.depends('product_variant_ids.barcode')
|
||||
def _compute_template_barcode_type(self):
|
||||
ppo = self.env['product.product']
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from . import sale_order
|
||||
from . import account_move
|
||||
|
||||
33
sale_stock_usability/models/account_move.py
Normal file
33
sale_stock_usability/models/account_move.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _report_get_sale_pickings(self, sale_order=None):
|
||||
self.ensure_one()
|
||||
# the sale_order arg is usefull when using
|
||||
# py3o_lines_layout_groupby_order() to display the delivery orders
|
||||
# linked to a specific sale_order
|
||||
assert self.move_type in ('out_invoice', 'out_refund')
|
||||
sale_orders = sale_order or self.sale_ids
|
||||
picking_domain = [
|
||||
('id', 'in', sale_orders.picking_ids.ids),
|
||||
('state', '=', 'done'),
|
||||
('date_done', '<', self.create_date),
|
||||
('company_id', '=', self.company_id.id),
|
||||
]
|
||||
previous_inv = self.env['account.move'].search([
|
||||
('move_type', 'in', ('out_invoice', 'out_refund')),
|
||||
('create_date', '<', self.create_date),
|
||||
('id', 'in', sale_orders.invoice_ids.ids),
|
||||
('company_id', '=', self.company_id.id),
|
||||
], limit=1, order='id desc')
|
||||
if previous_inv:
|
||||
picking_domain.append(('date_done', '>', previous_inv.create_date))
|
||||
pickings = self.env['stock.picking'].search(picking_domain)
|
||||
return pickings
|
||||
@@ -27,6 +27,7 @@
|
||||
<field name="client_order_ref"/>
|
||||
</field>
|
||||
<button name="action_quotation_send" states="sent,sale" position="after">
|
||||
<button name="action_quotation_send" type="object" string="Send Order Acknowledgement" attrs="{'invisible': ['|', ('state', 'not in', ('sale', 'done')), ('invoice_count','>=',1)]}"/>
|
||||
<button name="%(sale.action_report_saleorder)d" type="action" string="Print" states="draft,sent,sale,done"/>
|
||||
</button>
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="after">
|
||||
|
||||
@@ -5,7 +5,9 @@ from . import stock_location
|
||||
from . import stock_location_route
|
||||
from . import stock_warehouse_orderpoint
|
||||
from . import stock_quant
|
||||
from . import stock_quant_package
|
||||
from . import stock_inventory
|
||||
from . import stock_production_lot
|
||||
from . import procurement_group
|
||||
from . import procurement_scheduler_log
|
||||
from . import product
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# @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 import models
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
|
||||
13
stock_usability/models/stock_production_lot.py
Normal file
13
stock_usability/models/stock_production_lot.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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 StockProductionLot(models.Model):
|
||||
_inherit = 'stock.production.lot'
|
||||
|
||||
name = fields.Char(tracking=True)
|
||||
product_id = fields.Many2one(tracking=True)
|
||||
ref = fields.Char(tracking=True)
|
||||
@@ -2,7 +2,11 @@
|
||||
# @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 import api, fields, models, _
|
||||
from odoo.tools import float_compare
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
@@ -16,7 +20,7 @@ class StockQuant(models.Model):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
"stock.stock_move_line_action")
|
||||
action['domain'] = [
|
||||
('state', 'not in', ('draft', 'done')),
|
||||
('state', 'not in', ('draft', 'done', 'cancel')),
|
||||
('product_id', '=', self.product_id.id),
|
||||
('location_id', '=', self.location_id.id),
|
||||
('lot_id', '=', self.lot_id.id or False),
|
||||
@@ -26,3 +30,48 @@ class StockQuant(models.Model):
|
||||
]
|
||||
action['context'] = {'create': 0, 'stock_move_line_main_view': True}
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _fix_reserved_quantity(self):
|
||||
# called by a script when we start to have wrong 'reserved_quantity' on quants
|
||||
logger.info('START _fix_reserved_quantity')
|
||||
self._cr.execute("DELETE FROM stock_move_line WHERE move_id is null")
|
||||
self._cr.execute('SELECT id FROM stock_quant WHERE reserved_quantity > quantity AND reserved_quantity > 0')
|
||||
qty_quant_digits = 4
|
||||
all_rows = self._cr.fetchall()
|
||||
logger.info('%d bad quants detected', len(all_rows))
|
||||
for row in all_rows:
|
||||
quant_id = row[0]
|
||||
quant = self.env['stock.quant'].browse(quant_id)
|
||||
logger.info('Processing quant ID %s product %s lot %s location %s package %s quantity %s reserved_quantity %s', quant.id, quant.product_id.display_name, quant.lot_id.display_name or '-', quant.location_id.display_name, quant.package_id.display_name, quant.quantity, quant.reserved_quantity)
|
||||
reserved_quantity_cmp = float_compare(quant.reserved_quantity, 0, precision_digits=qty_quant_digits)
|
||||
# should never happen
|
||||
if reserved_quantity_cmp < 0:
|
||||
raise UserError(_("On quant ID %d, reserved_quantity is negative (%s).") % (quant_id, quant.reserved_quantity))
|
||||
if not reserved_quantity_cmp:
|
||||
logger.info('Quant ID %d as reserved_quantity = 0 after proper rounding', quant.id)
|
||||
continue
|
||||
if quant.package_id:
|
||||
logger.warning("Skipping quant ID %d because this script doesn't support packages for the moment", quant.id)
|
||||
continue
|
||||
if quant.location_id.should_bypass_reservation():
|
||||
logger.warning('Skipping quant ID %d because the location is marked as should bypass resa', quant.id)
|
||||
continue
|
||||
ml_domain = [
|
||||
('product_id', '=', quant.product_id.id),
|
||||
('location_id', '=', quant.location_id.id),
|
||||
('state', 'not in', ('done', 'draft', 'cancel')),
|
||||
('lot_id', '=', quant.lot_id.id or False),
|
||||
('company_id', '=', quant.company_id.id),
|
||||
]
|
||||
move_lines = self.env['stock.move.line'].sudo().search(ml_domain)
|
||||
if not move_lines:
|
||||
quant.sudo().write({'reserved_quantity': 0})
|
||||
for move_line in move_lines:
|
||||
move_line.button_do_unreserve()
|
||||
move_lines = self.env['stock.move.line'].sudo().search(ml_domain)
|
||||
if not move_lines:
|
||||
quant.sudo().write({'reserved_quantity': 0})
|
||||
else:
|
||||
raise UserError(_("Product moves are still present after full unreserve on quant ID %d. This should never happen.") % quant.id)
|
||||
logger.info('END _fix_reserved_quantity')
|
||||
|
||||
31
stock_usability/models/stock_quant_package.py
Normal file
31
stock_usability/models/stock_quant_package.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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, models
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockQuantPackage(models.Model):
|
||||
_inherit = "stock.quant.package"
|
||||
|
||||
@api.model
|
||||
def _cron_auto_unpack_on_internal_locations(self):
|
||||
# Problem in v10: when you manage packs in Odoo for customer pickings,
|
||||
# you have the following problem: when you return a customer picking,
|
||||
# if you return all the products that were in the same pack, the pack
|
||||
# is returned, so you have in your stock one or several quants
|
||||
# inside a pack. This is a problem when you want to ship those
|
||||
# products again.
|
||||
# I provide the code in this module, but not the cron, because in some
|
||||
# scenarios, you may want to have packs in your stock.
|
||||
# Just add the cron in the specific module of your project.
|
||||
# Underlying problem solved in Odoo v11. Don't port that to v14 !
|
||||
logger.info('START cron auto unpack on internal locations')
|
||||
int_locs = self.env['stock.location'].search([('usage', '=', 'internal')])
|
||||
packages = self.search([('location_id', 'in', int_locs.ids)])
|
||||
logger.info('Unpacking %d packages on internal locations', len(packages))
|
||||
packages.unpack()
|
||||
logger.info('END cron auto unpack on internal locations')
|
||||
@@ -2,7 +2,7 @@
|
||||
# @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
|
||||
from odoo import api, fields, models
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -11,6 +11,8 @@ logger = logging.getLogger(__name__)
|
||||
class StockWarehouseOrderpoint(models.Model):
|
||||
_inherit = 'stock.warehouse.orderpoint'
|
||||
|
||||
product_barcode = fields.Char(related='product_id.barcode', string="Product Barcode")
|
||||
|
||||
@api.model
|
||||
def _procure_orderpoint_confirm(
|
||||
self, use_new_cursor=False, company_id=False, raise_user_error=True):
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<field name="model">stock.warehouse.orderpoint</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="product_barcode" optional="hide"/>
|
||||
</field>
|
||||
<field name="trigger" position="attributes">
|
||||
<attribute name="optional">show</attribute>
|
||||
</field>
|
||||
|
||||
Reference in New Issue
Block a user