Compare commits
3 Commits
10.0
...
10.0-ADD-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b57485cf7c | ||
|
|
20dcf1a333 | ||
|
|
7c7cb2e8d2 |
@@ -3,5 +3,4 @@
|
||||
from . import account
|
||||
from . import account_invoice_report
|
||||
from . import partner
|
||||
from . import product
|
||||
from . import wizard
|
||||
|
||||
@@ -57,11 +57,11 @@ class AccountInvoice(models.Model):
|
||||
def _compute_has_attachment(self):
|
||||
iao = self.env['ir.attachment']
|
||||
for inv in self:
|
||||
if iao.search_count([
|
||||
if iao.search([
|
||||
('res_model', '=', 'account.invoice'),
|
||||
('res_id', '=', inv.id),
|
||||
('type', '=', 'binary'),
|
||||
('company_id', '=', inv.company_id.id)]):
|
||||
('company_id', '=', inv.company_id.id)], limit=1):
|
||||
inv.has_attachment = True
|
||||
else:
|
||||
inv.has_attachment = False
|
||||
@@ -98,18 +98,20 @@ class AccountInvoice(models.Model):
|
||||
return res
|
||||
|
||||
# I really hate to see a "/" in the 'name' field of the account.move.line
|
||||
# generated from customer invoices linked to the partners' account because
|
||||
# the label of an account move line is an important field, we can't
|
||||
# write a rubbish '/' in it !
|
||||
# On a related topic, you should also consider to use this PR:
|
||||
# https://github.com/OCA/account-invoicing/pull/882
|
||||
# generated from customer invoices linked to the partners' account because:
|
||||
# 1) the label of an account move line is an important field, we can't
|
||||
# write a rubbish '/' in it !
|
||||
# 2) the 'name' field of the account.move.line is used in the overdue
|
||||
# letter, and '/' is not meaningful for our customer !
|
||||
@api.multi
|
||||
def action_move_create(self):
|
||||
res = super(AccountInvoice, self).action_move_create()
|
||||
for inv in self:
|
||||
self._cr.execute(
|
||||
"UPDATE account_move_line SET name=%s "
|
||||
"WHERE move_id=%s AND name='/'", (inv.number, inv.move_id.id))
|
||||
"UPDATE account_move_line SET name= "
|
||||
"CASE WHEN name='/' THEN %s "
|
||||
"ELSE %s||' - '||name END "
|
||||
"WHERE move_id=%s", (inv.number, inv.number, inv.move_id.id))
|
||||
self.invalidate_cache()
|
||||
return res
|
||||
|
||||
@@ -144,13 +146,6 @@ class AccountInvoice(models.Model):
|
||||
attach.id, attach.name)
|
||||
logger.info('END fix customer invoice attachment filename')
|
||||
|
||||
@api.multi
|
||||
def invoice_print(self):
|
||||
# Inherit a native method without calling super()
|
||||
# Don't mark invoice as 'sent' when you just click on 'Print Invoice'
|
||||
self.ensure_one()
|
||||
return self.env['report'].get_action(self, 'account.report_invoice')
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_inherit = 'account.invoice.line'
|
||||
@@ -603,15 +598,6 @@ class AccountBankStatementLine(models.Model):
|
||||
vals['ref'] = False
|
||||
return vals
|
||||
|
||||
def get_statement_line_for_reconciliation_widget(self):
|
||||
# In the work interface of the bank statement, when a partner_id
|
||||
# is selected, Odoo displays its 'name' => we prefer that it
|
||||
# displays its 'display_name'.
|
||||
data = super(AccountBankStatementLine, self).get_statement_line_for_reconciliation_widget()
|
||||
if self.partner_id:
|
||||
data['partner_name'] = self.partner_id.display_name
|
||||
return data
|
||||
|
||||
@api.multi
|
||||
def show_account_move(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
|
||||
index dc3247154be..077e004b53c 100644
|
||||
index 8ed1e48..615da43 100644
|
||||
--- a/addons/account/models/account_bank_statement.py
|
||||
+++ b/addons/account/models/account_bank_statement.py
|
||||
@@ -566,7 +566,13 @@ class AccountBankStatementLine(models.Model):
|
||||
@@ -563,7 +563,13 @@ class AccountBankStatementLine(models.Model):
|
||||
"""
|
||||
# Blue lines = payment on bank account not assigned to a statement yet
|
||||
reconciliation_aml_accounts = [self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id]
|
||||
@@ -18,10 +18,10 @@ index dc3247154be..077e004b53c 100644
|
||||
# Black lines = unreconciled & (not linked to a payment or open balance created by statement
|
||||
domain_matching = [('reconciled', '=', False)]
|
||||
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
|
||||
index 6a0fed7d143..ecc2ed67936 100644
|
||||
index b60ffbe..6c27c57 100644
|
||||
--- a/addons/account/models/account_move.py
|
||||
+++ b/addons/account/models/account_move.py
|
||||
@@ -633,6 +633,7 @@ class AccountMoveLine(models.Model):
|
||||
@@ -599,6 +599,7 @@ class AccountMoveLine(models.Model):
|
||||
domain = expression.AND([domain, [('id', 'not in', excluded_ids)]])
|
||||
if str:
|
||||
str_domain = [
|
||||
@@ -30,7 +30,7 @@ index 6a0fed7d143..ecc2ed67936 100644
|
||||
'|', ('move_id.ref', 'ilike', str),
|
||||
'|', ('date_maturity', 'like', str),
|
||||
diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js
|
||||
index 5d00984157c..836fe37fc2f 100644
|
||||
index 453bd41..48c396e 100644
|
||||
--- a/addons/account/static/src/js/account_reconciliation_widgets.js
|
||||
+++ b/addons/account/static/src/js/account_reconciliation_widgets.js
|
||||
@@ -76,7 +76,7 @@ var abstractReconciliation = Widget.extend(ControlPanelMixin, {
|
||||
@@ -42,7 +42,7 @@ index 5d00984157c..836fe37fc2f 100644
|
||||
// Number of reconciliations loaded initially and by clicking 'show more'
|
||||
this.num_reconciliations_fetched_in_batch = 10;
|
||||
this.animation_speed = 100; // "Blocking" animations
|
||||
@@ -1757,7 +1757,7 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({
|
||||
@@ -1755,7 +1755,7 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({
|
||||
relation: "res.partner",
|
||||
string: _t("Partner"),
|
||||
type: "many2one",
|
||||
|
||||
@@ -205,9 +205,6 @@ module -->
|
||||
<field name="categ_id" position="after">
|
||||
<field name="product_id"/>
|
||||
</field>
|
||||
<filter name="thisyear" position="after">
|
||||
<filter name="this_year_and_previous" string="This year and previous" domain="['|', ('date', '=', False), '&',('date','<=', (context_today() + relativedelta(day=31, month=12)).strftime('%Y-%m-%d')), ('date', '>=', (context_today() + relativedelta(day=1, month=1, years=-1)).strftime('%Y-%m-%d'))]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -1,561 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_usability
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-10-23 16:16+0000\n"
|
||||
"PO-Revision-Date: 2020-10-23 16:16+0000\n"
|
||||
"Last-Translator: <>\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: account_usability
|
||||
#: model:ir.model.fields,help:account_usability.field_account_invoice_line_state
|
||||
msgid " * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
|
||||
" * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n"
|
||||
" * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n"
|
||||
" * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
|
||||
" * The 'Cancelled' status is used when user cancel invoice."
|
||||
msgstr "* L'état \"Brouillon\" est utilisé lorsqu'un utilisateur est en train de saisir ou de modifier une nouvelle facture non confirmée.\n"
|
||||
"* L'état \"Pro-forma\" est utilisé lorsque la facture n'a pas de numéro de facture.\n"
|
||||
"* L'état 'Ouvert' est utilisé lorsque l'utilisateur crée une facture, celle-ci a alors un numéro de facture. La facture reste dans l'état \"Ouvert\" tant qu'elle n'est pas payée.\n"
|
||||
"* L'état 'Payé' est affecté automatiquement lorsque la facture est payée. Les écritures correspondantes dans les journaux peuvent ou non être lettrées.\n"
|
||||
"* L'état \"Annulé\" est utilisé lorsque l'utilisateur annule la facture."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_move_line_form
|
||||
msgid "-> View partially reconciled entries"
|
||||
msgstr "-> Voir les écritures partiellement lettrées"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_move_line_form
|
||||
msgid "<span colspan=\"2\" attrs=\"{'invisible': ['|', '|', ('full_reconcile_id', '!=', False), ('matched_debit_ids', '!=', []), ('matched_credit_ids', '!=', [])]}\" class=\"o_form_field\">No Partial Reconcile</span>"
|
||||
msgstr "<span colspan=\"2\" attrs=\"{'invisible': ['|', '|', ('full_reconcile_id', '!=', False), ('matched_debit_ids', '!=', []), ('matched_credit_ids', '!=', [])]}\" class=\"o_form_field\">No Partial Reconcile</span>"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_account
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Account"
|
||||
msgstr "Compte"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_move
|
||||
msgid "Account Entry"
|
||||
msgstr "Pièce comptable"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_move_backtodraft
|
||||
msgid "Account Move Unpost"
|
||||
msgstr "Account Move Unpost"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_move_reversal
|
||||
msgid "Account move reversal"
|
||||
msgstr "Extourne de la pièce comptable"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_move_backtodraft_form
|
||||
msgid "All selected journal entries will be unposted (if allowed by the journal configuration)."
|
||||
msgstr "All selected journal entries will be unposted (if allowed by the journal configuration)."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_line_account_reconcile
|
||||
msgid "Allow Reconciliation"
|
||||
msgstr "Autoriser le lettrage"
|
||||
|
||||
#. module: account_usability
|
||||
#: sql_constraint:account.analytic.account:0
|
||||
msgid "An analytic account with the same code already exists in the same company!"
|
||||
msgstr "Un compte analytique avec le même code existe déjà pour la même société !"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_analytic_account
|
||||
msgid "Analytic Account"
|
||||
msgstr "Compte analytique"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.menu,name:account_usability.bank_account_account_config_menu
|
||||
#: model:ir.ui.menu,name:account_usability.res_partner_bank_account_config_menu
|
||||
msgid "Bank Accounts"
|
||||
msgstr "Comptes bancaires"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_bank_statement
|
||||
msgid "Bank Statement"
|
||||
msgstr "Relevé bancaire"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_bank_statement_line
|
||||
msgid "Bank Statement Line"
|
||||
msgstr "Ligne de relevé bancaire"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.menu,name:account_usability.res_bank_account_config_menu
|
||||
msgid "Banks"
|
||||
msgstr "Banques"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_mark_sent_form
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_move_backtodraft_form
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,help:account_usability.field_account_move_line_account_reconcile
|
||||
msgid "Check this box if this account allows invoices & payments matching of journal items."
|
||||
msgstr "Cochez cette case si ce compte permet de faire du rapprochement entre factures et paiements."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_search
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_line_commercial_partner_id
|
||||
msgid "Commercial Entity"
|
||||
msgstr "Entité commerciale"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_create_uid
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_create_date
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_create_date
|
||||
msgid "Created on"
|
||||
msgstr "Créé le"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Current Year"
|
||||
msgstr "Année en cours"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.out_invoice_line_action
|
||||
#: model:ir.actions.act_window,name:account_usability.out_invoice_refund_line_action
|
||||
msgid "Customer Invoice Lines"
|
||||
msgstr "Lignes de facture client"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Customer Invoices"
|
||||
msgstr "Factures clients"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.out_refund_line_action
|
||||
msgid "Customer Refund Lines"
|
||||
msgstr "Lignes d'avoir client"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Customer Refunds"
|
||||
msgstr "Avoirs client"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Debit or Credit"
|
||||
msgstr "Débit ou crédit"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_default_account_id
|
||||
msgid "Default Debit Account"
|
||||
msgstr "Compte de débit par défaut"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_default_move_line_name
|
||||
msgid "Default Label"
|
||||
msgstr "Libellé par défaut"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_default_credit
|
||||
msgid "Default credit"
|
||||
msgstr "Default credit"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_default_debit
|
||||
msgid "Default debit"
|
||||
msgstr "Default debit"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_display_name
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Draft"
|
||||
msgstr "Brouillon"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_bank_statement_end_date
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_bank_statement_search
|
||||
msgid "End Date"
|
||||
msgstr "Date de fin"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_fiscal_position
|
||||
msgid "Fiscal Position"
|
||||
msgstr "Position fiscale"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Fully Reconciled"
|
||||
msgstr "Lettré totalement"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_journal_search
|
||||
msgid "Group By"
|
||||
msgstr "Regrouper par"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_has_attachment
|
||||
msgid "Has attachment"
|
||||
msgstr "Pièce(s) jointe(s) présente(s)"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_has_discount
|
||||
msgid "Has discount"
|
||||
msgstr "A une remise"
|
||||
|
||||
#. 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
|
||||
msgid "Hide Bank Statement Balance"
|
||||
msgstr "Masquer le solde du relevé"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_id
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_product_supplierinfo
|
||||
msgid "Information about a product vendor"
|
||||
msgstr "Information sur le vendeur de l'article"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_invoice
|
||||
msgid "Invoice"
|
||||
msgstr "Facture"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_line_date_invoice
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Invoice Date"
|
||||
msgstr "Date de facturation"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_invoice_line
|
||||
msgid "Invoice Line"
|
||||
msgstr "Ligne de facture"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_line_invoice_number
|
||||
msgid "Invoice Number"
|
||||
msgstr "Numéro de facture"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_line_state
|
||||
msgid "Invoice State"
|
||||
msgstr "État de la facture"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_report_tree
|
||||
msgid "Invoices Analysis"
|
||||
msgstr "Analyse des factures"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_invoice_report
|
||||
msgid "Invoices Statistics"
|
||||
msgstr "Statistiques des factures"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,help:account_usability.field_account_move_default_account_id
|
||||
msgid "It acts as a default account for debit amount"
|
||||
msgstr "Ça sert de compte par défaut pour les montants en débit"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_journal
|
||||
msgid "Journal"
|
||||
msgstr "Journal"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_move_line
|
||||
msgid "Journal Item"
|
||||
msgstr "Écriture comptable"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,help:account_usability.field_account_invoice_line_date_invoice
|
||||
msgid "Keep empty to use the current date"
|
||||
msgstr "Laissez vide pour utiliser la date courante"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent___last_update
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft___last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_write_uid
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Mis à jour par"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_mark_sent_write_date
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_backtodraft_write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Mis à jour le"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.account_invoice_mark_sent_action
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_mark_sent_form
|
||||
msgid "Mark as Sent"
|
||||
msgstr "Marquer comme envoyé"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_invoice_mark_sent
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_mark_sent_form
|
||||
msgid "Mark invoices as sent"
|
||||
msgstr "Marquer les factures comme envoyées"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_invoice_filter
|
||||
msgid "Missing Attachment"
|
||||
msgstr "Missing Attachment"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Name or Reference"
|
||||
msgstr "Name or Reference"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/account.py:615
|
||||
#, python-format
|
||||
msgid "No journal entry linked to this bank statement line."
|
||||
msgstr "No journal entry linked to this bank statement line."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Not Paid"
|
||||
msgstr "Non payées"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_report_number
|
||||
msgid "Number"
|
||||
msgstr "Numéro"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/account.py:218
|
||||
#, python-format
|
||||
msgid "On journal '%s', the default credit account '%s' should be configured with Type = 'Bank and Cash'."
|
||||
msgstr "On journal '%s', the default credit account '%s' should be configured with Type = 'Bank and Cash'."
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/account.py:209
|
||||
#, python-format
|
||||
msgid "On journal '%s', the default debit account '%s' should be configured with Type = 'Bank and Cash'."
|
||||
msgstr "On journal '%s', the default debit account '%s' should be configured with Type = 'Bank and Cash'."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Paid"
|
||||
msgstr "Payé"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_partial_reconcile
|
||||
msgid "Partial Reconcile"
|
||||
msgstr "Lettrage partiel"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_res_partner
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Partner"
|
||||
msgstr "Partenaire"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_reconcile_model
|
||||
msgid "Preset to create journal entries during a invoices and payments matching"
|
||||
msgstr "Préconfigurer pour créer une écriture pendant la correspondance entre des factures et des paiements"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Previous Year"
|
||||
msgstr "Année précédente"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Product"
|
||||
msgstr "Article"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Modèle d'article"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_product_product_purchase_price_type
|
||||
#: model:ir.model.fields,field_description:account_usability.field_product_supplierinfo_purchase_price_type
|
||||
#: model:ir.model.fields,field_description:account_usability.field_product_template_purchase_price_type
|
||||
msgid "Purchase Price Type"
|
||||
msgstr "Type de prix d'achat"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_move_line_reconcile_string
|
||||
msgid "Reconcile"
|
||||
msgstr "Reconcile"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_product_product_sale_price_type
|
||||
#: model:ir.model.fields,field_description:account_usability.field_product_template_sale_price_type
|
||||
msgid "Sale Price Type"
|
||||
msgstr "Type de prix de vente"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Search Invoice Lines"
|
||||
msgstr "Search Invoice Lines"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_invoice_filter
|
||||
msgid "Sent"
|
||||
msgstr "Envoyé"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_move_line_tree
|
||||
msgid "Show Journal Entry"
|
||||
msgstr "Show Journal Entry"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/account.py:291
|
||||
#, python-format
|
||||
msgid "Some account groups already exists"
|
||||
msgstr "Some account groups already exists"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_bank_statement_start_date
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_bank_statement_search
|
||||
msgid "Start Date"
|
||||
msgstr "Date de début"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.in_invoice_line_action
|
||||
#: model:ir.actions.act_window,name:account_usability.in_invoice_refund_line_action
|
||||
msgid "Supplier Invoice Lines"
|
||||
msgstr "Supplier Invoice Lines"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Supplier Invoices"
|
||||
msgstr "Supplier Invoices"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.in_refund_line_action
|
||||
msgid "Supplier Refund Lines"
|
||||
msgstr "Supplier Refund Lines"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_line_search
|
||||
msgid "Supplier Refunds"
|
||||
msgstr "Supplier Refunds"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model,name:account_usability.model_account_tax
|
||||
#: model:ir.ui.view,arch_db:account_usability.product_supplierinfo_tree_view
|
||||
msgid "Tax"
|
||||
msgstr "Taxe"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_tax_group_form
|
||||
msgid "Tax Group"
|
||||
msgstr "Tax Group"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.account_tax_group_action
|
||||
#: model:ir.ui.menu,name:account_usability.account_tax_group_menu
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_tax_group_tree
|
||||
msgid "Tax Groups"
|
||||
msgstr "Tax Groups"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/product.py:22
|
||||
#, python-format
|
||||
msgid "Tax excl."
|
||||
msgstr "HT"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/product.py:22
|
||||
#, python-format
|
||||
msgid "Tax incl."
|
||||
msgstr "TTC"
|
||||
|
||||
#. module: account_usability
|
||||
#: code:addons/account_usability/wizard/account_move_backtodraft.py:20
|
||||
#, python-format
|
||||
msgid "There is no journal items in posted state to unpost."
|
||||
msgstr "There is no journal items in posted state to unpost."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_invoice_mark_sent_form
|
||||
msgid "This wizard will mark as <i>sent</i> all the selected invoices in open or paid state."
|
||||
msgstr "This wizard will mark as <i>sent</i> all the selected invoices in open or paid state."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_invoice_report_search
|
||||
msgid "This year and previous"
|
||||
msgstr "Cette année et la précédente"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_invoice_filter
|
||||
msgid "To Send"
|
||||
msgstr "A envoyer"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_move_line_tree
|
||||
msgid "Total Balance"
|
||||
msgstr "Total Balance"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,field_description:account_usability.field_account_invoice_line_invoice_type
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_journal_search
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.actions.act_window,name:account_usability.account_move_backtodraft_action
|
||||
#: model:ir.ui.view,arch_db:account_usability.account_move_backtodraft_form
|
||||
msgid "Unpost Journal Entries"
|
||||
msgstr "Unpost Journal Entries"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_account_move_line_filter
|
||||
msgid "Unreconciled or Partially Reconciled"
|
||||
msgstr "Non lettré ou partiellement lettré"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.view_bank_statement_form
|
||||
msgid "View Account Move"
|
||||
msgstr "View Account Move"
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.model.fields,help:account_usability.field_account_bank_statement_hide_bank_statement_balance
|
||||
#: model:ir.model.fields,help:account_usability.field_account_journal_hide_bank_statement_balance
|
||||
msgid "You may want to enable this option when your bank journal is generated from a bank statement file that doesn't handle start/end balance (QIF for instance) and you don't want to enter the start/end balance manually: it will prevent the display of wrong information in the accounting dashboard and on bank statements."
|
||||
msgstr "You may want to enable this option when your bank journal is generated from a bank statement file that doesn't handle start/end balance (QIF for instance) and you don't want to enter the start/end balance manually: it will prevent the display of wrong information in the accounting dashboard and on bank statements."
|
||||
|
||||
#. module: account_usability
|
||||
#: model:ir.ui.view,arch_db:account_usability.invoice_supplier_form
|
||||
msgid "⇒ Delete lines qty=0"
|
||||
msgstr "⇒ Supprimer les lignes qté=0"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2015-2016 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 api, fields, models, _
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
# DON'T put store=True on those fields, because they are company dependent
|
||||
sale_price_type = fields.Selection(
|
||||
'_sale_purchase_price_type_sel', compute='_compute_sale_price_type',
|
||||
string='Sale Price Type', compute_sudo=False, readonly=True)
|
||||
purchase_price_type = fields.Selection(
|
||||
'_sale_purchase_price_type_sel', compute='_compute_purchase_price_type',
|
||||
string='Purchase Price Type', compute_sudo=False, readonly=True)
|
||||
|
||||
@api.model
|
||||
def _sale_purchase_price_type_sel(self):
|
||||
return [('incl', _('Tax incl.')), ('excl', _('Tax excl.'))]
|
||||
|
||||
@api.depends('taxes_id')
|
||||
def _compute_sale_price_type(self):
|
||||
for pt in self:
|
||||
sale_price_type = 'incl'
|
||||
if pt.taxes_id and all([not t.price_include for t in pt.taxes_id if t.amount_type == 'percent']):
|
||||
sale_price_type = 'excl'
|
||||
pt.sale_price_type = sale_price_type
|
||||
|
||||
@api.depends('supplier_taxes_id')
|
||||
def _compute_purchase_price_type(self):
|
||||
for pt in self:
|
||||
purchase_price_type = 'incl'
|
||||
if pt.supplier_taxes_id and all([not t.price_include for t in pt.supplier_taxes_id if t.amount_type == 'percent']):
|
||||
purchase_price_type = 'excl'
|
||||
pt.purchase_price_type = purchase_price_type
|
||||
|
||||
|
||||
class ProductSupplierinfo(models.Model):
|
||||
_inherit = 'product.supplierinfo'
|
||||
|
||||
# DON'T put store=True on those fields, because they are company dependent
|
||||
purchase_price_type = fields.Selection(
|
||||
related='product_tmpl_id.purchase_price_type', related_sudo=False)
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017-2020 Akretion (http://www.akretion.com/)
|
||||
© 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).
|
||||
-->
|
||||
@@ -16,7 +16,6 @@ Here, we set all those fields on account.group_account_invoice
|
||||
<record id="product_template_form_view" model="ir.ui.view">
|
||||
<field name="name">account_usability.product.template.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="priority">100</field> <!-- when you replace a field, it's always better to inherit at the end -->
|
||||
<field name="inherit_id" ref="account.product_template_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="property_account_income_id" position="attributes">
|
||||
@@ -25,14 +24,6 @@ Here, we set all those fields on account.group_account_invoice
|
||||
<field name="property_account_expense_id" position="attributes">
|
||||
<attribute name="groups">account.group_account_invoice</attribute>
|
||||
</field>
|
||||
<field name="list_price" position="replace">
|
||||
<label for="list_price"/>
|
||||
<div name="list_price">
|
||||
<field name="list_price" widget='monetary' options="{'currency_field': 'currency_id'}" class="oe_inline"/>
|
||||
<label for="sale_price_type" string=" "/>
|
||||
<field name="sale_price_type"/>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -47,27 +38,5 @@ Here, we set all those fields on account.group_account_invoice
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_supplierinfo_form_view" model="ir.ui.view">
|
||||
<field name="name">account_usability.product.supplierinfo.form</field>
|
||||
<field name="model">product.supplierinfo</field>
|
||||
<field name="inherit_id" ref="product.product_supplierinfo_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="currency_id" position="after">
|
||||
<field name="purchase_price_type"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_supplierinfo_tree_view" model="ir.ui.view">
|
||||
<field name="name">account_usability.product.supplierinfo.tree</field>
|
||||
<field name="model">product.supplierinfo</field>
|
||||
<field name="inherit_id" ref="product.product_supplierinfo_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="price" position="after">
|
||||
<field name="purchase_price_type" string="Tax"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -24,9 +24,6 @@ def migrate(cr, version):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
rppo = env['res.partner.phone']
|
||||
|
||||
# ondelete='cascade' was missing in previous versions
|
||||
cr.execute('DELETE FROM res_partner_phone WHERE partner_id is null')
|
||||
|
||||
wdict = {} # key = partnerID, values = {id: {'type': '1_home', 'phone': '+33'}}
|
||||
for rec in rppo.search_read([('type', '!=', False)], ['type', 'phone', 'partner_id', 'note']):
|
||||
if rec['partner_id'][0] not in wdict:
|
||||
|
||||
@@ -113,17 +113,13 @@ class ResPartner(models.Model):
|
||||
phone_ids = fields.One2many(
|
||||
'res.partner.phone', 'partner_id', string='Phones')
|
||||
phone = Phone(
|
||||
compute='_compute_partner_phone',
|
||||
store=True, readonly=True, compute_sudo=True)
|
||||
compute='_compute_partner_phone', store=True, readonly=True)
|
||||
mobile = Phone(
|
||||
compute='_compute_partner_phone',
|
||||
store=True, readonly=True, compute_sudo=True)
|
||||
compute='_compute_partner_phone', store=True, readonly=True)
|
||||
fax = Fax(
|
||||
compute='_compute_partner_phone',
|
||||
store=True, readonly=True, compute_sudo=True)
|
||||
compute='_compute_partner_phone', store=True, readonly=True)
|
||||
email = fields.Char(
|
||||
compute='_compute_partner_phone',
|
||||
store=True, readonly=True, compute_sudo=True)
|
||||
compute='_compute_partner_phone', store=True, readonly=True)
|
||||
|
||||
@api.depends('phone_ids.phone', 'phone_ids.type', 'phone_ids.email')
|
||||
def _compute_partner_phone(self):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock
|
||||
from . import sale_report
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 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 fields, models
|
||||
|
||||
|
||||
class SaleReport(models.Model):
|
||||
_inherit = 'sale.report'
|
||||
|
||||
carrier_id = fields.Many2one(
|
||||
"delivery.carrier", string="Delivery Method", readonly=True)
|
||||
|
||||
def _select(self):
|
||||
select_str = super(SaleReport, self)._select()
|
||||
select_str += ", s.carrier_id as carrier_id"
|
||||
return select_str
|
||||
|
||||
def _group_by(self):
|
||||
groupby_str = super(SaleReport, self)._group_by()
|
||||
groupby_str += ", s.carrier_id"
|
||||
return groupby_str
|
||||
13
mail_single_send_several_recipients/README.rst
Normal file
13
mail_single_send_several_recipients/README.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
Mail Single Send with Several Recipients
|
||||
========================================
|
||||
|
||||
With this module, when there are several recipients,
|
||||
Odoo sends only one single e-mail with all the addressees instead of one email per address.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Chafique Delli (chafique.delli@akretion.com)
|
||||
3
mail_single_send_several_recipients/__init__.py
Normal file
3
mail_single_send_several_recipients/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
12
mail_single_send_several_recipients/__manifest__.py
Normal file
12
mail_single_send_several_recipients/__manifest__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Mail Single Send with Several Recipients',
|
||||
'version': '10.0.1.0.0',
|
||||
'category': 'Mail',
|
||||
'license': 'AGPL-3',
|
||||
'summary': "When there are several recipients, always send only one e-mail for all the addressees",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['mail'],
|
||||
'installable': True,
|
||||
}
|
||||
3
mail_single_send_several_recipients/models/__init__.py
Normal file
3
mail_single_send_several_recipients/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import mail_mail
|
||||
29
mail_single_send_several_recipients/models/mail_mail.py
Normal file
29
mail_single_send_several_recipients/models/mail_mail.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, api
|
||||
from email.utils import formataddr
|
||||
|
||||
|
||||
class MailMail(models.Model):
|
||||
_inherit = 'mail.mail'
|
||||
|
||||
@api.multi
|
||||
def send(self, auto_commit=False, raise_exception=False):
|
||||
for mail in self:
|
||||
email_to = []
|
||||
for partner in mail.recipient_ids:
|
||||
for partner_email in [formataddr((partner.name, partner.email))]:
|
||||
email_to.append(partner_email)
|
||||
mail.email_to = email_to
|
||||
return super(MailMail, self).send(auto_commit=auto_commit,
|
||||
raise_exception=raise_exception)
|
||||
|
||||
@api.multi
|
||||
def send_get_mail_to(self, partner=None):
|
||||
super(MailMail, self).send_get_mail_to(partner=partner)
|
||||
self.ensure_one()
|
||||
email_to = []
|
||||
for partner in self.recipient_ids:
|
||||
email_to.append(formataddr((partner.name, partner.email)))
|
||||
self.recipient_ids = [(6, 0, [])]
|
||||
return email_to
|
||||
@@ -29,7 +29,6 @@ Small usability improvements on mails:
|
||||
'views/mail_view.xml',
|
||||
'data/mail_data.xml',
|
||||
'wizard/email_template_preview_view.xml',
|
||||
'wizard/mail_compose_message_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@ class ResPartner(models.Model):
|
||||
('all_except_notification', 'All Messages Except Notifications')],
|
||||
default='all_except_notification')
|
||||
opt_out = fields.Boolean(track_visibility='onchange')
|
||||
# This field is designed to be included in mail templates
|
||||
display_address_mail_template = fields.Text(compute='_compute_display_address_mail_template', string='Display Address in Mail Template')
|
||||
|
||||
def _compute_display_address_mail_template(self):
|
||||
for partner in self:
|
||||
partner.display_address_mail_template = partner._display_address(without_company=True)
|
||||
|
||||
def _should_be_notify_by_email(self, message):
|
||||
if message.message_type == 'notification':
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="email_compose_message_wizard_form" model="ir.ui.view">
|
||||
<field name="model">mail.compose.message</field>
|
||||
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="no_auto_thread" position="before">
|
||||
<field name="auto_delete_message" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -139,7 +139,7 @@ class MrpBom(models.Model):
|
||||
if float_compare(
|
||||
pt.standard_price, self.total_cost,
|
||||
precision_digits=precision):
|
||||
pt.write({'standard_price': self.total_cost})
|
||||
pt.standard_price = self.total_cost
|
||||
logger.info(
|
||||
'Cost price updated to %s on product %s',
|
||||
self.total_cost, pt.display_name)
|
||||
@@ -161,18 +161,12 @@ class MrpBom(models.Model):
|
||||
class MrpBomLine(models.Model):
|
||||
_inherit = 'mrp.bom.line'
|
||||
|
||||
# In v10, don't put a property field as related field
|
||||
# because it won't have the right value in multi-company context
|
||||
standard_price = fields.Float(
|
||||
compute='_compute_standard_price', readonly=True)
|
||||
related='product_id.standard_price', readonly=True)
|
||||
company_currency_id = fields.Many2one(
|
||||
related='bom_id.company_id.currency_id', readonly=True,
|
||||
string='Company Currency')
|
||||
|
||||
def _compute_standard_price(self):
|
||||
for line in self:
|
||||
line.standard_price = line.product_id.standard_price
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
@@ -199,10 +193,10 @@ class MrpProduction(models.Model):
|
||||
unit_cost_bom_uom =\
|
||||
self.bom_id.total_cost / self.bom_id.product_qty
|
||||
unit_cost_mo_uom = self.bom_id.product_uom_id._compute_quantity(
|
||||
unit_cost_bom_uom, self.product_uom_id, round=False)
|
||||
unit_cost_bom_uom, self.product_uom_id)
|
||||
# MO and finished move are in the same UoM
|
||||
move.write({'price_unit': unit_cost_mo_uom})
|
||||
self.write({'unit_cost': unit_cost_mo_uom})
|
||||
move.price_unit = unit_cost_mo_uom
|
||||
self.unit_cost = unit_cost_mo_uom
|
||||
return move
|
||||
|
||||
# No need to write directly on standard_price of product
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import wizard
|
||||
|
||||
@@ -23,7 +23,6 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
|
||||
'depends': ['procurement'],
|
||||
'data': [
|
||||
'procurement_view.xml',
|
||||
'wizard/procurement_mass_cancel_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import procurement_mass_cancel
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 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 models
|
||||
|
||||
|
||||
class ProcurementMassCancel(models.TransientModel):
|
||||
_name = 'procurement.mass.cancel'
|
||||
_description = 'Procurement Order Mass Cancel'
|
||||
|
||||
def run(self):
|
||||
assert self.env.context.get('active_model') == 'procurement.order'
|
||||
active_ids = self.env.context.get('active_ids')
|
||||
assert active_ids
|
||||
procs = self.env['procurement.order'].browse(active_ids)
|
||||
procs.cancel() # they already do the filtered on state != done
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="procurement_mass_cancel_form" model="ir.ui.view">
|
||||
<field name="model">procurement.mass.cancel</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group name="main">
|
||||
<div>This wizard will cancel the selected procurements which are not already in <i>Done</i> state.</div>
|
||||
</group>
|
||||
<footer>
|
||||
<button type="object" name="run" string="Cancel Procurements" class="btn-primary"/>
|
||||
<button special="cancel" string="Close" class="btn-default"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
id="procurement_mass_cancel_action"
|
||||
name="Cancel Procurements"
|
||||
res_model="procurement.mass.cancel"
|
||||
src_model="procurement.order"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
multi="True"
|
||||
key2="client_action_multi"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
@@ -12,7 +12,7 @@
|
||||
'description': "",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['account', 'base_suspend_security'],
|
||||
'depends': ['account'],
|
||||
'data': ['product_view.xml'],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -36,15 +36,11 @@ class ProductCategTaxMixin(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# suspend_security() is needed to read/set taxes in all companies
|
||||
self = self.suspend_security()
|
||||
self.write_or_create(vals)
|
||||
return super(ProductCategTaxMixin, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# suspend_security() is needed to read/set taxes in all companies
|
||||
self = self.suspend_security()
|
||||
self.write_or_create(vals)
|
||||
return super(ProductCategTaxMixin, self).write(vals)
|
||||
|
||||
@@ -54,7 +50,7 @@ class ProductTemplate(models.Model):
|
||||
_name = 'product.template'
|
||||
|
||||
@api.one
|
||||
@api.constrains('taxes_id', 'supplier_taxes_id', 'categ_id')
|
||||
@api.constrains('taxes_id', 'supplier_taxes_id')
|
||||
def _check_tax_categ(self):
|
||||
# self.name != 'Pay Debt' is a stupid hack to avoid blocking the
|
||||
# installation of the module 'pos_debt_notebook'
|
||||
@@ -64,7 +60,7 @@ class ProductTemplate(models.Model):
|
||||
"The sale taxes configured on the product '%s' "
|
||||
"are not the same as the sale taxes configured "
|
||||
"on it's related internal category '%s'.")
|
||||
% (self.name, self.categ_id.display_name))
|
||||
% (self.name, self.categ_id.name_get()[0][1]))
|
||||
if (
|
||||
self.categ_id.purchase_tax_ids.ids !=
|
||||
self.supplier_taxes_id.ids):
|
||||
@@ -72,7 +68,7 @@ class ProductTemplate(models.Model):
|
||||
"The purchase taxes configured on the product '%s' "
|
||||
"are not the same as the purchase taxes configured "
|
||||
"on it's related internal category '%s'.")
|
||||
% (self.name, self.categ_id.display_name))
|
||||
% (self.name, self.categ_id.name_get()[0][1]))
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
<field name="date_approve" position="after">
|
||||
<field name="origin"/>
|
||||
</field>
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='company_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -41,11 +38,9 @@
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- We can't show a non-stored computed field in tree view
|
||||
otherwise we have perf issues
|
||||
<field name="state" position="after">
|
||||
<field name="is_shipped" invisible="not context.get('show_purchase', False)"/>
|
||||
</field> -->
|
||||
</field>
|
||||
<!-- the 'origin' field can be very long ; it can list a lot of MO or OP!
|
||||
I think limiting the size of the field would not be the best option,
|
||||
because the info it carries can be interesting. So we just remove it from
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sale
|
||||
from . import wizard
|
||||
|
||||
@@ -19,8 +19,6 @@ When you confirm a quotation, Odoo will open a small wizard where you will be ab
|
||||
* invoicing address,
|
||||
* payment terms.
|
||||
|
||||
It will also display the sale warning if the customer's company has one. And it is a blocker warning, the user won't be able to confirm the quotation.
|
||||
|
||||
This module has been developped because the experience has shown, when a sales assistant confirms a quotation in Odoo, it overlooks the important information written in the customer PO that may be different from the information of the quotation in Odoo, which causes many errors in delivery and invoicing.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 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).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
def sale_confirm_wizard_button(self):
|
||||
"""This method is designed to be inherited.
|
||||
For example, inherit it if you don't want to start the wizard in
|
||||
some scenarios"""
|
||||
action = self.env.ref(
|
||||
'sale_confirm_wizard.sale_confirm_action').read()[0]
|
||||
return action
|
||||
@@ -12,10 +12,12 @@
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_confirm" type="object" states="draft" position="attributes">
|
||||
<attribute name="name">sale_confirm_wizard_button</attribute>
|
||||
<attribute name="type">action</attribute>
|
||||
<attribute name="name">%(sale_confirm_action)d</attribute>
|
||||
</button>
|
||||
<button name="action_confirm" type="object" states="sent" position="attributes">
|
||||
<attribute name="name">sale_confirm_wizard_button</attribute>
|
||||
<attribute name="type">action</attribute>
|
||||
<attribute name="name">%(sale_confirm_action)d</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
# © 2017 Akretion (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.exceptions import UserError
|
||||
from odoo.addons.base.res.res_partner import WARNING_MESSAGE
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleConfirm(models.TransientModel):
|
||||
@@ -26,21 +24,15 @@ class SaleConfirm(models.TransientModel):
|
||||
show_partner_shipping_id = fields.Many2one(
|
||||
related='partner_shipping_id', readonly=True,
|
||||
string='Detailed Delivery Address')
|
||||
sale_warn = fields.Selection(
|
||||
WARNING_MESSAGE, 'Sale Warning Type', readonly=True)
|
||||
sale_warn_msg = fields.Text(string='Sale Warning Message', readonly=True)
|
||||
|
||||
@api.model
|
||||
def _prepare_default_get(self, order):
|
||||
partner = order.partner_id.commercial_partner_id
|
||||
default = {
|
||||
'sale_id': order.id,
|
||||
'client_order_ref': order.client_order_ref,
|
||||
'payment_term_id': order.payment_term_id.id or False,
|
||||
'partner_invoice_id': order.partner_invoice_id.id,
|
||||
'partner_shipping_id': order.partner_shipping_id.id,
|
||||
'sale_warn_msg': partner.sale_warn_msg,
|
||||
'sale_warn': partner.sale_warn,
|
||||
}
|
||||
return default
|
||||
|
||||
@@ -67,12 +59,6 @@ class SaleConfirm(models.TransientModel):
|
||||
@api.multi
|
||||
def confirm(self):
|
||||
self.ensure_one()
|
||||
partner = self.sale_id.partner_id.commercial_partner_id
|
||||
if partner.sale_warn == 'block':
|
||||
raise UserError(_(
|
||||
"You cannot confirm this quotation because "
|
||||
"customer '%s' has a blocker sale warning:\n\n%s")
|
||||
% (partner.display_name, partner.sale_warn_msg))
|
||||
vals = self._prepare_update_so()
|
||||
self.sale_id.write(vals)
|
||||
# confirm sale order
|
||||
|
||||
@@ -12,12 +12,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Confirm Order">
|
||||
<div><p>At this stage, you have received the Purchase Order from the customer and you are about to convert the related quotation to an order.</p></div>
|
||||
<group name="warn" groups="sale.group_warning_sale" attrs="{'invisible': ['|', ('sale_warn', '=', False), ('sale_warn', '=', 'no-message')]}" string="Warning" col="4">
|
||||
<field name="sale_warn" nolabel="1"/>
|
||||
<field name="sale_warn_msg" nolabel="1" colspan="3"/>
|
||||
</group>
|
||||
<group name="main" attrs="{'invisible': [('sale_warn', '=', 'block')]}">
|
||||
<field name="sale_id" invisible="1"/>
|
||||
<group name="main">
|
||||
<field name="sale_id"/>
|
||||
<field name="client_order_ref"/>
|
||||
<field name="partner_invoice_id" context="{'default_type': 'invoice'}"
|
||||
groups="sale.group_delivery_invoice_address"/>
|
||||
@@ -36,7 +32,8 @@
|
||||
</group>
|
||||
<footer>
|
||||
<button type="object" name="confirm"
|
||||
string="Confirm Sale" class="btn-primary" attrs="{'invisible': [('sale_warn', '=', 'block')]}"/>
|
||||
string="Confirm Sale" class="btn-primary"/>
|
||||
or
|
||||
<button special="cancel" string="Annuler" class="btn-default"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
'description': """
|
||||
This module adds a wizard *Add Kit* on the form view of a quotation that allows the user to select a 'kit' BOM: Odoo will automatically add the components of the kit as sale order lines.
|
||||
|
||||
The wizard *Add Kit* is also available on a draft picking.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
@@ -21,8 +19,7 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
'depends': ['sale', 'mrp'],
|
||||
'data': [
|
||||
'wizard/sale_add_phantom_bom_view.xml',
|
||||
'views/sale_order.xml',
|
||||
'views/stock_picking.xml',
|
||||
'sale_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2016-2021 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
© 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_picking_form" model="ir.ui.view">
|
||||
<field name="name">add.bom.stock.picking.form</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_confirm" position="after">
|
||||
<button name="%(sale_add_phantom_bom_action)d" type="action"
|
||||
string="Add Kit" states="draft" groups="stock.group_stock_user"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -12,26 +12,19 @@ class SaleAddPhantomBom(models.TransientModel):
|
||||
_description = 'Add Kit to Quotation'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super(SaleAddPhantomBom, self).default_get(fields_list)
|
||||
if self._context.get('active_model') == 'sale.order':
|
||||
res['sale_id'] = self._context['active_id']
|
||||
elif self._context.get('active_model') == 'stock.picking':
|
||||
res['picking_id'] = self._context['active_id']
|
||||
else:
|
||||
raise UserError(_(
|
||||
"The wizard can only be started from a sale order or a picking."))
|
||||
return res
|
||||
def _default_sale_id(self):
|
||||
assert self._context.get('active_model') == 'sale.order'
|
||||
return self.env['sale.order'].browse(self._context['active_id'])
|
||||
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Kit', required=True,
|
||||
domain=[('type', '=', 'phantom'), ('sale_ok', '=', True)])
|
||||
qty = fields.Integer(
|
||||
string='Number of Kits to Add', default=1, required=True)
|
||||
# I can 't put the sale_id fields required=True because
|
||||
# it may block the deletion of a sale order
|
||||
sale_id = fields.Many2one(
|
||||
'sale.order', string='Quotation')
|
||||
picking_id = fields.Many2one(
|
||||
'stock.picking', string='Picking')
|
||||
'sale.order', string='Quotation', default=_default_sale_id)
|
||||
|
||||
@api.model
|
||||
def _prepare_sale_order_line(self, bom_line, sale_order, wizard_qty):
|
||||
@@ -43,30 +36,12 @@ class SaleAddPhantomBom(models.TransientModel):
|
||||
'product_uom_qty': qty_in_product_uom * wizard_qty,
|
||||
'order_id': sale_order.id,
|
||||
}
|
||||
# on sale.order.line, company_id is a related field
|
||||
return vals
|
||||
|
||||
@api.model
|
||||
def _prepare_stock_move(self, bom_line, picking, wizard_qty):
|
||||
product = bom_line.product_id
|
||||
qty_in_product_uom = bom_line.product_uom_id._compute_quantity(
|
||||
bom_line.product_qty, product.uom_id)
|
||||
vals = {
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': qty_in_product_uom * wizard_qty,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'company_id': picking.company_id.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
'name': product.partner_ref,
|
||||
}
|
||||
return vals
|
||||
|
||||
@api.multi
|
||||
def add(self):
|
||||
self.ensure_one()
|
||||
assert self.sale_id or self.picking_id, 'No related sale_id or picking_id'
|
||||
assert self.sale_id, 'No related sale_id'
|
||||
if self.qty < 1:
|
||||
raise UserError(_(
|
||||
"The number of kits to add must be 1 or superior"))
|
||||
@@ -75,8 +50,8 @@ class SaleAddPhantomBom(models.TransientModel):
|
||||
raise UserError(_("The selected kit is empty !"))
|
||||
prec = self.env['decimal.precision'].precision_get(
|
||||
'Product Unit of Measure')
|
||||
today = fields.Date.context_today(self)
|
||||
solo = self.env['sale.order.line']
|
||||
smo = self.env['stock.move']
|
||||
for line in self.bom_id.bom_line_ids:
|
||||
if float_is_zero(line.product_qty, precision_digits=prec):
|
||||
continue
|
||||
@@ -84,10 +59,6 @@ class SaleAddPhantomBom(models.TransientModel):
|
||||
# of sale order line in the 'sale' module
|
||||
# TODO: if needed, we could increment existing order lines
|
||||
# with the same product instead of always creating new lines
|
||||
if self.sale_id:
|
||||
vals = self._prepare_sale_order_line(line, self.sale_id, self.qty)
|
||||
solo.create(vals)
|
||||
elif self.picking_id:
|
||||
vals = self._prepare_stock_move(line, self.picking_id, self.qty)
|
||||
smo.create(vals)
|
||||
vals = self._prepare_sale_order_line(line, self.sale_id, self.qty)
|
||||
solo.create(vals)
|
||||
return True
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</group>
|
||||
<footer>
|
||||
<button name="add" type="object"
|
||||
class="oe_highlight" string="Add"/>
|
||||
class="oe_highlight" string="Add to Quotation"/>
|
||||
<button special="cancel" string="Cancel" class="oe_link"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -18,13 +18,12 @@ The usability enhancements include:
|
||||
|
||||
* *To invoice* filter on pickings filters on invoice_state = 2binvoiced AND state = done
|
||||
* Add a tab with the list of related pickings in sale order form
|
||||
* Show field *To refund in SO* on stock.move form view
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sale_stock', 'stock_usability'],
|
||||
'depends': ['sale_stock'],
|
||||
'data': ['sale_stock_view.xml'],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -34,27 +34,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_form" model="ir.ui.view">
|
||||
<field name="name">sale_stock_usability.stock.move.form</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="inherit_id" ref="stock_usability.view_move_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="price_unit" position="after">
|
||||
<field name="to_refund_so" readonly="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_picking_form" model="ir.ui.view">
|
||||
<field name="name">sale_stock_usability.stock.move.picking.form</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="inherit_id" ref="stock_usability.view_move_picking_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="price_unit" position="after">
|
||||
<field name="to_refund_so" readonly="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import float_is_zero
|
||||
from collections import OrderedDict
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools.misc import formatLang
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
@@ -27,9 +25,6 @@ class SaleOrder(models.Model):
|
||||
# for reports
|
||||
has_discount = fields.Boolean(
|
||||
compute='_compute_has_discount', readonly=True)
|
||||
has_attachment = fields.Boolean(
|
||||
compute='_compute_has_attachment',
|
||||
search='_search_has_attachment', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def _compute_has_discount(self):
|
||||
@@ -42,30 +37,6 @@ class SaleOrder(models.Model):
|
||||
break
|
||||
order.has_discount = has_discount
|
||||
|
||||
def _compute_has_attachment(self):
|
||||
iao = self.env['ir.attachment']
|
||||
for order in self:
|
||||
if iao.search_count([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', order.id),
|
||||
('type', '=', 'binary'),
|
||||
('company_id', '=', order.company_id.id)]):
|
||||
order.has_attachment = True
|
||||
else:
|
||||
order.has_attachment = False
|
||||
|
||||
def _search_has_attachment(self, operator, value):
|
||||
att_order_ids = {}
|
||||
if operator == '=':
|
||||
search_res = self.env['ir.attachment'].search_read([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('type', '=', 'binary'),
|
||||
('res_id', '!=', False)], ['res_id'])
|
||||
for att in search_res:
|
||||
att_order_ids[att['res_id']] = True
|
||||
res = [('id', value and 'in' or 'not in', att_order_ids.keys())]
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def action_confirm(self):
|
||||
'''Reload view upon order confirmation to display the 3 qty cols'''
|
||||
@@ -119,37 +90,6 @@ class SaleOrder(models.Model):
|
||||
return res2
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
@api.onchange('product_uom', 'product_uom_qty')
|
||||
def product_uom_change(self):
|
||||
# When the user has manually set a custom price
|
||||
# he is often upset when Odoo changes it when he changes the qty
|
||||
# So we add a warning in which we recall the old price.
|
||||
res = {}
|
||||
old_price = self.price_unit
|
||||
super(SaleOrderLine, self).product_uom_change()
|
||||
new_price = self.price_unit
|
||||
prec = self.env['decimal.precision'].precision_get('Product Price')
|
||||
if float_compare(old_price, new_price, precision_digits=prec):
|
||||
pricelist = self.order_id.pricelist_id
|
||||
cur_symbol = pricelist.currency_id.symbol
|
||||
res['warning'] = {
|
||||
'title': _('Price updated'),
|
||||
'message': _(
|
||||
"Due to the update of the ordered quantity on line '%s', "
|
||||
"the price has been updated according to pricelist %s.\n"
|
||||
"Old price: %s\n"
|
||||
"New price: %s") % (
|
||||
self.name,
|
||||
pricelist.display_name,
|
||||
formatLang(self.env, old_price, currency_obj=pricelist.currency_id),
|
||||
formatLang(self.env, new_price, currency_obj=pricelist.currency_id))
|
||||
}
|
||||
return res
|
||||
|
||||
|
||||
class ProcurementGroup(models.Model):
|
||||
_inherit = 'procurement.group'
|
||||
|
||||
|
||||
@@ -75,10 +75,6 @@
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="groups">analytic.group_analytic_accounting</attribute>
|
||||
</field>
|
||||
<filter name="message_needaction" position="before">
|
||||
<filter name="no_attachment" string="Missing Attachment" domain="[('has_attachment', '=', False)]"/>
|
||||
<separator/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 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).
|
||||
##############################################################################
|
||||
#
|
||||
# Stock Account Usability module for Odoo
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
{
|
||||
'name': 'Stock Account Usability',
|
||||
'version': '10.0.1.0.0',
|
||||
'version': '0.1',
|
||||
'category': 'Inventory, Logistic, Storage',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Several usability enhancements in stock_account',
|
||||
@@ -14,13 +32,13 @@ Stock Account Usability
|
||||
========================
|
||||
|
||||
The usability enhancements inclure:
|
||||
* show property_cost_method on product form
|
||||
* tracking on the invoice_state field
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['stock_account'],
|
||||
'data': ['product_view.xml'],
|
||||
'installable': True,
|
||||
'data': [],
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_template_property_form" model="ir.ui.view">
|
||||
<field name="name">stock_account.product.template.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock_account.view_template_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!--
|
||||
I use replace instead of attributes with invisible=0
|
||||
because I need a smooth display of property_cost_method even when
|
||||
account_usability is installed, which replaces the field list_price
|
||||
-->
|
||||
<field name="property_cost_method" position="replace"/>
|
||||
<field name="company_id" position="before">
|
||||
<field name="property_cost_method" groups="stock_account.group_inventory_valuation"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
29
stock_account_usability/stock.py
Normal file
29
stock_account_usability/stock.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Stock Account Usability module for Odoo
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp import models, fields
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
invoice_state = fields.Selection(track_visibility='onchange')
|
||||
@@ -170,32 +170,6 @@ class StockIncoterms(models.Model):
|
||||
return res
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = 'stock.quant'
|
||||
|
||||
@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')])
|
||||
quants = self.search([
|
||||
('location_id', 'in', int_locs.ids),
|
||||
('package_id', '!=', False)])
|
||||
packages = quants.mapped('package_id')
|
||||
logger.info('Unpacking %d packages on internal locations', len(packages))
|
||||
packages.unpack()
|
||||
logger.info('END cron auto unpack on internal locations')
|
||||
|
||||
|
||||
class ProcurementGroup(models.Model):
|
||||
_inherit = 'procurement.group'
|
||||
|
||||
|
||||
@@ -14,5 +14,4 @@ class StockReturnPicking(models.TransientModel):
|
||||
self.product_return_moves.write({'quantity': 0})
|
||||
action = self.env.ref('stock.act_stock_return_picking').read()[0]
|
||||
action['res_id'] = self.id
|
||||
action['context'] = self._context
|
||||
return action
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import wizard
|
||||
from . import models
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{
|
||||
'name': 'Stock Valuation XLSX',
|
||||
'version': '10.0.1.0.1',
|
||||
'version': '10.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Generate XLSX reports for past or present stock levels',
|
||||
@@ -38,11 +38,8 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['stock_account'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/stock_valuation_xlsx_view.xml',
|
||||
'wizard/stock_variation_xlsx_view.xml',
|
||||
'views/stock_inventory.xml',
|
||||
'views/stock_expiry_depreciation_rule.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_expiry_depreciation_rule
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2021 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 StockExpiryDepreciationRule(models.Model):
|
||||
_name = 'stock.expiry.depreciation.rule'
|
||||
_description = 'Stock Expiry Depreciation Rule'
|
||||
_order = 'company_id, start_limit_days'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company',
|
||||
ondelete='cascade', required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
start_limit_days = fields.Integer(
|
||||
string='Days Before/After Expiry', required=True,
|
||||
help="Enter negative value for days before expiry. Enter positive values for days after expiry. This value is the START of the time interval when going from future to past.")
|
||||
ratio = fields.Integer(string='Depreciation Ratio (%)', required=True)
|
||||
name = fields.Char(string='Label')
|
||||
|
||||
_sql_constraints = [(
|
||||
'ratio_positive',
|
||||
'CHECK(ratio >= 0)',
|
||||
'The depreciation ratio must be positive.'
|
||||
), (
|
||||
'ratio_max',
|
||||
'CHECK(ratio <= 100)',
|
||||
'The depreciation ratio cannot be above 100%.'
|
||||
), (
|
||||
'start_limit_days_unique',
|
||||
'unique(company_id, start_limit_days)',
|
||||
'This depreciation rule already exists in this company.'
|
||||
)]
|
||||
@@ -1,3 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_stock_expiry_depreciation_rule_full,Full access on stock.expiry.depreciation.rule to account manager,model_stock_expiry_depreciation_rule,account.group_account_manager,1,1,1,1
|
||||
access_stock_expiry_depreciation_rule_read,Read access on stock.expiry.depreciation.rule to stock manager,model_stock_expiry_depreciation_rule,stock.group_stock_manager,1,0,0,0
|
||||
|
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="stock_expiry_depreciation_rule_tree" model="ir.ui.view">
|
||||
<field name="model">stock.expiry.depreciation.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="start_limit_days"/>
|
||||
<field name="ratio"/>
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_expiry_depreciation_rule_action" model="ir.actions.act_window">
|
||||
<field name="name">Stock Depreciation Rules</field>
|
||||
<field name="res_model">stock.expiry.depreciation.rule</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="stock_expiry_depreciation_rule_menu"
|
||||
action="stock_expiry_depreciation_rule_action"
|
||||
parent="account.account_management_menu"
|
||||
sequence="100"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_valuation_xlsx
|
||||
from . import stock_variation_xlsx
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo.tools import float_is_zero, float_round
|
||||
from cStringIO import StringIO
|
||||
from datetime import datetime
|
||||
@@ -35,8 +34,7 @@ class StockValuationXlsx(models.TransientModel):
|
||||
help="The childen locations of the selected locations will "
|
||||
u"be taken in the valuation.")
|
||||
categ_ids = fields.Many2many(
|
||||
'product.category', string='Product Category Filter',
|
||||
help="Leave this field empty to have a stock valuation for all your products.",
|
||||
'product.category', string='Product Categories',
|
||||
states={'done': [('readonly', True)]})
|
||||
source = fields.Selection([
|
||||
('inventory', 'Physical Inventory'),
|
||||
@@ -57,33 +55,17 @@ class StockValuationXlsx(models.TransientModel):
|
||||
categ_subtotal = fields.Boolean(
|
||||
string='Subtotals per Categories', default=True,
|
||||
states={'done': [('readonly', True)]},
|
||||
help="Show a subtotal per product category.")
|
||||
help="Show a subtotal per product category")
|
||||
standard_price_date = fields.Selection([
|
||||
('past', 'Past Date or Inventory Date'),
|
||||
('present', 'Current'),
|
||||
], default='past', string='Cost Price Date',
|
||||
states={'done': [('readonly', True)]})
|
||||
# I can't put a compute field for has_expiry_date
|
||||
# because I want to have the value when the wizard is started,
|
||||
# and not wait until run
|
||||
has_expiry_date = fields.Boolean(
|
||||
default=lambda self: self._default_has_expiry_date(), readonly=True)
|
||||
apply_depreciation = fields.Boolean(
|
||||
string='Apply Depreciation Rules', default=True,
|
||||
states={'done': [('readonly', True)]})
|
||||
split_by_lot = fields.Boolean(
|
||||
string='Display Lots', states={'done': [('readonly', True)]})
|
||||
split_by_location = fields.Boolean(
|
||||
string='Display Stock Locations', states={'done': [('readonly', True)]})
|
||||
|
||||
@api.model
|
||||
def _default_has_expiry_date(self):
|
||||
splo = self.env['stock.production.lot']
|
||||
has_expiry_date = False
|
||||
if hasattr(splo, 'expiry_date'):
|
||||
has_expiry_date = True
|
||||
return has_expiry_date
|
||||
|
||||
@api.model
|
||||
def _default_location(self):
|
||||
wh = self.env.ref('stock.warehouse0')
|
||||
@@ -137,23 +119,11 @@ class StockValuationXlsx(models.TransientModel):
|
||||
def _prepare_product_fields(self):
|
||||
return ['uom_id', 'name', 'default_code', 'categ_id']
|
||||
|
||||
def _prepare_expiry_depreciation_rules(self, company_id, past_date):
|
||||
rules = self.env['stock.expiry.depreciation.rule'].search_read([('company_id', '=', company_id)], ['start_limit_days', 'ratio'], order='start_limit_days desc')
|
||||
if past_date:
|
||||
date_dt = fields.Date.from_string(past_date)
|
||||
else:
|
||||
date_dt = fields.Date.from_string(fields.Date.context_today(self))
|
||||
for rule in rules:
|
||||
rule['start_date'] = fields.Date.to_string(
|
||||
date_dt - relativedelta(days=rule['start_limit_days']))
|
||||
logger.debug('depreciation_rules=%s', rules)
|
||||
return rules
|
||||
|
||||
def compute_product_data(
|
||||
self, company_id, in_stock_product_ids, standard_price_past_date=False):
|
||||
self.ensure_one()
|
||||
logger.debug('Start compute_product_data')
|
||||
ppo = self.env['product.product'].with_context(force_company=company_id)
|
||||
ppo = self.env['product.product']
|
||||
ppho = self.env['product.price.history']
|
||||
fields_list = self._prepare_product_fields()
|
||||
if not standard_price_past_date:
|
||||
@@ -182,56 +152,38 @@ class StockValuationXlsx(models.TransientModel):
|
||||
logger.debug('End compute_product_data')
|
||||
return product_id2data
|
||||
|
||||
@api.model
|
||||
def product_categ_id2name(self, categories):
|
||||
def id2name(self, product_ids):
|
||||
logger.debug('Start id2name')
|
||||
pco = self.env['product.category']
|
||||
splo = self.env['stock.production.lot']
|
||||
slo = self.env['stock.location'].with_context(active_test=False)
|
||||
puo = self.env['product.uom'].with_context(active_test=False)
|
||||
categ_id2name = {}
|
||||
categ_domain = []
|
||||
if categories:
|
||||
categ_domain = [('id', 'child_of', categories.ids)]
|
||||
if self.categ_ids:
|
||||
categ_domain = [('id', 'child_of', self.categ_ids.ids)]
|
||||
for categ in pco.search_read(categ_domain, ['display_name']):
|
||||
categ_id2name[categ['id']] = categ['display_name']
|
||||
return categ_id2name
|
||||
|
||||
@api.model
|
||||
def uom_id2name(self):
|
||||
puo = self.env['product.uom'].with_context(active_test=False)
|
||||
uom_id2name = {}
|
||||
uoms = puo.search_read([], ['name'])
|
||||
for uom in uoms:
|
||||
uom_id2name[uom['id']] = uom['name']
|
||||
return uom_id2name
|
||||
|
||||
@api.model
|
||||
def prodlot_id2data(self, product_ids, has_expiry_date, depreciation_rules):
|
||||
splo = self.env['stock.production.lot']
|
||||
lot_id2data = {}
|
||||
lot_fields = ['name']
|
||||
if has_expiry_date:
|
||||
if hasattr(splo, 'expiry_date'):
|
||||
lot_fields.append('expiry_date')
|
||||
|
||||
lots = splo.search_read(
|
||||
[('product_id', 'in', product_ids)], lot_fields)
|
||||
for lot in lots:
|
||||
lot_id2data[lot['id']] = lot
|
||||
lot_id2data[lot['id']]['depreciation_ratio'] = 0
|
||||
if depreciation_rules and lot.get('expiry_date'):
|
||||
expiry_date = lot['expiry_date']
|
||||
for rule in depreciation_rules:
|
||||
if expiry_date <= rule['start_date']:
|
||||
lot_id2data[lot['id']]['depreciation_ratio'] = rule['ratio'] / 100.0
|
||||
break
|
||||
return lot_id2data
|
||||
|
||||
@api.model
|
||||
def stock_location_id2name(self, location):
|
||||
slo = self.env['stock.location'].with_context(active_test=False)
|
||||
loc_id2name = {}
|
||||
locs = slo.search_read(
|
||||
[('id', 'child_of', location.id)], ['display_name'])
|
||||
[('id', 'child_of', self.location_id.id)], ['display_name'])
|
||||
for loc in locs:
|
||||
loc_id2name[loc['id']] = loc['display_name']
|
||||
return loc_id2name
|
||||
logger.debug('End id2name')
|
||||
return categ_id2name, uom_id2name, lot_id2data, loc_id2name
|
||||
|
||||
def compute_data_from_inventory(self, product_ids, prec_qty):
|
||||
self.ensure_one()
|
||||
@@ -283,7 +235,7 @@ class StockValuationXlsx(models.TransientModel):
|
||||
self.ensure_one()
|
||||
logger.debug('Start compute_data_from_past_stock past_date=%s', past_date)
|
||||
ppo = self.env['product.product']
|
||||
products = ppo.with_context(to_date=past_date, location=self.location_id.id).browse(product_ids)
|
||||
products = ppo.with_context(to_date=past_date, location_id=self.location_id.id).browse(product_ids)
|
||||
res = []
|
||||
in_stock_products = {}
|
||||
for product in products:
|
||||
@@ -319,7 +271,7 @@ class StockValuationXlsx(models.TransientModel):
|
||||
def stringify_and_sort_result(
|
||||
self, product_ids, product_id2data, data,
|
||||
prec_qty, prec_price, prec_cur_rounding, categ_id2name,
|
||||
uom_id2name, lot_id2data, loc_id2name, apply_depreciation):
|
||||
uom_id2name, lot_id2data, loc_id2name):
|
||||
logger.debug('Start stringify_and_sort_result')
|
||||
res = []
|
||||
for l in data:
|
||||
@@ -328,27 +280,17 @@ class StockValuationXlsx(models.TransientModel):
|
||||
standard_price = float_round(
|
||||
product_id2data[product_id]['standard_price'],
|
||||
precision_digits=prec_price)
|
||||
subtotal_before_depreciation = float_round(
|
||||
subtotal = float_round(
|
||||
standard_price * qty, precision_rounding=prec_cur_rounding)
|
||||
depreciation_ratio = 0
|
||||
if apply_depreciation and l['lot_id']:
|
||||
depreciation_ratio = lot_id2data[l['lot_id']].get('depreciation_ratio', 0)
|
||||
subtotal = float_round(
|
||||
subtotal_before_depreciation * (1 - depreciation_ratio),
|
||||
precision_rounding=prec_cur_rounding)
|
||||
else:
|
||||
subtotal = subtotal_before_depreciation
|
||||
res.append(dict(
|
||||
product_id2data[product_id],
|
||||
product_name=product_id2data[product_id]['name'],
|
||||
loc_name=l['location_id'] and loc_id2name[l['location_id']] or '',
|
||||
lot_name=l['lot_id'] and lot_id2data[l['lot_id']]['name'] or '',
|
||||
expiry_date=l['lot_id'] and lot_id2data[l['lot_id']].get('expiry_date'),
|
||||
depreciation_ratio=depreciation_ratio,
|
||||
qty=qty,
|
||||
uom_name=uom_id2name[product_id2data[product_id]['uom_id']],
|
||||
standard_price=standard_price,
|
||||
subtotal_before_depreciation=subtotal_before_depreciation,
|
||||
subtotal=subtotal,
|
||||
categ_name=categ_id2name[product_id2data[product_id]['categ_id']],
|
||||
))
|
||||
@@ -367,12 +309,6 @@ class StockValuationXlsx(models.TransientModel):
|
||||
prec_cur_rounding = company.currency_id.rounding
|
||||
self._check_config(company_id)
|
||||
|
||||
apply_depreciation = self.apply_depreciation
|
||||
if (
|
||||
(self.source == 'stock' and self.stock_date_type == 'past') or
|
||||
not self.split_by_lot or
|
||||
not self.has_expiry_date):
|
||||
apply_depreciation = False
|
||||
product_ids = self.get_product_ids()
|
||||
if not product_ids:
|
||||
raise UserError(_("There are no products to analyse."))
|
||||
@@ -392,32 +328,18 @@ class StockValuationXlsx(models.TransientModel):
|
||||
elif self.source == 'inventory':
|
||||
past_date = self.inventory_id.date
|
||||
data, in_stock_products = self.compute_data_from_inventory(product_ids, prec_qty)
|
||||
if self.source == 'stock' and self.stock_date_type == 'present':
|
||||
standard_price_past_date = past_date
|
||||
if not (self.source == 'stock' and self.stock_date_type == 'present') and self.standard_price_date == 'present':
|
||||
standard_price_past_date = False
|
||||
else: # field standard_price_date is shown on screen
|
||||
if self.standard_price_date == 'present':
|
||||
standard_price_past_date = False
|
||||
else:
|
||||
standard_price_past_date = past_date
|
||||
depreciation_rules = []
|
||||
if apply_depreciation:
|
||||
depreciation_rules = self._prepare_expiry_depreciation_rules(company_id, past_date)
|
||||
if not depreciation_rules:
|
||||
raise UserError(_(
|
||||
"The are not stock depreciation rule for company '%s'.")
|
||||
% company.display_name)
|
||||
in_stock_product_ids = in_stock_products.keys()
|
||||
product_id2data = self.compute_product_data(
|
||||
company_id, in_stock_product_ids,
|
||||
standard_price_past_date=standard_price_past_date)
|
||||
data_res = self.group_result(data, split_by_lot, split_by_location)
|
||||
categ_id2name = self.product_categ_id2name(self.categ_ids)
|
||||
uom_id2name = self.uom_id2name()
|
||||
lot_id2data = self.prodlot_id2data(in_stock_product_ids, self.has_expiry_date, depreciation_rules)
|
||||
loc_id2name = self.stock_location_id2name(self.location_id)
|
||||
categ_id2name, uom_id2name, lot_id2data, loc_id2name = self.id2name(in_stock_product_ids)
|
||||
res = self.stringify_and_sort_result(
|
||||
product_ids, product_id2data, data_res, prec_qty, prec_price, prec_cur_rounding,
|
||||
categ_id2name, uom_id2name, lot_id2data, loc_id2name, apply_depreciation)
|
||||
categ_id2name, uom_id2name, lot_id2data, loc_id2name)
|
||||
|
||||
logger.debug('Start create XLSX workbook')
|
||||
file_data = StringIO()
|
||||
@@ -430,15 +352,12 @@ class StockValuationXlsx(models.TransientModel):
|
||||
if not split_by_lot:
|
||||
cols.pop('lot_name', None)
|
||||
cols.pop('expiry_date', None)
|
||||
if not self.has_expiry_date:
|
||||
if not hasattr(splo, 'expiry_date'):
|
||||
cols.pop('expiry_date', None)
|
||||
if not split_by_location:
|
||||
cols.pop('loc_name', None)
|
||||
if not categ_subtotal:
|
||||
cols.pop('categ_subtotal', None)
|
||||
if not apply_depreciation:
|
||||
cols.pop('depreciation_ratio', None)
|
||||
cols.pop('subtotal_before_depreciation', None)
|
||||
|
||||
j = 0
|
||||
for col, col_vals in sorted(cols.items(), key=lambda x: x[1]['sequence']):
|
||||
@@ -495,9 +414,6 @@ class StockValuationXlsx(models.TransientModel):
|
||||
letter_qty = cols['qty']['pos_letter']
|
||||
letter_price = cols['standard_price']['pos_letter']
|
||||
letter_subtotal = cols['subtotal']['pos_letter']
|
||||
if apply_depreciation:
|
||||
letter_subtotal_before_depreciation = cols['subtotal_before_depreciation']['pos_letter']
|
||||
letter_depreciation_ratio = cols['depreciation_ratio']['pos_letter']
|
||||
crow = 0
|
||||
lines = res
|
||||
for categ_id in categ_ids:
|
||||
@@ -513,20 +429,12 @@ class StockValuationXlsx(models.TransientModel):
|
||||
total += l['subtotal']
|
||||
ctotal += l['subtotal']
|
||||
categ_has_line = True
|
||||
qty_by_price_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1)
|
||||
if apply_depreciation:
|
||||
sheet.write_formula(i, cols['subtotal_before_depreciation']['pos'], qty_by_price_formula, styles['regular_currency'], l['subtotal_before_depreciation'])
|
||||
subtotal_formula = '=%s%d*(1 - %s%d)' % (letter_subtotal_before_depreciation, i + 1, letter_depreciation_ratio, i + 1)
|
||||
else:
|
||||
subtotal_formula = qty_by_price_formula
|
||||
subtotal_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1)
|
||||
sheet.write_formula(i, cols['subtotal']['pos'], subtotal_formula, styles['regular_currency'], l['subtotal'])
|
||||
for col_name, col in cols.items():
|
||||
if not col.get('formula'):
|
||||
if col.get('type') == 'date':
|
||||
if l[col_name]:
|
||||
l[col_name] = fields.Date.from_string(l[col_name])
|
||||
else:
|
||||
l[col_name] = '' # to avoid display of 31/12/1899
|
||||
if col.get('type') == 'date' and l[col_name]:
|
||||
l[col_name] = fields.Date.from_string(l[col_name])
|
||||
sheet.write(i, col['pos'], l[col_name], styles[col['style']])
|
||||
if categ_subtotal:
|
||||
if categ_has_line:
|
||||
@@ -586,7 +494,6 @@ class StockValuationXlsx(models.TransientModel):
|
||||
'regular_date': workbook.add_format({'num_format': 'dd/mm/yyyy'}),
|
||||
'regular_currency': workbook.add_format({'num_format': currency_num_format}),
|
||||
'regular_price_currency': workbook.add_format({'num_format': price_currency_num_format}),
|
||||
'regular_int_percent': workbook.add_format({'num_format': u'0.%'}),
|
||||
'regular': workbook.add_format({}),
|
||||
'regular_small': workbook.add_format({'font_size': regular_font_size - 2}),
|
||||
'categ_title': workbook.add_format({
|
||||
@@ -611,10 +518,8 @@ class StockValuationXlsx(models.TransientModel):
|
||||
'qty': {'width': 8, 'style': 'regular', 'sequence': 60, 'title': _('Qty')},
|
||||
'uom_name': {'width': 5, 'style': 'regular_small', 'sequence': 70, 'title': _('UoM')},
|
||||
'standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 80, 'title': _('Cost Price')},
|
||||
'subtotal_before_depreciation': {'width': 16, 'style': 'regular_currency', 'sequence': 90, 'title': _('Sub-total'), 'formula': True},
|
||||
'depreciation_ratio': {'width': 10, 'style': 'regular_int_percent', 'sequence': 100, 'title': _('Depreciation')},
|
||||
'subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 110, 'title': _('Sub-total'), 'formula': True},
|
||||
'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 120, 'title': _('Categ Sub-total'), 'formula': True},
|
||||
'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 130, 'title': _('Product Category')},
|
||||
'subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 90, 'title': _('Sub-total'), 'formula': True},
|
||||
'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 100, 'title': _('Categ Sub-total'), 'formula': True},
|
||||
'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 110, 'title': _('Product Category')},
|
||||
}
|
||||
return cols
|
||||
|
||||
@@ -27,10 +27,8 @@
|
||||
<field name="past_date" attrs="{'invisible': ['|', ('source', '!=', 'stock'), ('stock_date_type', '!=', 'past')], 'required': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
|
||||
<field name="standard_price_date" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'present')]}" widget="radio"/>
|
||||
<field name="categ_subtotal" />
|
||||
<field name="has_expiry_date" invisible="1"/>
|
||||
<field name="split_by_lot" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}" groups="stock.group_production_lot"/>
|
||||
<field name="split_by_location" attrs="{'invisible': [('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
|
||||
<field name="apply_depreciation" groups="stock.group_production_lot" attrs="{'invisible': ['|', '|', ('split_by_lot', '=', False), ('has_expiry_date', '=', False), '&', ('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
|
||||
</group>
|
||||
<group name="done" states="done" string="Result">
|
||||
<field name="export_file" filename="export_filename"/>
|
||||
@@ -57,7 +55,6 @@
|
||||
<record id="stock_account.menu_action_wizard_valuation_history" model="ir.ui.menu">
|
||||
<field name="action" ref="stock_valuation_xlsx.stock_valuation_xlsx_action"/>
|
||||
<field name="name">Stock Valuation XLSX</field>
|
||||
<field name="sequence">0</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 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 models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero, float_round
|
||||
from cStringIO import StringIO
|
||||
from datetime import datetime
|
||||
import xlsxwriter
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockVariationXlsx(models.TransientModel):
|
||||
_name = 'stock.variation.xlsx'
|
||||
_description = 'Generate XLSX report for stock valuation variation between 2 dates'
|
||||
|
||||
export_file = fields.Binary(string='XLSX Report', readonly=True)
|
||||
export_filename = fields.Char(readonly=True)
|
||||
state = fields.Selection([
|
||||
('setup', 'Setup'),
|
||||
('done', 'Done'),
|
||||
], string='State', default='setup', readonly=True)
|
||||
warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', string='Warehouse',
|
||||
states={'done': [('readonly', True)]})
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', string='Root Stock Location', required=True,
|
||||
domain=[('usage', 'in', ('view', 'internal'))],
|
||||
default=lambda self: self._default_location(),
|
||||
states={'done': [('readonly', True)]},
|
||||
help="The childen locations of the selected locations will "
|
||||
"be taken in the valuation.")
|
||||
categ_ids = fields.Many2many(
|
||||
'product.category', string='Product Category Filter',
|
||||
help="Leave this fields empty to have a stock valuation for all your products.",
|
||||
states={'done': [('readonly', True)]})
|
||||
start_date = fields.Datetime(
|
||||
string='Start Date', required=True,
|
||||
states={'done': [('readonly', True)]})
|
||||
standard_price_start_date_type = fields.Selection([
|
||||
('start', 'Start Date'),
|
||||
('present', 'Current'),
|
||||
], default='start', required=True,
|
||||
string='Cost Price for Start Date',
|
||||
states={'done': [('readonly', True)]})
|
||||
end_date_type = fields.Selection([
|
||||
('present', 'Present'),
|
||||
('past', 'Past'),
|
||||
], string='End Date Type', default='present', required=True,
|
||||
states={'done': [('readonly', True)]})
|
||||
end_date = fields.Datetime(
|
||||
string='End Date', states={'done': [('readonly', True)]},
|
||||
default=fields.Datetime.now)
|
||||
standard_price_end_date_type = fields.Selection([
|
||||
('end', 'End Date'),
|
||||
('present', 'Current'),
|
||||
], default='end', string='Cost Price for End Date', required=True,
|
||||
states={'done': [('readonly', True)]})
|
||||
categ_subtotal = fields.Boolean(
|
||||
string='Subtotals per Categories', default=True,
|
||||
states={'done': [('readonly', True)]},
|
||||
help="Show a subtotal per product category.")
|
||||
|
||||
@api.model
|
||||
def _default_location(self):
|
||||
wh = self.env.ref('stock.warehouse0')
|
||||
return wh.lot_stock_id
|
||||
|
||||
@api.onchange('warehouse_id')
|
||||
def warehouse_id_change(self):
|
||||
if self.warehouse_id:
|
||||
self.location_id = self.warehouse_id.view_location_id.id
|
||||
|
||||
def _check_config(self, company_id):
|
||||
self.ensure_one()
|
||||
present = fields.Datetime.now()
|
||||
if self.end_date_type == 'past':
|
||||
if not self.end_date:
|
||||
raise UserError(_("End Date is missing."))
|
||||
if self.end_date > present:
|
||||
raise UserError(_("The end date must be in the past."))
|
||||
if self.end_date <= self.start_date:
|
||||
raise UserError(_("The start date must be before the end date."))
|
||||
else:
|
||||
if self.start_date >= present:
|
||||
raise UserError(_("The start date must be in the past."))
|
||||
cost_method_real_count = self.env['ir.property'].search([
|
||||
('company_id', '=', company_id),
|
||||
('name', '=', 'property_cost_method'),
|
||||
('value_text', '=', 'real'),
|
||||
('type', '=', 'selection'),
|
||||
], count=True)
|
||||
if cost_method_real_count:
|
||||
raise UserError(_(
|
||||
"There are %d properties that have "
|
||||
"'Costing Method' = 'Real Price'. This costing "
|
||||
"method is not supported by this module.")
|
||||
% cost_method_real_count)
|
||||
|
||||
def _prepare_product_domain(self):
|
||||
self.ensure_one()
|
||||
domain = [('type', '=', 'product')]
|
||||
if self.categ_ids:
|
||||
domain += [('categ_id', 'child_of', self.categ_ids.ids)]
|
||||
return domain
|
||||
|
||||
def get_product_ids(self):
|
||||
self.ensure_one()
|
||||
domain = self._prepare_product_domain()
|
||||
# Should we also add inactive products ??
|
||||
products = self.env['product.product'].search(domain)
|
||||
return products.ids
|
||||
|
||||
def _prepare_product_fields(self):
|
||||
return ['uom_id', 'name', 'default_code', 'categ_id']
|
||||
|
||||
def compute_product_data(
|
||||
self, company_id, filter_product_ids, standard_price_start_date=False, standard_price_end_date=False):
|
||||
self.ensure_one()
|
||||
logger.debug('Start compute_product_data')
|
||||
ppo = self.env['product.product'].with_context(force_company=company_id)
|
||||
ppho = self.env['product.price.history']
|
||||
fields_list = self._prepare_product_fields()
|
||||
if not standard_price_start_date or not standard_price_end_date:
|
||||
fields_list.append('standard_price')
|
||||
products = ppo.search_read([('id', 'in', filter_product_ids)], fields_list)
|
||||
product_id2data = {}
|
||||
for p in products:
|
||||
logger.debug('p=%d', p['id'])
|
||||
# I don't call the native method get_history_price()
|
||||
# because it requires a browse record and it is too slow
|
||||
if standard_price_start_date:
|
||||
history = ppho.search_read([
|
||||
('company_id', '=', company_id),
|
||||
('product_id', '=', p['id']),
|
||||
('datetime', '<=', standard_price_start_date)],
|
||||
['cost'], order='datetime desc, id desc', limit=1)
|
||||
start_standard_price = history and history[0]['cost'] or 0.0
|
||||
else:
|
||||
start_standard_price = p['standard_price']
|
||||
if standard_price_end_date:
|
||||
history = ppho.search_read([
|
||||
('company_id', '=', company_id),
|
||||
('product_id', '=', p['id']),
|
||||
('datetime', '<=', standard_price_end_date)],
|
||||
['cost'], order='datetime desc, id desc', limit=1)
|
||||
end_standard_price = history and history[0]['cost'] or 0.0
|
||||
else:
|
||||
end_standard_price = p['standard_price']
|
||||
|
||||
product_id2data[p['id']] = {
|
||||
'start_standard_price': start_standard_price,
|
||||
'end_standard_price': end_standard_price,
|
||||
}
|
||||
for pfield in fields_list:
|
||||
if pfield.endswith('_id'):
|
||||
product_id2data[p['id']][pfield] = p[pfield][0]
|
||||
else:
|
||||
product_id2data[p['id']][pfield] = p[pfield]
|
||||
logger.debug('End compute_product_data')
|
||||
return product_id2data
|
||||
|
||||
def compute_data_from_stock(self, product_ids, prec_qty, start_date, end_date_type, end_date, company_id):
|
||||
self.ensure_one()
|
||||
logger.debug('Start compute_data_from_stock past_date=%s end_date_type=%s, end_date=%s', start_date, end_date_type, end_date)
|
||||
ppo = self.env['product.product']
|
||||
smo = self.env['stock.move']
|
||||
sqo = self.env['stock.quant']
|
||||
ppo_loc = ppo.with_context(location=self.location_id.id, force_company=company_id)
|
||||
# Inspired by odoo/addons/stock/models/product.py
|
||||
# method _compute_quantities_dict()
|
||||
domain_quant_loc, domain_move_in_loc, domain_move_out_loc = ppo_loc._get_domain_locations()
|
||||
domain_quant = [('product_id', 'in', product_ids)] + domain_quant_loc
|
||||
domain_move_in = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_in_loc
|
||||
domain_move_out = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_out_loc
|
||||
quants_res = dict((item['product_id'][0], item['qty']) for item in sqo.read_group(domain_quant, ['product_id', 'qty'], ['product_id'], orderby='id'))
|
||||
domain_move_in_start_to_end = [('date', '>', start_date)] + domain_move_in
|
||||
domain_move_out_start_to_end = [('date', '>', start_date)] + domain_move_out
|
||||
if end_date_type == 'past':
|
||||
|
||||
domain_move_in_end_to_present = [('date', '>', end_date)] + domain_move_in
|
||||
domain_move_out_end_to_present = [('date', '>', end_date)] + domain_move_out
|
||||
moves_in_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
|
||||
moves_out_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
|
||||
|
||||
domain_move_in_start_to_end += [('date', '<', end_date)]
|
||||
domain_move_out_start_to_end += [('date', '<', end_date)]
|
||||
|
||||
moves_in_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
|
||||
moves_out_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
|
||||
|
||||
product_data = {} # key = product_id , value = dict
|
||||
for product in ppo.browse(product_ids):
|
||||
end_qty = quants_res.get(product.id, 0.0)
|
||||
if end_date_type == 'past':
|
||||
end_qty += moves_out_res_end_to_present.get(product.id, 0.0) - moves_in_res_end_to_present.get(product.id, 0.0)
|
||||
in_qty = moves_in_res_start_to_end.get(product.id, 0.0)
|
||||
out_qty = moves_out_res_start_to_end.get(product.id, 0.0)
|
||||
start_qty = end_qty - in_qty + out_qty
|
||||
if (
|
||||
not float_is_zero(start_qty, precision_digits=prec_qty) or
|
||||
not float_is_zero(in_qty, precision_digits=prec_qty) or
|
||||
not float_is_zero(out_qty, precision_digits=prec_qty) or
|
||||
not float_is_zero(end_qty, precision_digits=prec_qty)):
|
||||
product_data[product.id] = {
|
||||
'product_id': product.id,
|
||||
'start_qty': start_qty,
|
||||
'in_qty': in_qty,
|
||||
'out_qty': out_qty,
|
||||
'end_qty': end_qty,
|
||||
}
|
||||
logger.debug('End compute_data_from_stock')
|
||||
return product_data
|
||||
|
||||
def stringify_and_sort_result(
|
||||
self, product_data, product_id2data, prec_qty, prec_price, prec_cur_rounding,
|
||||
categ_id2name, uom_id2name):
|
||||
logger.debug('Start stringify_and_sort_result')
|
||||
res = []
|
||||
for product_id, l in product_data.items():
|
||||
start_qty = float_round(l['start_qty'], precision_digits=prec_qty)
|
||||
in_qty = float_round(l['in_qty'], precision_digits=prec_qty)
|
||||
out_qty = float_round(l['out_qty'], precision_digits=prec_qty)
|
||||
end_qty = float_round(l['end_qty'], precision_digits=prec_qty)
|
||||
start_standard_price = float_round(
|
||||
product_id2data[product_id]['start_standard_price'],
|
||||
precision_digits=prec_price)
|
||||
end_standard_price = float_round(
|
||||
product_id2data[product_id]['end_standard_price'],
|
||||
precision_digits=prec_price)
|
||||
start_subtotal = float_round(
|
||||
start_standard_price * start_qty, precision_rounding=prec_cur_rounding)
|
||||
end_subtotal = float_round(
|
||||
end_standard_price * end_qty, precision_rounding=prec_cur_rounding)
|
||||
variation = float_round(
|
||||
end_subtotal - start_subtotal, precision_rounding=prec_cur_rounding)
|
||||
res.append(dict(
|
||||
product_id2data[product_id],
|
||||
product_name=product_id2data[product_id]['name'],
|
||||
start_qty=start_qty,
|
||||
start_standard_price=start_standard_price,
|
||||
start_subtotal=start_subtotal,
|
||||
in_qty=in_qty,
|
||||
out_qty=out_qty,
|
||||
end_qty=end_qty,
|
||||
end_standard_price=end_standard_price,
|
||||
end_subtotal=end_subtotal,
|
||||
variation=variation,
|
||||
uom_name=uom_id2name[product_id2data[product_id]['uom_id']],
|
||||
categ_name=categ_id2name[product_id2data[product_id]['categ_id']],
|
||||
))
|
||||
sort_res = sorted(res, key=lambda x: x['product_name'])
|
||||
logger.debug('End stringify_and_sort_result')
|
||||
return sort_res
|
||||
|
||||
def generate(self):
|
||||
self.ensure_one()
|
||||
logger.debug('Start generate XLSX stock variation report')
|
||||
svxo = self.env['stock.valuation.xlsx']
|
||||
prec_qty = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
prec_price = self.env['decimal.precision'].precision_get('Product Price')
|
||||
company = self.env.user.company_id
|
||||
company_id = company.id
|
||||
prec_cur_rounding = company.currency_id.rounding
|
||||
self._check_config(company_id)
|
||||
|
||||
product_ids = self.get_product_ids()
|
||||
if not product_ids:
|
||||
raise UserError(_("There are no products to analyse."))
|
||||
|
||||
product_data = self.compute_data_from_stock(
|
||||
product_ids, prec_qty, self.start_date, self.end_date_type, self.end_date,
|
||||
company_id)
|
||||
standard_price_start_date = standard_price_end_date = False
|
||||
if self.standard_price_start_date_type == 'start':
|
||||
standard_price_start_date = self.start_date
|
||||
if self.standard_price_end_date_type == 'end' and self.end_date_type == 'past':
|
||||
standard_price_end_date = self.end_date
|
||||
|
||||
product_id2data = self.compute_product_data(
|
||||
company_id, list(product_data.keys()),
|
||||
standard_price_start_date, standard_price_end_date)
|
||||
categ_id2name = svxo.product_categ_id2name(self.categ_ids)
|
||||
uom_id2name = svxo.uom_id2name()
|
||||
res = self.stringify_and_sort_result(
|
||||
product_data, product_id2data, prec_qty, prec_price, prec_cur_rounding,
|
||||
categ_id2name, uom_id2name)
|
||||
|
||||
logger.debug('Start create XLSX workbook')
|
||||
file_data = StringIO()
|
||||
workbook = xlsxwriter.Workbook(file_data)
|
||||
sheet = workbook.add_worksheet('Stock_Variation')
|
||||
styles = svxo._prepare_styles(workbook, company, prec_price)
|
||||
cols = self._prepare_cols()
|
||||
categ_subtotal = self.categ_subtotal
|
||||
# remove cols that we won't use
|
||||
if not categ_subtotal:
|
||||
cols.pop('categ_subtotal', None)
|
||||
|
||||
j = 0
|
||||
for col, col_vals in sorted(cols.items(), key=lambda x: x[1]['sequence']):
|
||||
cols[col]['pos'] = j
|
||||
cols[col]['pos_letter'] = chr(j + 97).upper()
|
||||
sheet.set_column(j, j, cols[col]['width'])
|
||||
j += 1
|
||||
|
||||
# HEADER
|
||||
now_dt = fields.Datetime.context_timestamp(self, datetime.now())
|
||||
now_str = fields.Datetime.to_string(now_dt)
|
||||
start_time_utc_str = self.start_date
|
||||
start_time_utc_dt = fields.Datetime.from_string(start_time_utc_str)
|
||||
start_time_dt = fields.Datetime.context_timestamp(self, start_time_utc_dt)
|
||||
start_time_str = fields.Datetime.to_string(start_time_dt)
|
||||
if self.end_date_type == 'past':
|
||||
end_time_utc_str = self.end_date
|
||||
end_time_utc_dt = fields.Datetime.from_string(end_time_utc_str)
|
||||
end_time_dt = fields.Datetime.context_timestamp(self, end_time_utc_dt)
|
||||
end_time_str = fields.Datetime.to_string(end_time_dt)
|
||||
else:
|
||||
end_time_str = now_str
|
||||
if standard_price_start_date:
|
||||
standard_price_start_date_str = start_time_str
|
||||
else:
|
||||
standard_price_start_date_str = now_str
|
||||
if standard_price_end_date:
|
||||
standard_price_end_date_str = end_time_str
|
||||
else:
|
||||
standard_price_end_date_str = now_str
|
||||
i = 0
|
||||
sheet.write(i, 0, 'Odoo - Stock Valuation Variation', styles['doc_title'])
|
||||
sheet.set_row(0, 26)
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Start Date: %s' % start_time_str, styles['doc_subtitle'])
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Cost Price Start Date: %s' % standard_price_start_date_str, styles['doc_subtitle'])
|
||||
i += 1
|
||||
sheet.write(i, 0, 'End Date: %s' % end_time_str, styles['doc_subtitle'])
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Cost Price End Date: %s' % standard_price_end_date_str, styles['doc_subtitle'])
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Stock location (children included): %s' % self.location_id.complete_name, styles['doc_subtitle'])
|
||||
if self.categ_ids:
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Product Categories: %s' % ', '.join([categ.display_name for categ in self.categ_ids]), styles['doc_subtitle'])
|
||||
i += 1
|
||||
sheet.write(i, 0, 'Generated on %s by %s' % (now_str, self.env.user.name), styles['regular_small'])
|
||||
|
||||
# TITLE of COLS
|
||||
i += 2
|
||||
for col in cols.values():
|
||||
sheet.write(i, col['pos'], col['title'], styles['col_title'])
|
||||
|
||||
i += 1
|
||||
sheet.write(i, 0, _("TOTALS:"), styles['total_title'])
|
||||
total_row = i
|
||||
|
||||
# LINES
|
||||
if categ_subtotal:
|
||||
categ_ids = categ_id2name.keys()
|
||||
else:
|
||||
categ_ids = [0]
|
||||
|
||||
start_total = end_total = variation_total = 0.0
|
||||
letter_start_qty = cols['start_qty']['pos_letter']
|
||||
letter_in_qty = cols['in_qty']['pos_letter']
|
||||
letter_out_qty = cols['out_qty']['pos_letter']
|
||||
letter_end_qty = cols['end_qty']['pos_letter']
|
||||
letter_start_price = cols['start_standard_price']['pos_letter']
|
||||
letter_end_price = cols['end_standard_price']['pos_letter']
|
||||
letter_start_subtotal = cols['start_subtotal']['pos_letter']
|
||||
letter_end_subtotal = cols['end_subtotal']['pos_letter']
|
||||
letter_variation = cols['variation']['pos_letter']
|
||||
crow = 0
|
||||
lines = res
|
||||
for categ_id in categ_ids:
|
||||
ctotal = 0.0
|
||||
categ_has_line = False
|
||||
if categ_subtotal:
|
||||
# skip a line and save it's position as crow
|
||||
i += 1
|
||||
crow = i
|
||||
lines = filter(lambda x: x['categ_id'] == categ_id, res)
|
||||
for l in lines:
|
||||
i += 1
|
||||
start_total += l['start_subtotal']
|
||||
end_total += l['end_subtotal']
|
||||
variation_total += l['variation']
|
||||
ctotal += l['variation']
|
||||
categ_has_line = True
|
||||
end_qty_formula = '=%s%d+%s%d-%s%d' % (letter_start_qty, i + 1, letter_in_qty, i + 1, letter_out_qty, i + 1)
|
||||
sheet.write_formula(i, cols['end_qty']['pos'], end_qty_formula, styles[cols['end_qty']['style']], l['end_qty'])
|
||||
start_subtotal_formula = '=%s%d*%s%d' % (letter_start_qty, i + 1, letter_start_price, i + 1)
|
||||
sheet.write_formula(i, cols['start_subtotal']['pos'], start_subtotal_formula, styles[cols['start_subtotal']['style']], l['start_subtotal'])
|
||||
end_subtotal_formula = '=%s%d*%s%d' % (letter_end_qty, i + 1, letter_end_price, i + 1)
|
||||
sheet.write_formula(i, cols['end_subtotal']['pos'], end_subtotal_formula, styles[cols['end_subtotal']['style']], l['end_subtotal'])
|
||||
variation_formula = '=%s%d-%s%d' % (letter_end_subtotal, i + 1, letter_start_subtotal, i + 1)
|
||||
sheet.write_formula(i, cols['variation']['pos'], variation_formula, styles[cols['variation']['style']], l['variation'])
|
||||
sheet.write_formula(i, cols['end_subtotal']['pos'], end_subtotal_formula, styles[cols['end_subtotal']['style']], l['end_subtotal'])
|
||||
for col_name, col in cols.items():
|
||||
if not col.get('formula'):
|
||||
if col.get('type') == 'date' and l[col_name]:
|
||||
l[col_name] = fields.Date.from_string(l[col_name])
|
||||
sheet.write(i, col['pos'], l[col_name], styles[col['style']])
|
||||
if categ_subtotal:
|
||||
if categ_has_line:
|
||||
sheet.write(crow, 0, categ_id2name[categ_id], styles['categ_title'])
|
||||
for x in range(cols['categ_subtotal']['pos'] - 1):
|
||||
sheet.write(crow, x + 1, '', styles['categ_title'])
|
||||
|
||||
cformula = '=SUM(%s%d:%s%d)' % (letter_variation, crow + 2, letter_variation, i + 1)
|
||||
sheet.write_formula(crow, cols['categ_subtotal']['pos'], cformula, styles['categ_currency'], float_round(ctotal, precision_rounding=prec_cur_rounding))
|
||||
else:
|
||||
i -= 1 # go back to skipped line
|
||||
|
||||
# Write total
|
||||
start_total_formula = '=SUM(%s%d:%s%d)' % (letter_start_subtotal, total_row + 2, letter_start_subtotal, i + 1)
|
||||
sheet.write_formula(total_row, cols['start_subtotal']['pos'], start_total_formula, styles['total_currency'], float_round(start_total, precision_rounding=prec_cur_rounding))
|
||||
end_total_formula = '=SUM(%s%d:%s%d)' % (letter_end_subtotal, total_row + 2, letter_end_subtotal, i + 1)
|
||||
sheet.write_formula(total_row, cols['end_subtotal']['pos'], end_total_formula, styles['total_currency'], float_round(end_total, precision_rounding=prec_cur_rounding))
|
||||
variation_total_formula = '=SUM(%s%d:%s%d)' % (letter_variation, total_row + 2, letter_variation, i + 1)
|
||||
sheet.write_formula(total_row, cols['variation']['pos'], variation_total_formula, styles['total_currency'], float_round(variation_total, precision_rounding=prec_cur_rounding))
|
||||
|
||||
workbook.close()
|
||||
logger.debug('End create XLSX workbook')
|
||||
file_data.seek(0)
|
||||
filename = 'Odoo_stock_%s_%s.xlsx' % (
|
||||
start_time_str.replace(' ', '-').replace(':', '_'),
|
||||
end_time_str.replace(' ', '-').replace(':', '_'))
|
||||
export_file_b64 = file_data.read().encode('base64')
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'export_filename': filename,
|
||||
'export_file': export_file_b64,
|
||||
})
|
||||
action = self.env['ir.actions.act_window'].for_xml_id(
|
||||
'stock_valuation_xlsx', 'stock_variation_xlsx_action')
|
||||
action.update({
|
||||
'res_id': self.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def _prepare_cols(self):
|
||||
cols = {
|
||||
'default_code': {'width': 18, 'style': 'regular', 'sequence': 10, 'title': _('Product Code')},
|
||||
'product_name': {'width': 40, 'style': 'regular', 'sequence': 20, 'title': _('Product Name')},
|
||||
'uom_name': {'width': 5, 'style': 'regular_small', 'sequence': 30, 'title': _('UoM')},
|
||||
'start_qty': {'width': 8, 'style': 'regular', 'sequence': 40, 'title': _('Start Qty')},
|
||||
'start_standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 50, 'title': _('Start Cost Price')},
|
||||
'start_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 60, 'title': _('Start Value'), 'formula': True},
|
||||
'in_qty': {'width': 8, 'style': 'regular', 'sequence': 70, 'title': _('In Qty')},
|
||||
'out_qty': {'width': 8, 'style': 'regular', 'sequence': 80, 'title': _('Out Qty')},
|
||||
'end_qty': {'width': 8, 'style': 'regular', 'sequence': 90, 'title': _('End Qty'), 'formula': True},
|
||||
'end_standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 100, 'title': _('End Cost Price')},
|
||||
'end_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 110, 'title': _('End Value'), 'formula': True},
|
||||
'variation': {'width': 16, 'style': 'regular_currency', 'sequence': 120, 'title': _('Variation'), 'formula': True},
|
||||
'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 130, 'title': _('Categ Sub-total'), 'formula': True},
|
||||
'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 140, 'title': _('Product Category')},
|
||||
}
|
||||
return cols
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="stock_variation_xlsx_form" model="ir.ui.view">
|
||||
<field name="name">stock.variation.xlsx.form</field>
|
||||
<field name="model">stock.variation.xlsx</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Stock variation XLSX">
|
||||
<div name="help">
|
||||
<p>The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens).</p>
|
||||
</div>
|
||||
<group name="setup">
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="categ_ids" widget="many2many_tags"/>
|
||||
<field name="warehouse_id"/>
|
||||
<field name="location_id"/>
|
||||
<field name="categ_subtotal" />
|
||||
</group>
|
||||
<group name="start_end">
|
||||
<group name="start" string="Start">
|
||||
<field name="start_date"/>
|
||||
<field name="standard_price_start_date_type"/>
|
||||
</group>
|
||||
<group name="end" string="End">
|
||||
<field name="end_date_type"/>
|
||||
<field name="end_date" attrs="{'invisible': [('end_date_type', '!=', 'past')], 'required': [('end_date_type', '=', 'past')]}"/>
|
||||
<field name="standard_price_end_date_type"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="done" states="done" string="Result">
|
||||
<field name="export_file" filename="export_filename"/>
|
||||
<field name="export_filename" invisible="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="generate" type="object" states="setup"
|
||||
class="btn-primary" string="Generate"/>
|
||||
<button special="cancel" string="Cancel" class="btn-default" states="setup"/>
|
||||
<button special="cancel" string="Close" class="btn-default" states="done"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_variation_xlsx_action" model="ir.actions.act_window">
|
||||
<field name="name">Stock Variation XLSX</field>
|
||||
<field name="res_model">stock.variation.xlsx</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<!-- Replace native menu, to avoid user confusion -->
|
||||
<menuitem id="stock_variation_xlsx_menu" action="stock_variation_xlsx_action" parent="stock.menu_warehouse_report" sequence="1"/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user