Compare commits
1 Commits
14.0-po-li
...
14.0-produ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45500f5bd8 |
@@ -31,7 +31,6 @@
|
||||
'views/account_report.xml',
|
||||
'wizard/account_invoice_mark_sent_view.xml',
|
||||
'wizard/account_group_generate_view.xml',
|
||||
'wizard/account_payment_register_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -11,6 +11,8 @@ from odoo.osv import expression
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
default_move_line_name = fields.Char(
|
||||
string='Default Label', states={'posted': [('readonly', True)]})
|
||||
# By default, we can still modify "ref" when account move is posted
|
||||
# which seems a bit lazy for me...
|
||||
ref = fields.Char(states={'posted': [('readonly', True)]})
|
||||
@@ -178,35 +180,6 @@ class AccountMove(models.Model):
|
||||
])
|
||||
move.suitable_journal_ids = self.env['account.journal'].search(domain)
|
||||
|
||||
def button_draft(self):
|
||||
super().button_draft()
|
||||
# Delete attached pdf invoice
|
||||
try:
|
||||
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice')
|
||||
except IndexError:
|
||||
report_invoice = False
|
||||
if report_invoice and report_invoice.attachment:
|
||||
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
|
||||
# The pb is that the filename is dynamic and related to move.state
|
||||
# in v12, the feature was native and they used that kind of code:
|
||||
# with invoice.env.do_in_draft():
|
||||
# invoice.number, invoice.state = invoice.move_name, 'open'
|
||||
# attachment = self.env.ref('account.account_invoices').retrieve_attachment(invoice)
|
||||
# But do_in_draft() doesn't exists in v14
|
||||
# If you know how we could do that, please update the code below
|
||||
attachment = self.env['ir.attachment'].search([
|
||||
('name', '=', self._get_invoice_attachment_name()),
|
||||
('res_id', '=', move.id),
|
||||
('res_model', '=', self._name),
|
||||
('type', '=', 'binary'),
|
||||
], limit=1)
|
||||
if attachment:
|
||||
attachment.unlink()
|
||||
|
||||
def _get_invoice_attachment_name(self):
|
||||
self.ensure_one()
|
||||
return '%s.pdf' % (self.name and self.name.replace('/', '_') or 'INV')
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
<field name="invoice_incoterm_id" position="attributes">
|
||||
<attribute name="widget">selection</attribute>
|
||||
</field>
|
||||
<button name="action_register_payment" position="attributes">
|
||||
<attribute name="class">btn-default</attribute>
|
||||
</button>
|
||||
<button name="action_register_payment" position="before">
|
||||
<button name="%(account.account_invoices)d" type="action" string="Print" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
</button>
|
||||
@@ -29,9 +26,6 @@
|
||||
</button>
|
||||
<!-- move sent field and make it visible -->
|
||||
<field name="is_move_sent" position="replace"/>
|
||||
<field name="invoice_origin" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</field>
|
||||
<field name="invoice_origin" position="after">
|
||||
<field name="is_move_sent" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
</field>
|
||||
|
||||
@@ -27,7 +27,7 @@ Here, we set all those fields on account.group_account_invoice
|
||||
</field>
|
||||
<field name="list_price" position="replace">
|
||||
<div name="list_price">
|
||||
<field name="list_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" class="oe_inline"/>
|
||||
<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>
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
<field name="property_account_position_id" position="attributes">
|
||||
<attribute name="widget">selection</attribute>
|
||||
</field>
|
||||
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_number']" position="after">
|
||||
<field name="acc_type"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -1,23 +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>
|
||||
|
||||
<!-- When you change the date, it resets the amount via the onchange
|
||||
So, in the view, the date should be BEFORE the amount -->
|
||||
<record id="view_account_payment_register_form" model="ir.ui.view">
|
||||
<field name="model">account.payment.register</field>
|
||||
<field name="inherit_id" ref="account.view_account_payment_register_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<label for="amount" position="before">
|
||||
<field name="payment_date" position="move"/>
|
||||
</label>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1,14 +1,14 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * base_partner_one2many_phone
|
||||
# * base_partner_one2many_phone
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 21:12+0000\n"
|
||||
"PO-Revision-Date: 2021-10-29 21:12+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"POT-Creation-Date: 2020-01-27 18:03+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 18:03+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -16,120 +16,92 @@ msgstr ""
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__create_uid
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__create_date
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__display_name
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__display_name
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__email
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_email
|
||||
msgid "E-Mail"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:61
|
||||
#, python-format
|
||||
msgid ""
|
||||
"E-mail field must be empty when type is Primary/Secondary Phone, "
|
||||
"Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgid "E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:51
|
||||
#, python-format
|
||||
msgid ""
|
||||
"E-mail field must have a value when type is Primary E-mail or Secondary "
|
||||
"E-mail."
|
||||
msgid "E-mail field must have a value when type is Primary E-mail or Secondary E-mail."
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__email
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner____last_update
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone____last_update
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone___last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__write_uid
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__write_date
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__mobile
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__mobile
|
||||
msgid "Mobile"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
|
||||
msgid "Multiple emails and phones for partners"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__note
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_note
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
|
||||
msgid "Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_phone
|
||||
msgid "Phone"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_form
|
||||
msgid "Phone and E-mail"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:54
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Phone field must be empty when type is Primary E-mail or Secondary E-mail."
|
||||
msgid "Phone field must be empty when type is Primary E-mail or Secondary E-mail."
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:58
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Phone field must have a value when type is Primary/Secondary Phone, "
|
||||
"Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgid "Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_ids
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users_phone_ids
|
||||
msgid "Phones"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
|
||||
msgid "Phones and E-mail"
|
||||
msgstr ""
|
||||
|
||||
@@ -140,63 +112,63 @@ msgid "Phones/E-mails"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__phone_ids
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__phone_ids
|
||||
msgid "Phones/Emails"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__1_email_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary E-mail"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__7_fax_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Fax"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__5_mobile_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Mobile"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__3_phone_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Phone"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__partner_id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_partner_id
|
||||
msgid "Related Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
msgid "Search Phones/E-mail"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__2_email_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary E-mail"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__8_fax_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Fax"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__6_mobile_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Mobile"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__4_phone_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Phone"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__type
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_type
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
|
||||
msgid "res.partner.phone"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * base_partner_one2many_phone
|
||||
# * base_partner_one2many_phone
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-29 21:12+0000\n"
|
||||
"PO-Revision-Date: 2021-10-29 21:12+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"POT-Creation-Date: 2020-01-27 17:56+0000\n"
|
||||
"PO-Revision-Date: 2020-01-27 17:56+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -16,187 +16,159 @@ msgstr ""
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr "Contact"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__create_uid
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__create_date
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_date
|
||||
msgid "Created on"
|
||||
msgstr "Créé le"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__display_name
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__display_name
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
msgstr "Nom à afficher"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__email
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_email
|
||||
msgid "E-Mail"
|
||||
msgstr "E-Mail"
|
||||
msgstr "Courriel"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:61
|
||||
#, python-format
|
||||
msgid ""
|
||||
"E-mail field must be empty when type is Primary/Secondary Phone, "
|
||||
"Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr "Le champ E-mail doit être vide quand le type est Tél. principal/secondaire, Portable principal/secondaire ou Fax principal/secondaire."
|
||||
msgid "E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr "Le champ courriel doit être vide quand le type est tél. primaire/secondaire, portable primaire/secondaire ou fax primaire/secondaire."
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:51
|
||||
#, python-format
|
||||
msgid ""
|
||||
"E-mail field must have a value when type is Primary E-mail or Secondary "
|
||||
"E-mail."
|
||||
msgstr "Le champ E-mail doit avoir une valeur quand le type est E-mail principal ou secondaire."
|
||||
msgid "E-mail field must have a value when type is Primary E-mail or Secondary E-mail."
|
||||
msgstr "Le champ courriel doit être renseigné quand le type est courriel primaire ou courriel secondaire."
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__email
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__email
|
||||
msgid "Email"
|
||||
msgstr "E-mail"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
msgstr "ID"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner____last_update
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone____last_update
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone___last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__write_uid
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Dernière modification par"
|
||||
msgstr "Dernière mise à jour par"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__write_date
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Dernière modification le"
|
||||
msgstr "Dernière mise à jour le"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__mobile
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__mobile
|
||||
msgid "Mobile"
|
||||
msgstr "Portable"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
|
||||
msgid "Multiple emails and phones for partners"
|
||||
msgstr "Multiples e-mails et téléphones pour les partenaires"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__note
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_note
|
||||
msgid "Note"
|
||||
msgstr "Note"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
|
||||
msgid "Partner"
|
||||
msgstr "Partenaire"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_phone
|
||||
msgid "Phone"
|
||||
msgstr "Tél."
|
||||
msgstr "Téléphone"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_form
|
||||
msgid "Phone and E-mail"
|
||||
msgstr "Tél. et E-mail"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:54
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Phone field must be empty when type is Primary E-mail or Secondary E-mail."
|
||||
msgstr "Le champ Tél. doit être vide quand le type est E-mail principal ou E-mail secondaire."
|
||||
msgid "Phone field must be empty when type is Primary E-mail or Secondary E-mail."
|
||||
msgstr "Le champ téléphone doit être vide quand le type est courriel primaire ou courriel secondaire."
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:0
|
||||
#: code:addons/base_partner_one2many_phone/partner_phone.py:58
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Phone field must have a value when type is Primary/Secondary Phone, "
|
||||
"Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr "Le champ Tél. doit avoir une valeur quand le type est Tél. principal/secondaire, Portable principal/secondaire ou Fax principal/secondaire."
|
||||
msgid "Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
|
||||
msgstr "Le champ téléphone doit être renseigné quand le type est tél. primaire/secondaire, portable primaire/secondaire ou fax primaire/secondaire.."
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_ids
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users_phone_ids
|
||||
msgid "Phones"
|
||||
msgstr "Téléphones"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
|
||||
msgid "Phones and E-mail"
|
||||
msgstr "Téls et E-mail"
|
||||
msgstr "Téls et courriels"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.actions.act_window,name:base_partner_one2many_phone.res_partner_phone_action
|
||||
#: model:ir.ui.menu,name:base_partner_one2many_phone.res_partner_phone_menu
|
||||
msgid "Phones/E-mails"
|
||||
msgstr "Téls/E-mails"
|
||||
msgstr "Téls/Courriels"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner__phone_ids
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users__phone_ids
|
||||
msgid "Phones/Emails"
|
||||
msgstr "Téls/E-mails"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__1_email_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary E-mail"
|
||||
msgstr "E-mail principal"
|
||||
msgstr "Courriel principal"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__7_fax_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Fax"
|
||||
msgstr "Fax principal"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__5_mobile_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Mobile"
|
||||
msgstr "Portable principal"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__3_phone_primary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Primary Phone"
|
||||
msgstr "Tél. principal"
|
||||
msgstr "Tél principal"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__partner_id
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_partner_id
|
||||
msgid "Related Partner"
|
||||
msgstr "Partenaire associé"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
msgid "Search Phones/E-mail"
|
||||
msgstr ""
|
||||
msgstr "Search Phones/E-mail"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__2_email_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary E-mail"
|
||||
msgstr "E-mail secondaire"
|
||||
msgstr "Courriel secondaire"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__8_fax_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Fax"
|
||||
msgstr "Fax secondaire"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__6_mobile_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Mobile"
|
||||
msgstr "Portable secondaire"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields.selection,name:base_partner_one2many_phone.selection__res_partner_phone__type__4_phone_secondary
|
||||
#: selection:res.partner.phone,type:0
|
||||
msgid "Secondary Phone"
|
||||
msgstr "Tél. secondaire"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone__type
|
||||
#: model_terms:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_type
|
||||
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#. module: base_partner_one2many_phone
|
||||
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
|
||||
msgid "res.partner.phone"
|
||||
msgstr "res.partner.phone"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<field name="name">res.partner.phone.tree</field>
|
||||
<field name="model">res.partner.phone</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<tree string="Phones and E-mail" editable="bottom">
|
||||
<field name="partner_id" invisible="not context.get('partner_phone_main_view')"/>
|
||||
<field name="type"/>
|
||||
<field name="phone" widget="phone" options="{'enable_sms': false}" attrs="{'required': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'readonly': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
|
||||
@@ -28,7 +28,7 @@
|
||||
<field name="name">res.partner.phone.form</field>
|
||||
<field name="model">res.partner.phone</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<form string="Phone and E-mail">
|
||||
<group name="main">
|
||||
<field name="partner_id" invisible="not context.get('partner_phone_main_view')"/>
|
||||
<field name="type"/>
|
||||
@@ -44,7 +44,7 @@
|
||||
<field name="name">res.partner.phone.search</field>
|
||||
<field name="model">res.partner.phone</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<search string="Search Phones/E-mail">
|
||||
<field name="phone" />
|
||||
<field name="email" />
|
||||
<group name="groupby">
|
||||
|
||||
@@ -1 +1 @@
|
||||
from . import models
|
||||
from . import partner
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright 2017-2021 Akretion (http://www.akretion.com)
|
||||
# Copyright 2017-2019 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Base Partner Reference',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Partner',
|
||||
'license': 'AGPL-3',
|
||||
'summary': "Improve usage of partner's Internal Reference",
|
||||
@@ -21,6 +21,6 @@ Base Partner Reference
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['base'],
|
||||
'data': ['views/res_partner.xml'],
|
||||
'installable': True,
|
||||
'data': ['partner_view.xml'],
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from . import res_partner
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2017-2021 Akretion
|
||||
# Copyright 2017-2019 Akretion
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
@@ -18,9 +18,9 @@ class ResPartner(models.Model):
|
||||
)]
|
||||
|
||||
# add 'ref' in depends
|
||||
@api.depends('ref', 'invalidate_display_name')
|
||||
@api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name', 'ref', 'invalidate_display_name')
|
||||
def _compute_display_name(self):
|
||||
super()._compute_display_name()
|
||||
super(ResPartner, self)._compute_display_name()
|
||||
|
||||
def _get_name(self):
|
||||
partner = self
|
||||
@@ -32,13 +32,12 @@ class ResPartner(models.Model):
|
||||
# END modif of native method
|
||||
if partner.company_name or partner.parent_id:
|
||||
if not name and partner.type in ['invoice', 'delivery', 'other']:
|
||||
name = dict(self.fields_get(
|
||||
['type'])['type']['selection'])[partner.type]
|
||||
name = dict(self.fields_get(['type'])['type']['selection'])[partner.type]
|
||||
if not partner.is_company:
|
||||
# START modif of native name_get() method
|
||||
company_name = partner.commercial_company_name or partner.parent_id.name
|
||||
if partner.parent_id.ref:
|
||||
company_name = "[%s] %s" % (partner.parent_id.ref, company_name)
|
||||
company_name = u"[%s] %s" % (partner.parent_id.ref, company_name)
|
||||
name = "%s, %s" % (company_name, name)
|
||||
# END modif of native name_get() method
|
||||
if self._context.get('show_address_only'):
|
||||
@@ -48,8 +47,7 @@ class ResPartner(models.Model):
|
||||
name = name.replace('\n\n', '\n')
|
||||
name = name.replace('\n\n', '\n')
|
||||
if self._context.get('address_inline'):
|
||||
splitted_names = name.split("\n")
|
||||
name = ", ".join([n for n in splitted_names if n.strip()])
|
||||
name = name.replace('\n', ', ')
|
||||
if self._context.get('show_email') and partner.email:
|
||||
name = "%s <%s>" % (name, partner.email)
|
||||
if self._context.get('html_format'):
|
||||
@@ -65,6 +63,5 @@ class ResPartner(models.Model):
|
||||
if name and operator == 'ilike':
|
||||
recs = self.search([('ref', '=', name)] + args, limit=limit)
|
||||
if recs:
|
||||
rec_childs = self.search([('id', 'child_of', recs.ids)])
|
||||
return rec_childs.name_get()
|
||||
return recs.name_get()
|
||||
return super().name_search(name=name, args=args, operator=operator, limit=limit)
|
||||
@@ -11,34 +11,29 @@
|
||||
<field name="name">Move ref in partner form to make it more visible</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority">1000</field> <!-- inherit after l10n_fr -->
|
||||
<field name="arch" type="xml">
|
||||
<field name="type" position="after">
|
||||
<field name="ref"/>
|
||||
</field>
|
||||
<xpath expr="//page[@name='sales_purchases']//field[@name='ref']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='sales_purchases']//field[@name='ref']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- show name and ref in separate columns -->
|
||||
<!-- ref is added in tree view by base_usability with optional="hide"
|
||||
<record id="view_partner_tree" model="ir.ui.view">
|
||||
<field name="name">Add ref in partner tree view</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- show name and ref in separate columns -->
|
||||
<field name="display_name" position="after">
|
||||
<field name="name"/>
|
||||
<field name="ref" optional="hide"/>
|
||||
<field name="ref"/>
|
||||
</field>
|
||||
<field name="display_name" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record id="res_partner_kanban_view" model="ir.ui.view">
|
||||
<field name="name">Add ref in partner kanban view</field>
|
||||
@@ -39,9 +39,6 @@
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="display_name" position="after">
|
||||
<field name="ref" optional="hide"/>
|
||||
</field>
|
||||
<field name="phone" position="after">
|
||||
<field name="mobile" optional="show" widget="phone" class="o_force_ltr"/>
|
||||
</field>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copyright 2016-2021 Akretion (http://www.akretion.com)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
{
|
||||
'name': 'CRM Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Customer Relationship Management',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'CRM usability enhancements',
|
||||
'description': """
|
||||
CRM Usability
|
||||
=============
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['crm'],
|
||||
'data': [
|
||||
'views/crm_lead.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import crm_lead
|
||||
@@ -1,13 +0,0 @@
|
||||
# Copyright 2017-2021 Akretion (http://www.akretion.com)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CrmLead(models.Model):
|
||||
_inherit = 'crm.lead'
|
||||
|
||||
probability = fields.Float(tracking=100)
|
||||
date_deadline = fields.Date(tracking=110)
|
||||
name = fields.Char(tracking=1)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB |
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017-2021 Akretion (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<!-- SEARCH OPPOR -->
|
||||
<record id="view_crm_case_opportunities_filter" model="ir.ui.view">
|
||||
<field name="name">usability.crm.lead.opportunity.search</field>
|
||||
<field name="model">crm.lead</field>
|
||||
<field name="inherit_id" ref="crm.view_crm_case_opportunities_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="saleschannel" position="after">
|
||||
<filter name="partner_groupby" string="Customer" context="{'group_by': 'partner_id'}"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import stock_picking
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright 2021 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'HR Contract Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Human Resources/Contracts',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Usability improvements on HR Contract module',
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': [
|
||||
'hr_contract',
|
||||
],
|
||||
'data': [
|
||||
'views/hr_payroll_structure_type.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import hr_payroll_structure_type
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright 2021 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_inherit = 'hr.payroll.structure.type'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
@@ -1,63 +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="hr_payroll_structure_type_form" model="ir.ui.view">
|
||||
<field name="model">hr.payroll.structure.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<group name="main">
|
||||
<field name="name"/>
|
||||
<field name="default_resource_calendar_id"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="country_id"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_structure_type_tree" model="ir.ui.view">
|
||||
<field name="model">hr.payroll.structure.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="default_resource_calendar_id" optional="show"/>
|
||||
<field name="country_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_structure_type_search" model="ir.ui.view">
|
||||
<field name="model">hr.payroll.structure.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<group name="groupby">
|
||||
<filter name="country_groupby" string="Country" context="{'group_by': 'country_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_structure_type_action" model="ir.actions.act_window">
|
||||
<field name="name">Salary Structure Types</field>
|
||||
<field name="res_model">hr.payroll.structure.type</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="hr_payroll_structure_type_menu"
|
||||
action="hr_payroll_structure_type_action"
|
||||
parent="hr_contract.menu_human_resources_configuration_contract"
|
||||
sequence="10"/>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright 2019-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).
|
||||
|
||||
{
|
||||
'name': 'Link Tracker Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Marketing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Improve usability for link tracker',
|
||||
'description': """
|
||||
Link Tracker Usability
|
||||
======================
|
||||
|
||||
Several small usability improvements.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['link_tracker'],
|
||||
'data': [
|
||||
'views/link_tracker_click.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019-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="link_tracker_click_view_tree" model="ir.ui.view">
|
||||
<field name="name">usability.link.tracker.click.tree</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="country_id" position="after">
|
||||
<field name="create_date" string="Click Date"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="link_tracker_click_view_form" model="ir.ui.view">
|
||||
<field name="name">usability.link.tracker.click.form</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="country_id" position="after">
|
||||
<field name="create_date" string="Click Date"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="link_tracker_click_view_search" model="ir.ui.view">
|
||||
<field name="name">usability.link.tracker.click.search</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="groupby_link_id" position="before">
|
||||
<filter name="create_date_groupby" string="Click Date" context="{'group_by': 'create_date'}"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,29 +0,0 @@
|
||||
# Copyright 2019-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).
|
||||
|
||||
{
|
||||
'name': 'Mass Mailing Campaigns Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Marketing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Improve usability of mass mailing campaigns',
|
||||
'description': """
|
||||
Mass Mailing Campaigns Usability
|
||||
================================
|
||||
|
||||
Several small usability improvements on the module mass_mailing:
|
||||
|
||||
* show fields on link.tracker.click that are not displayed by default
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['mass_mailing', 'link_tracker_usability'],
|
||||
'data': [
|
||||
# 'views/link_tracker.xml',
|
||||
],
|
||||
'installable': False,
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019 Akretion (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_link_tracker_click_tree" model="ir.ui.view">
|
||||
<field name="name">mm.usability.link.tracker.click.tree</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker.view_link_tracker_click_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="country_id" position="after">
|
||||
<field name="mass_mailing_id"/>
|
||||
<field name="mail_stat_recipient"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_link_tracker_click_form" model="ir.ui.view">
|
||||
<field name="name">mm.usability.link.tracker.click.form</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker.view_link_tracker_click_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="country_id" position="after">
|
||||
<field name="mass_mailing_id"/>
|
||||
<field name="mass_mailing_campaign_id"/>
|
||||
<field name="mail_stat_id"/>
|
||||
<field name="mail_stat_recipient"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="link_tracker_click_search" model="ir.ui.view">
|
||||
<field name="name">mm.usability.link.tracker.click.search</field>
|
||||
<field name="model">link.tracker.click</field>
|
||||
<field name="inherit_id" ref="link_tracker_usability.link_tracker_click_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="link_id" position="after">
|
||||
<field name="mail_stat_recipient"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -12,20 +12,16 @@ class ProductTemplate(models.Model):
|
||||
only one BoM form or a list of BoMs."""
|
||||
self.ensure_one()
|
||||
if self.bom_count == 1:
|
||||
action_xml_id = "mrp.mrp_bom_form_action"
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(action_xml_id)
|
||||
action = self.env.ref("mrp.mrp_bom_form_action").read()[0]
|
||||
bom = self.env["mrp.bom"].search([("product_tmpl_id", "=", self.id)])
|
||||
action.update(
|
||||
{
|
||||
"context": {"default_product_tmpl_id": self.id},
|
||||
"views": False,
|
||||
"view_mode": "form,tree",
|
||||
"res_id": bom.id,
|
||||
}
|
||||
)
|
||||
action.update({
|
||||
"context": {"default_product_tmpl_id": self.id},
|
||||
"views": False,
|
||||
"view_mode": "form,tree",
|
||||
"res_id": bom.id,
|
||||
})
|
||||
else:
|
||||
action_xml_id = "mrp.template_open_bom"
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(action_xml_id)
|
||||
action = self.env.ref("mrp.template_open_bom").read()[0]
|
||||
return action
|
||||
|
||||
|
||||
@@ -36,11 +32,9 @@ class ProductProduct(models.Model):
|
||||
action = super().action_view_bom()
|
||||
bom_target_ids = self.env["mrp.bom"].search(action["domain"])
|
||||
if len(bom_target_ids) == 1:
|
||||
action.update(
|
||||
{
|
||||
"views": False,
|
||||
"view_mode": "form,tree",
|
||||
"res_id": bom_target_ids[0].id,
|
||||
}
|
||||
)
|
||||
action.update({
|
||||
"views": False,
|
||||
"view_mode": "form,tree",
|
||||
"res_id": bom_target_ids[0].id,
|
||||
})
|
||||
return action
|
||||
|
||||
28
pos_no_product_template_menu/__manifest__.py
Normal file
28
pos_no_product_template_menu/__manifest__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'POS No Product Template Menu',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Point of sale',
|
||||
'license': 'AGPL-3',
|
||||
'summary': "Replace product.template menu entries by product.product menu",
|
||||
'description': """
|
||||
POS No Product Template
|
||||
=======================
|
||||
|
||||
This module replaces the menu entry for product.template by menu entries
|
||||
for product.product in the *Point Of Sale > Product* menu.
|
||||
|
||||
This module also switches to the tree view by default
|
||||
for Product menu entries, instead of the kanban view.
|
||||
|
||||
This module has been written by David Béal
|
||||
from Akretion <david.beal@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['point_of_sale', 'sale_purchase_no_product_template_menu'],
|
||||
'auto_install': True,
|
||||
'data': ['pos_view.xml'],
|
||||
'installable': False,
|
||||
}
|
||||
16
pos_no_product_template_menu/pos_view.xml
Normal file
16
pos_no_product_template_menu/pos_view.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="product_product_action_pos" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{'default_available_in_pos': True, 'search_default_filter_to_availabe_pos': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="point_of_sale.menu_pos_products" model="ir.ui.menu">
|
||||
<field name="action" ref="product_product_action_pos"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -28,9 +28,7 @@ Akretion:
|
||||
"depends": ["point_of_sale"],
|
||||
"data": [
|
||||
"report/pos.xml",
|
||||
"views/report_pos_order.xml",
|
||||
"views/pos_category.xml",
|
||||
"views/pos_session.xml",
|
||||
"views/product.xml",
|
||||
],
|
||||
"installable": True,
|
||||
|
||||
@@ -1,31 +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_pos_session_form" model="ir.ui.view">
|
||||
<field name="model">pos.session</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_session_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="show_journal_items" position="after">
|
||||
<button name="%(point_of_sale.action_report_pos_order_all)d" type="action" class="oe_stat_button" icon="fa-table" string="Stats" context="{'search_default_session_id': active_id}"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_pos_session_tree" model="ir.ui.view">
|
||||
<field name="model">pos.session</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_session_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="state" position="attributes">
|
||||
<attribute name="decoration-success">state == 'opened'</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,26 +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_report_pos_order_search" model="ir.ui.view">
|
||||
<field name="model">report.pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_report_pos_order_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_categ_id" position="after">
|
||||
<field name="session_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="point_of_sale.action_report_pos_order_all" model="ir.actions.act_window">
|
||||
<field name="view_mode">pivot,graph</field> <!-- invert native order -->
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1,208 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * purchase_usability
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-30 13:35+0000\n"
|
||||
"PO-Revision-Date: 2021-11-30 13:35+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: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Analytic Account"
|
||||
msgstr "Compte Analytique"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form
|
||||
msgid "Are you sure you want to cancel this purchase order?"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__invoice_status
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter
|
||||
msgid "Billing Status"
|
||||
msgstr "État de facturation"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Bills Received"
|
||||
msgstr "Factures reçues"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_method
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_method
|
||||
msgid "Control Policy"
|
||||
msgstr "Politique de contrôle"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__currency_id
|
||||
msgid "Currency"
|
||||
msgstr "Devise"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__delivery_partner_id
|
||||
msgid "Delivery Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__dest_address_id
|
||||
msgid "Drop Ship Address"
|
||||
msgstr "Adresse de livraison directe"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__fiscal_position_id
|
||||
msgid "Fiscal Position"
|
||||
msgstr "Position fiscale"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__invoiced
|
||||
msgid "Fully Billed"
|
||||
msgstr "Complètement facturé"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order_line__product_barcode
|
||||
msgid "International Article Number used for product identification."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__no
|
||||
msgid "Nothing to Bill"
|
||||
msgstr "Rien à facturer"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_method
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_method
|
||||
msgid ""
|
||||
"On ordered quantities: Control bills based on ordered quantities.\n"
|
||||
"On received quantities: Control bills based on received quantities."
|
||||
msgstr ""
|
||||
"Sur base des quantités commandées: factures de controle basées sur les quantités commandées. \n"
|
||||
"Sur base des quantités reçues: factures de controle basées sur les quantités reçues."
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__payment_term_id
|
||||
msgid "Payment Terms"
|
||||
msgstr "Conditions de paiement"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form
|
||||
msgid "Print"
|
||||
msgstr "Imprimer"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__product_barcode
|
||||
msgid "Product Barcode"
|
||||
msgstr "Code-barre produit"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Modèle d'article"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_purchase_order
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__purchase_warn
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_users__purchase_warn
|
||||
msgid "Purchase Order"
|
||||
msgstr "Commande fournisseur"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_purchase_order_line
|
||||
msgid "Purchase Order Line"
|
||||
msgstr "Ligne de commande fournisseur"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_line_warn
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_line_warn
|
||||
msgid "Purchase Order Line Warning"
|
||||
msgstr "Avertissement Ligne de Commande "
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order__dest_address_id
|
||||
msgid ""
|
||||
"Put an address if you want to deliver directly from the vendor to the "
|
||||
"customer. Otherwise, keep empty to deliver to your own company."
|
||||
msgstr ""
|
||||
"Ajoutez une adresse si vous voulez livrer directement du fournisseur au "
|
||||
"client. Sinon, laissez vide pour vous faire livrer à votre société."
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order__partner_ref
|
||||
msgid ""
|
||||
"Reference of the sales order or bid sent by the vendor. It's used to do the "
|
||||
"matching when you receive the products as this reference is usually written "
|
||||
"on the delivery order sent by your vendor."
|
||||
msgstr ""
|
||||
"Référence de la commande client ou offre envoyée par le fournisseur. Utilisé"
|
||||
" principalement pour faire la correspondance lors de la réception des "
|
||||
"articles, puisque cette référence est généralement écrite sur le bon de "
|
||||
"livraison envoyé par votre fournisseur."
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter
|
||||
msgid "Reference, Origin or Vendor Reference"
|
||||
msgstr "Référence, Origine ou Référence fournisseur"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_line_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_line_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_res_partner__purchase_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_res_users__purchase_warn
|
||||
msgid ""
|
||||
"Selecting the \"Warning\" option will notify user with the message, "
|
||||
"Selecting \"Blocking Message\" will throw an exception with the message and "
|
||||
"block the flow. The Message has to be written in the next field."
|
||||
msgstr ""
|
||||
"Sélectionner l'option 'Avertissement' notifiera l'utilisateur avec le "
|
||||
"Message. Sélectionner 'Message Bloquant' lancera une exception avec le "
|
||||
"message et bloquera le flux. Le Message doit être encodé dans le champ "
|
||||
"suivant."
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__partner_ref
|
||||
msgid "Vendor Reference"
|
||||
msgstr "Référence fournisseur"
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__to_invoice
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Waiting Bills"
|
||||
msgstr "Factures en attente"
|
||||
@@ -1,196 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * purchase_usability
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-02 09:44+0000\n"
|
||||
"PO-Revision-Date: 2021-11-02 09:44+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: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Analytic Account"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form
|
||||
msgid "Are you sure you want to cancel this purchase order?"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__invoice_status
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter
|
||||
msgid "Billing Status"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Bills Received"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_method
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_method
|
||||
msgid "Control Policy"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__currency_id
|
||||
msgid "Currency"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__delivery_partner_id
|
||||
msgid "Delivery Partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__display_name
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__dest_address_id
|
||||
msgid "Drop Ship Address"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__fiscal_position_id
|
||||
msgid "Fiscal Position"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__invoiced
|
||||
msgid "Fully Billed"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__id
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order_line__product_barcode
|
||||
msgid "International Article Number used for product identification."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line____last_update
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__no
|
||||
msgid "Nothing to Bill"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_method
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_method
|
||||
msgid ""
|
||||
"On ordered quantities: Control bills based on ordered quantities.\n"
|
||||
"On received quantities: Control bills based on received quantities."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__payment_term_id
|
||||
msgid "Payment Terms"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form
|
||||
msgid "Print"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__product_barcode
|
||||
msgid "Product Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_purchase_order
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__purchase_warn
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_res_users__purchase_warn
|
||||
msgid "Purchase Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model,name:purchase_usability.model_purchase_order_line
|
||||
msgid "Purchase Order Line"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_line_warn
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_line_warn
|
||||
msgid "Purchase Order Line Warning"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order__dest_address_id
|
||||
msgid ""
|
||||
"Put an address if you want to deliver directly from the vendor to the "
|
||||
"customer. Otherwise, keep empty to deliver to your own company."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_purchase_order__partner_ref
|
||||
msgid ""
|
||||
"Reference of the sales order or bid sent by the vendor. It's used to do the "
|
||||
"matching when you receive the products as this reference is usually written "
|
||||
"on the delivery order sent by your vendor."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter
|
||||
msgid "Reference, Origin or Vendor Reference"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_line_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_line_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_res_partner__purchase_warn
|
||||
#: model:ir.model.fields,help:purchase_usability.field_res_users__purchase_warn
|
||||
msgid ""
|
||||
"Selecting the \"Warning\" option will notify user with the message, "
|
||||
"Selecting \"Blocking Message\" will throw an exception with the message and "
|
||||
"block the flow. The Message has to be written in the next field."
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__partner_ref
|
||||
msgid "Vendor Reference"
|
||||
msgstr ""
|
||||
|
||||
#. module: purchase_usability
|
||||
#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__to_invoice
|
||||
#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search
|
||||
msgid "Waiting Bills"
|
||||
msgstr ""
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
@@ -74,33 +73,3 @@ class PurchaseOrderLine(models.Model):
|
||||
|
||||
# for optional display in tree view
|
||||
product_barcode = fields.Char(related='product_id.barcode', string="Product Barcode")
|
||||
invoice_status = fields.Selection(
|
||||
[
|
||||
("no", "Nothing to Bill"),
|
||||
("to invoice", "Waiting Bills"),
|
||||
("invoiced", "Fully Billed"),
|
||||
],
|
||||
string="Billing Status",
|
||||
compute="_compute_invoice_status",
|
||||
store=True,
|
||||
readonly=True,
|
||||
default="no",
|
||||
)
|
||||
|
||||
@api.depends("state", "qty_to_invoice", "qty_invoiced")
|
||||
def _compute_invoice_status(self):
|
||||
"""Mimic PO '_get_invoiced' method to compute PO line invoice status"""
|
||||
prec = self.env["decimal.precision"].precision_get("Product Unit of Measure")
|
||||
for line in self:
|
||||
if line.state not in ("purchase", "done") or line.display_type:
|
||||
line.invoice_status = "no"
|
||||
continue
|
||||
|
||||
if not float_is_zero(line.qty_to_invoice, precision_digits=prec):
|
||||
line.invoice_status = "to invoice"
|
||||
elif float_is_zero(
|
||||
line.qty_to_invoice, precision_digits=prec
|
||||
) and not float_is_zero(line.qty_invoiced, precision_digits=prec):
|
||||
line.invoice_status = "invoiced"
|
||||
else:
|
||||
line.invoice_status = "no"
|
||||
|
||||
@@ -131,10 +131,7 @@
|
||||
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
|
||||
</field>
|
||||
<field name="date_planned" position="after">
|
||||
<field name="state" decoration-success="state == 'purchase' or state == 'done'" decoration-warning="state == 'to approve'"
|
||||
decoration-info="state == 'draft' or state == 'sent'" optional="show" widget="badge" />
|
||||
<field name="invoice_status" decoration-success="invoice_status == 'invoiced'" decoration-info="invoice_status == 'to invoice'"
|
||||
optional="show" widget="badge" />
|
||||
<field name="state"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
@@ -147,13 +144,7 @@
|
||||
<field name="partner_id" position="after">
|
||||
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
|
||||
</field>
|
||||
<xpath expr="//filter[@name='hide_cancelled']" position="after">
|
||||
<separator/>
|
||||
<filter name="not_invoiced" string="Waiting Bills" domain="[('invoice_status', '=', 'to invoice')]" />
|
||||
<filter name="invoiced" string="Bills Received" domain="[('invoice_status', '=', 'invoiced')]" />
|
||||
</xpath>
|
||||
<group expand="0" position="inside">
|
||||
<filter string="Billing Status" name="invoice_status" context="{'group_by' : 'invoice_status'}" />
|
||||
<filter string="Analytic Account" name="account_analytic_groupby" context="{'group_by': 'account_analytic_id'}" groups="analytic.group_analytic_accounting"/>
|
||||
</group>
|
||||
</field>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -1,38 +0,0 @@
|
||||
# Copyright 2017-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).
|
||||
|
||||
{
|
||||
'name': 'Sale Confirm Wizard',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Sales',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Open a wizard when you confirm a sale order to update important info',
|
||||
'description': """
|
||||
Sale Confirm Wizard
|
||||
===================
|
||||
|
||||
When you confirm a quotation, Odoo will open a small wizard where you will be able to check and update important information:
|
||||
|
||||
* customer PO number,
|
||||
* delivery address,
|
||||
* 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
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sale'],
|
||||
'data': [
|
||||
'wizard/sale_confirm_view.xml',
|
||||
'views/sale_order.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import sale_order
|
||||
@@ -1,17 +0,0 @@
|
||||
# Copyright 2020-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 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.sudo().env.ref(
|
||||
'sale_confirm_wizard.sale_confirm_action').read()[0]
|
||||
return action
|
||||
@@ -1,2 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_confirm_wizard,Full access on sale.confirm wizard,model_sale_confirm,sales_team.group_sale_salesman,1,1,1,1
|
||||
|
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017-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_order_form" model="ir.ui.view">
|
||||
<field name="name">sale.confirm.wizard.sale_order_form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button id="action_confirm" position="attributes">
|
||||
<attribute name="name">sale_confirm_wizard_button</attribute>
|
||||
</button>
|
||||
<button name="action_confirm" attrs="{'invisible': [('state', 'not in', ['draft'])]}" position="attributes">
|
||||
<attribute name="name">sale_confirm_wizard_button</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import sale_confirm
|
||||
@@ -1,78 +0,0 @@
|
||||
# Copyright 2017-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, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.base.models.res_partner import WARNING_MESSAGE
|
||||
|
||||
|
||||
class SaleConfirm(models.TransientModel):
|
||||
_name = 'sale.confirm'
|
||||
_description = 'Wizard to confirm a sale order'
|
||||
|
||||
sale_id = fields.Many2one(
|
||||
'sale.order', string='Sale Order', readonly=True)
|
||||
client_order_ref = fields.Char(string='Customer PO Number')
|
||||
payment_term_id = fields.Many2one(
|
||||
'account.payment.term', string='Payment Terms')
|
||||
partner_invoice_id = fields.Many2one(
|
||||
'res.partner', 'Invoice Address', required=True)
|
||||
show_partner_invoice_id = fields.Many2one(
|
||||
related='partner_invoice_id',
|
||||
string='Detailed Invoice Address')
|
||||
partner_shipping_id = fields.Many2one(
|
||||
'res.partner', 'Delivery Address', required=True)
|
||||
show_partner_shipping_id = fields.Many2one(
|
||||
related='partner_shipping_id',
|
||||
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
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
assert self._context.get('active_model') == 'sale.order',\
|
||||
'active_model should be sale.order'
|
||||
order = self.env['sale.order'].browse(self._context.get('active_id'))
|
||||
default = self._prepare_default_get(order)
|
||||
res.update(default)
|
||||
return res
|
||||
|
||||
def _prepare_update_so(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'client_order_ref': self.client_order_ref,
|
||||
'payment_term_id': self.payment_term_id.id or False,
|
||||
'partner_invoice_id': self.partner_invoice_id.id,
|
||||
'partner_shipping_id': self.partner_shipping_id.id,
|
||||
}
|
||||
|
||||
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
|
||||
self.sale_id.action_confirm()
|
||||
return True
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017-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="sale_confirm_form" model="ir.ui.view">
|
||||
<field name="name">sale.confirm.form</field>
|
||||
<field name="model">sale.confirm</field>
|
||||
<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"/>
|
||||
<field name="client_order_ref"/>
|
||||
<field name="partner_invoice_id" context="{'default_type': 'invoice'}"
|
||||
groups="sale.group_delivery_invoice_address"/>
|
||||
<!-- partner_invoice_id can't show the full address because
|
||||
we are in edit mode -->
|
||||
<field name="show_partner_invoice_id" options="{'always_reload': True}"
|
||||
context="{'show_address': 1}"
|
||||
groups="sale.group_delivery_invoice_address"/>
|
||||
<field name="partner_shipping_id"
|
||||
context="{'default_type': 'delivery'}"
|
||||
groups="sale.group_delivery_invoice_address"/>
|
||||
<field name="show_partner_shipping_id" options="{'always_reload': True}"
|
||||
context="{'show_address': 1}"
|
||||
groups="sale.group_delivery_invoice_address"/>
|
||||
<field name="payment_term_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button type="object" name="confirm"
|
||||
string="Confirm Sale" class="btn-primary" attrs="{'invisible': [('sale_warn', '=', 'block')]}"/>
|
||||
<button special="cancel" string="Annuler" class="btn-default"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_confirm_action" model="ir.actions.act_window">
|
||||
<field name="name">Confirm Order</field>
|
||||
<field name="res_model">sale.confirm</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
0
sale_purchase_no_product_template_menu/__init__.py
Normal file
0
sale_purchase_no_product_template_menu/__init__.py
Normal file
29
sale_purchase_no_product_template_menu/__manifest__.py
Normal file
29
sale_purchase_no_product_template_menu/__manifest__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright 2015-2019 Akretion France (http://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Sale Purchase No Product Template Menu',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Sale and Purchase',
|
||||
'license': 'AGPL-3',
|
||||
'summary': "Replace product.template menu entries by product.product menu entries",
|
||||
'description': """
|
||||
Sale Purchase No Product Template
|
||||
=================================
|
||||
|
||||
This module replaces the menu entries for product.template by menu entries for product.product in the *Sales* and *Purchases* menu entries. With this module, the only menu entry for product.template is in the menu *Sales > Configuration > Product Categories and Attributes*.
|
||||
|
||||
This module also switches to the tree view by default for Product menu entries, instead of the kanban view.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': [
|
||||
'purchase',
|
||||
'sale',
|
||||
],
|
||||
'data': ['view.xml'],
|
||||
'installable': False,
|
||||
}
|
||||
33
sale_purchase_no_product_template_menu/i18n/fr.po
Normal file
33
sale_purchase_no_product_template_menu/i18n/fr.po
Normal file
@@ -0,0 +1,33 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * sale_purchase_no_product_template_menu
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 8.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-05-30 15:27+0000\n"
|
||||
"PO-Revision-Date: 2016-05-30 15:27+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: sale_purchase_no_product_template_menu
|
||||
#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu
|
||||
msgid "Product Templates"
|
||||
msgstr "Modèles d'article"
|
||||
|
||||
#. module: sale_purchase_no_product_template_menu
|
||||
#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased
|
||||
#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell
|
||||
msgid "Products"
|
||||
msgstr "Articles"
|
||||
|
||||
#. module: sale_purchase_no_product_template_menu
|
||||
#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view
|
||||
msgid "{'invisible': 1, 'required': 0}"
|
||||
msgstr "{'invisible': 1, 'required': 0}"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * sale_purchase_no_product_template_menu
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 8.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-05-30 15:27+0000\n"
|
||||
"PO-Revision-Date: 2016-05-30 15:27+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: sale_purchase_no_product_template_menu
|
||||
#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu
|
||||
msgid "Product Templates"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_purchase_no_product_template_menu
|
||||
#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased
|
||||
#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_purchase_no_product_template_menu
|
||||
#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view
|
||||
msgid "{'invisible': 1, 'required': 0}"
|
||||
msgstr ""
|
||||
|
||||
63
sale_purchase_no_product_template_menu/view.xml
Normal file
63
sale_purchase_no_product_template_menu/view.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2015-2019 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<!-- PURCHASE -->
|
||||
<record id="product_product_action_purchased" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="context">{'search_default_filter_to_purchase': 1}</field>
|
||||
<field name="search_view_id" eval="False"/> <!-- Force empty -->
|
||||
<field name="view_id" eval="False"/> <!-- Force empty -->
|
||||
</record>
|
||||
|
||||
<record id="purchase.menu_procurement_partner_contact_form" model="ir.ui.menu">
|
||||
<field name="action" ref="product_product_action_purchased"/>
|
||||
</record>
|
||||
|
||||
<!-- SALE -->
|
||||
<!-- I'd prefer to inherit product.product_normal_action_sell and
|
||||
change the "name" field, but it doesn't work with translation,
|
||||
so I redefine a new menu entry -->
|
||||
<record id="product_product_action_sell" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="context">{'search_default_filter_to_sell': 1}</field>
|
||||
<field name="search_view_id" eval="False"/>
|
||||
<field name="view_id" ref="product.product_product_tree_view"/>
|
||||
<field name="search_view_id" ref="product.product_search_form_view"/>
|
||||
</record>
|
||||
|
||||
<!-- To keep good translations, we re-use the product.template menu
|
||||
entry and link it to product product -->
|
||||
<record id="sale.menu_product_template_action" model="ir.ui.menu">
|
||||
<!-- related action is "product.product_template_action" -->
|
||||
<field name="action" ref="product_product_action_sell"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Templates</field> <!-- native value is "Products" -->
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Create a product template menu entry in configuration -->
|
||||
<menuitem id="sale_config_product_template_menu" action="product.product_template_action"
|
||||
parent="sale.prod_config_main"/>
|
||||
|
||||
|
||||
<record id="product.product_normal_action_sell" model="ir.actions.act_window">
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -7,24 +7,24 @@
|
||||
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
<record id="account_invoice_form" model="ir.ui.view">
|
||||
<field name="name">sale_usability.customer.invoice.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="sale.account_invoice_form" />
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_manager'))]" />
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="sale.account_invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button
|
||||
name="show_sale_orders"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-pencil-square-o"
|
||||
attrs="{'invisible': [('sale_count', '=', 0)]}">
|
||||
<field name="sale_count" widget="statinfo" string="Sale Orders" />
|
||||
<button name="show_sale_orders"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-pencil-square-o"
|
||||
attrs="{'invisible': [('sale_count', '=', 0)]}">
|
||||
<field name="sale_count" widget="statinfo" string="Sale Orders"/>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record id="view_move_form" model="ir.ui.view">
|
||||
<field name="name">sale_usability.account.move.form</field>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright 2021 Akretion France (http://www.akretion.com)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
{
|
||||
'name': 'Sales Teams Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Sales/Sales',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Sales Teams usability enhancements',
|
||||
'description': """
|
||||
Sales Teams Usability
|
||||
=====================
|
||||
|
||||
The usability improvements include:
|
||||
|
||||
* set 'name' field of crm.tag un-translatable
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sales_team'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import crm_tag
|
||||
@@ -1,11 +0,0 @@
|
||||
# Copyright 2021 Akretion France (http://www.akretion.com)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CrmTag(models.Model):
|
||||
_inherit = "crm.tag"
|
||||
|
||||
name = fields.Char(translate=False)
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copyright 2014-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).
|
||||
|
||||
|
||||
{
|
||||
'name': 'Stock Picking Type Default Partner',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Inventory, Logistics, Warehousing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Adds a default partner on types of operation',
|
||||
'description': """
|
||||
Stock Picking Type Default Partner
|
||||
==================================
|
||||
|
||||
This module adds a new field on the Types of Operation (stock.picking.type) : *Default Partner*. This is useful for multi-site companies that create inter-site Type of Operations: all the operations that use this Type of Operation should have the same destination partner.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['stock'],
|
||||
'data': ['views/stock_picking_type.xml'],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import stock_picking
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright 2014-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, fields, api
|
||||
|
||||
|
||||
class StockPickingType(models.Model):
|
||||
_inherit = 'stock.picking.type'
|
||||
|
||||
default_partner_id = fields.Many2one(
|
||||
'res.partner', string='Default Partner', ondelete='restrict',
|
||||
help="If set, it will be the default partner on this type of "
|
||||
"pickings.")
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
@api.model
|
||||
def _default_partner_id(self):
|
||||
if self._context.get('default_picking_type_id'):
|
||||
picktype = self.env['stock.picking.type'].browse(
|
||||
self._context.get('default_picking_type_id'))
|
||||
if picktype.default_partner_id:
|
||||
return picktype.default_partner_id
|
||||
return False
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
default=lambda self: self._default_partner_id())
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2015-2021 Akretion (http://www.akretion.com/)
|
||||
@author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_picking_type_form" model="ir.ui.view">
|
||||
<field name="name">default.partner.stock.picking.type.form</field>
|
||||
<field name="model">stock.picking.type</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_type_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="default_location_dest_id" position="after">
|
||||
<field name="default_partner_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright 2021 Akretion (https://www.akretion.com).
|
||||
# @author Kévin Roche <kevin.roche@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Stock relation usability",
|
||||
"summary": "SUMMARY",
|
||||
"version": "14.0.1.0.0",
|
||||
"category": "Inventory, Logistic, Storage",
|
||||
"website": "http://www.akretion.com",
|
||||
"author": "Akretion",
|
||||
"license": "AGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"depends": [
|
||||
"stock",
|
||||
"purchase",
|
||||
],
|
||||
"data": [
|
||||
"views/stock_picking.xml",
|
||||
],
|
||||
"demo": [],
|
||||
"qweb": [],
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import stock_move
|
||||
from . import stock_picking
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright (C) 2021 Akretion (<http://www.akretion.com>).
|
||||
# @author Kévin Roche <kevin.roche@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
location_dest_list = fields.Text(
|
||||
string="Locations", compute="_compute_locations_dest_list"
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"move_line_ids", "move_line_ids.location_dest_id", "move_line_ids.qty_done"
|
||||
)
|
||||
def _compute_locations_dest_list(self):
|
||||
for move in self:
|
||||
data = []
|
||||
separator = ", "
|
||||
dest_list = move.move_line_ids.location_dest_id
|
||||
for dest in dest_list:
|
||||
lines_qty = move.move_line_ids.search(
|
||||
[("move_id", "=", move.id), ("location_dest_id", "=", dest.id)]
|
||||
).mapped("qty_done")
|
||||
quantity = int(sum(lines_qty))
|
||||
location = dest.name
|
||||
data.append("{}: {}".format(quantity, location))
|
||||
move.location_dest_list = separator.join(data)
|
||||
|
||||
def _compute_is_quantity_done_editable(self):
|
||||
super()._compute_is_quantity_done_editable()
|
||||
for move in self:
|
||||
if len(move.move_line_ids) == 1 and move.show_details_visible:
|
||||
move.is_quantity_done_editable = True
|
||||
@@ -1,38 +0,0 @@
|
||||
# Copyright (C) 2021 Akretion (<http://www.akretion.com>).
|
||||
# @author Kévin Roche <kevin.roche@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
def action_fill_quantity_done(self):
|
||||
self.ensure_one()
|
||||
for move in self.move_ids_without_package:
|
||||
if move.move_line_ids:
|
||||
first_line = move.move_line_ids[0]
|
||||
else:
|
||||
first_line = False
|
||||
if move.quantity_done == 0 and first_line:
|
||||
qty = move.product_uom_qty
|
||||
if first_line.qty_done == 0:
|
||||
first_line.write(
|
||||
{
|
||||
"qty_done": qty,
|
||||
}
|
||||
)
|
||||
elif move.quantity_done < move.product_uom_qty or (
|
||||
move.quantity_done == 0 and not first_line
|
||||
):
|
||||
qty = move.product_uom_qty - move.quantity_done
|
||||
self.env["stock.move.line"].create(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"location_dest_id": move.location_dest_id.id,
|
||||
"location_id": move.location_id.id,
|
||||
"product_uom_id": move.product_uom.id,
|
||||
"qty_done": qty,
|
||||
}
|
||||
)
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_picking_form" model="ir.ui.view">
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='product_uom']" position="after">
|
||||
<field name="location_dest_list" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_uom_qty']" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'column_invisible': [('parent.state', '=', 'done')]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='move_ids_without_package']" position="before">
|
||||
<button
|
||||
name="action_fill_quantity_done"
|
||||
type="object"
|
||||
string="Fill Done Quantity"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,44 +2,10 @@
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class StockInventory(models.Model):
|
||||
_inherit = 'stock.inventory'
|
||||
|
||||
prefill_counted_quantity = fields.Selection(
|
||||
readonly=True, states={'draft': [('readonly', False)]})
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockInventoryLine(models.Model):
|
||||
_inherit = 'stock.inventory.line'
|
||||
|
||||
product_barcode = fields.Char(related='product_id.barcode', string="Product Barcode")
|
||||
difference_qty = fields.Float(search="_search_difference_qty_usability")
|
||||
|
||||
def _search_difference_qty_usability(self, operator, value):
|
||||
# Inspired by the method _search_difference_qty() from the
|
||||
# official stock module
|
||||
# So a part of this code is copyright Odoo SA under LGPL licence
|
||||
if not self.env.context.get('default_inventory_id'):
|
||||
raise NotImplementedError(_('Unsupported search on %s outside of an Inventory Adjustment', 'difference_qty'))
|
||||
lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))])
|
||||
line_ids = []
|
||||
for line in lines:
|
||||
if operator == '=':
|
||||
if float_is_zero(line.difference_qty, line.product_id.uom_id.rounding):
|
||||
line_ids.append(line.id)
|
||||
elif operator == '!=':
|
||||
if not float_is_zero(line.difference_qty, line.product_id.uom_id.rounding):
|
||||
line_ids.append(line.id)
|
||||
elif operator == '>':
|
||||
if float_compare(line.difference_qty, 0, line.product_id.uom_id.rounding) > 0:
|
||||
line_ids.append(line.id)
|
||||
elif operator == '<':
|
||||
if float_compare(line.difference_qty, 0, line.product_id.uom_id.rounding) < 0:
|
||||
line_ids.append(line.id)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
return [('id', 'in', line_ids)]
|
||||
|
||||
@@ -7,20 +7,6 @@
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_inventory_form" model="ir.ui.view">
|
||||
<field name="name">usability.stock.inventory.form</field>
|
||||
<field name="model">stock.inventory</field>
|
||||
<field name="inherit_id" ref="stock.view_inventory_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_open_inventory_lines" states="confirm" position="after">
|
||||
<button name="action_open_inventory_lines" states="done" string="Show Inventory Lines" type="object"/>
|
||||
</button>
|
||||
<field name="prefill_counted_quantity" position="attributes">
|
||||
<attribute name="attrs">{}</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_line_tree" model="ir.ui.view">
|
||||
<field name="name">usability.stock.inventory.line.tree</field>
|
||||
<field name="model">stock.inventory.line</field>
|
||||
@@ -44,25 +30,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_line_search" model="ir.ui.view">
|
||||
<field name="name">usability.stock.inventory.line.search</field>
|
||||
<field name="model">stock.inventory.line</field>
|
||||
<field name="inherit_id" ref="stock.stock_inventory_line_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="categ_id"/>
|
||||
</field>
|
||||
<filter name="difference" position="after">
|
||||
<filter string="Difference = 0"
|
||||
name="counted_equal" domain="[('difference_qty', '=', 0)]"/>
|
||||
<filter string="Counted lower than Theoretical"
|
||||
name="counted_lower" domain="[('difference_qty', '<', 0)]"/>
|
||||
<filter string="Counted higher than Theoretical"
|
||||
name="counted_higher" domain="[('difference_qty', '>', 0)]"/>
|
||||
<separator/>
|
||||
<filter string="Counted" name="counted" domain="[('product_qty', '>', 0)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<xpath expr="//field[@name='move_ids_without_package']/tree/field[@name='product_id']" position="after">
|
||||
<field name="product_barcode" optional="hide"/>
|
||||
<field name="name" optional="hide"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations" optional="show" domain="[('id', 'child_of', 'parent.location_id')]" options="{'no_create': True}"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" optional="show" domain="[('id', 'child_of', 'parent.location_dest_id')]" options="{'no_create': True}"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations" optional="show"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" optional="show"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='move_ids_without_package']/tree/button[@name='action_assign_serial']" position="after">
|
||||
<button type="object" name="button_do_unreserve" string="Unreserve"
|
||||
@@ -56,7 +56,7 @@
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_picking_internal_search" model="ir.ui.view">
|
||||
<field name="name">stock_usability.view_picking_search</field>
|
||||
<field name="model">stock.picking</field>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright 2020-2021 Akretion France (http://www.akretion.com)
|
||||
# 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).
|
||||
|
||||
|
||||
{
|
||||
'name': 'Stock Valuation XLSX',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Generate XLSX reports for past or present stock levels',
|
||||
@@ -37,11 +37,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,
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from . import stock_expiry_depreciation_rule
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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.company)
|
||||
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,5 +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
|
||||
access_stock_valuation_xlsx,stock.valuation.xlsx wizard,model_stock_valuation_xlsx,stock.group_stock_user,1,1,1,0
|
||||
access_stock_variation_xlsx,stock.variation.xlsx wizard,model_stock_variation_xlsx,stock.group_stock_user,1,1,1,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>
|
||||
@@ -16,7 +16,7 @@
|
||||
<button name="action_validate" position="after">
|
||||
<button name="%(stock_valuation_xlsx_action)d" type="action"
|
||||
states="done" string="XLSX Valuation Report"
|
||||
context="{'default_source': 'inventory', 'default_inventory_id': active_id}"/>
|
||||
context="{'default_source': 'inventory', 'default_inventory_id': active_id, 'default_location_id': location_id}"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
from . import stock_valuation_xlsx
|
||||
from . import stock_variation_xlsx
|
||||
|
||||
@@ -4,7 +4,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 io import BytesIO
|
||||
from datetime import datetime
|
||||
@@ -17,61 +16,59 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class StockValuationXlsx(models.TransientModel):
|
||||
_name = 'stock.valuation.xlsx'
|
||||
_check_company_auto = True
|
||||
_description = 'Generate XLSX report for stock valuation'
|
||||
|
||||
export_file = fields.Binary(string='XLSX Report', readonly=True, attachment=True)
|
||||
export_file = fields.Binary(string='XLSX Report', readonly=True)
|
||||
export_filename = fields.Char(readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', default=lambda self: self.env.company,
|
||||
required=True)
|
||||
# I don't use ir.actions.url on v12, because it renders
|
||||
# the wizard unusable after the first report generation, which creates
|
||||
# a lot of confusion for users
|
||||
state = fields.Selection([
|
||||
('setup', 'Setup'),
|
||||
('done', 'Done'),
|
||||
], string='State', default='setup', readonly=True)
|
||||
warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', string='Warehouse', check_company=True,
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
'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')), ('company_id', '=', company_id)]",
|
||||
default=lambda self: self._default_location(), check_company=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.")
|
||||
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'),
|
||||
('stock', 'Stock Levels'),
|
||||
], string='Source data', default='stock', required=True)
|
||||
], string='Source data', default='stock', required=True,
|
||||
states={'done': [('readonly', True)]})
|
||||
inventory_id = fields.Many2one(
|
||||
'stock.inventory', string='Inventory', check_company=True,
|
||||
domain="[('state', '=', 'done'), ('company_id', '=', company_id)]")
|
||||
'stock.inventory', string='Inventory', domain=[('state', '=', 'done')],
|
||||
states={'done': [('readonly', True)]})
|
||||
stock_date_type = fields.Selection([
|
||||
('present', 'Present'),
|
||||
('past', 'Past'),
|
||||
], string='Present or Past', default='present')
|
||||
], string='Present or Past', default='present',
|
||||
states={'done': [('readonly', True)]})
|
||||
past_date = fields.Datetime(
|
||||
string='Past Date', default=fields.Datetime.now)
|
||||
string='Past Date', states={'done': [('readonly', True)]},
|
||||
default=fields.Datetime.now)
|
||||
categ_subtotal = fields.Boolean(
|
||||
string='Subtotals per Categories', default=True,
|
||||
help="Show a subtotal per product category.")
|
||||
states={'done': [('readonly', True)]},
|
||||
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')
|
||||
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)
|
||||
split_by_lot = fields.Boolean(string='Display Lots')
|
||||
split_by_location = fields.Boolean(string='Display Stock Locations')
|
||||
|
||||
@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
|
||||
], default='past', string='Cost Price Date',
|
||||
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_location(self):
|
||||
@@ -126,41 +123,28 @@ 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.to_date(past_date) # convert datetime to date
|
||||
else:
|
||||
date_dt = fields.Date.context_today(self)
|
||||
for rule in rules:
|
||||
rule['start_date'] = 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']
|
||||
ppho = self.env['product.price.history']
|
||||
fields_list = self._prepare_product_fields()
|
||||
# if not standard_price_past_date: # TODO
|
||||
if True:
|
||||
if not standard_price_past_date:
|
||||
fields_list.append('standard_price')
|
||||
products = ppo.search_read([('id', 'in', in_stock_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_past_date:
|
||||
# No more product.price.history on v14
|
||||
# We are supposed to use stock.valuation.layer.revaluation
|
||||
# TODO migrate to stock.valuation.layer.revaluation
|
||||
#history = ppho.search_read([
|
||||
# ('company_id', '=', company_id),
|
||||
# ('product_id', '=', p['id']),
|
||||
# ('datetime', '<=', standard_price_past_date)],
|
||||
# ['cost'], order='datetime desc, id desc', limit=1)
|
||||
#standard_price = history and history[0]['cost'] or 0.0
|
||||
standard_price = p['standard_price'] # TODO remove this tmp stuff
|
||||
history = ppho.search_read([
|
||||
('company_id', '=', company_id),
|
||||
('product_id', '=', p['id']),
|
||||
('datetime', '<=', standard_price_past_date)],
|
||||
['cost'], order='datetime desc, id desc', limit=1)
|
||||
standard_price = history and history[0]['cost'] or 0.0
|
||||
else:
|
||||
standard_price = p['standard_price']
|
||||
product_id2data[p['id']] = {'standard_price': standard_price}
|
||||
@@ -172,56 +156,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['uom.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['uom.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', 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()
|
||||
@@ -309,7 +275,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:
|
||||
@@ -318,27 +284,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']],
|
||||
))
|
||||
@@ -349,19 +305,14 @@ class StockValuationXlsx(models.TransientModel):
|
||||
def generate(self):
|
||||
self.ensure_one()
|
||||
logger.debug('Start generate XLSX stock valuation report')
|
||||
splo = self.env['stock.production.lot'].with_context(active_test=False)
|
||||
prec_qty = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
prec_price = self.env['decimal.precision'].precision_get('Product Price')
|
||||
company = self.company_id
|
||||
company = self.env.user.company_id
|
||||
company_id = company.id
|
||||
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."))
|
||||
@@ -384,25 +335,15 @@ class StockValuationXlsx(models.TransientModel):
|
||||
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
|
||||
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 = list(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(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 = BytesIO()
|
||||
@@ -415,15 +356,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']):
|
||||
@@ -479,9 +417,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:
|
||||
@@ -497,20 +432,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:
|
||||
@@ -533,17 +460,21 @@ class StockValuationXlsx(models.TransientModel):
|
||||
filename = 'Odoo_stock_%s.xlsx' % stock_time_str.replace(' ', '-').replace(':', '_')
|
||||
export_file_b64 = base64.b64encode(file_data.read())
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'export_filename': filename,
|
||||
'export_file': export_file_b64,
|
||||
})
|
||||
action = {
|
||||
'name': _('Stock Valuation XLSX'),
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': "web/content/?model=%s&id=%d&filename_field=export_filename&"
|
||||
"field=export_file&download=true&filename=%s" % (
|
||||
self._name, self.id, self.export_filename),
|
||||
'target': 'new',
|
||||
}
|
||||
# action = {
|
||||
# 'name': _('Stock Valuation XLSX'),
|
||||
# 'type': 'ir.actions.act_url',
|
||||
# 'url': "web/content/?model=%s&id=%d&filename_field=export_filename&"
|
||||
# "field=export_file&download=true&filename=%s" % (
|
||||
# self._name, self.id, self.export_filename),
|
||||
# 'target': 'self',
|
||||
# }
|
||||
action = self.env['ir.actions.act_window'].for_xml_id(
|
||||
'stock_valuation_xlsx', 'stock_valuation_xlsx_action')
|
||||
action['res_id'] = self.id
|
||||
return action
|
||||
|
||||
def _prepare_styles(self, workbook, company, prec_price):
|
||||
@@ -551,8 +482,8 @@ class StockValuationXlsx(models.TransientModel):
|
||||
categ_bg_color = '#e1daf5'
|
||||
col_title_bg_color = '#fff9b4'
|
||||
regular_font_size = 10
|
||||
currency_num_format = '# ### ##0.00 %s' % company.currency_id.symbol
|
||||
price_currency_num_format = '# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol)
|
||||
currency_num_format = u'# ### ##0.00 %s' % company.currency_id.symbol
|
||||
price_currency_num_format = u'# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol)
|
||||
styles = {
|
||||
'doc_title': workbook.add_format({
|
||||
'bold': True, 'font_size': regular_font_size + 10,
|
||||
@@ -572,7 +503,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': '0.%'}),
|
||||
'regular': workbook.add_format({}),
|
||||
'regular_small': workbook.add_format({'font_size': regular_font_size - 2}),
|
||||
'categ_title': workbook.add_format({
|
||||
@@ -597,10 +527,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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<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="company_id" groups="base.group_multi_company"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="categ_ids" widget="many2many_tags"/>
|
||||
<field name="warehouse_id"/>
|
||||
<field name="location_id"/>
|
||||
@@ -27,14 +27,18 @@
|
||||
<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"/>
|
||||
<field name="export_filename" invisible="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="generate" type="object" class="btn-primary" string="Generate"/>
|
||||
<button special="cancel" string="Close" class="btn-default"/>
|
||||
<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>
|
||||
@@ -51,7 +55,6 @@
|
||||
<record id="stock_account.menu_valuation" 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,458 +0,0 @@
|
||||
# Copyright 2020-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 models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero, float_round
|
||||
from io import BytesIO
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import xlsxwriter
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockVariationXlsx(models.TransientModel):
|
||||
_name = 'stock.variation.xlsx'
|
||||
_check_company_auto = True
|
||||
_description = 'Generate XLSX report for stock valuation variation between 2 dates'
|
||||
|
||||
export_file = fields.Binary(string='XLSX Report', readonly=True, attachment=True)
|
||||
export_filename = fields.Char(readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', default=lambda self: self.env.company,
|
||||
required=True)
|
||||
warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', string='Warehouse', check_company=True,
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', string='Root Stock Location', required=True,
|
||||
domain="[('usage', 'in', ('view', 'internal')), ('company_id', '=', company_id)]",
|
||||
default=lambda self: self._default_location(), check_company=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.")
|
||||
start_date = fields.Datetime(
|
||||
string='Start Date', required=True)
|
||||
standard_price_start_date_type = fields.Selection([
|
||||
('start', 'Start Date'),
|
||||
('present', 'Current'),
|
||||
], default='start', required=True,
|
||||
string='Cost Price for Start Date')
|
||||
end_date_type = fields.Selection([
|
||||
('present', 'Present'),
|
||||
('past', 'Past'),
|
||||
], string='End Date Type', default='present', required=True)
|
||||
end_date = fields.Datetime(
|
||||
string='End Date', 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)
|
||||
categ_subtotal = fields.Boolean(
|
||||
string='Subtotals per Categories', default=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']
|
||||
fields_list = self._prepare_product_fields()
|
||||
# if not standard_price_start_date or not standard_price_end_date: # TODO
|
||||
if True:
|
||||
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'])
|
||||
if standard_price_start_date:
|
||||
# No more product.price.history on v14
|
||||
# We are supposed to use stock.valuation.layer.revaluation
|
||||
# TODO migrate to stock.valuation.layer.revaluation
|
||||
#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
|
||||
start_standard_price = p['standard_price'] # TODO remove this tmp stuff
|
||||
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
|
||||
end_standard_price = p['standard_price'] # TODO remove this tmp stuff
|
||||
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).with_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['quantity']) for item in sqo.read_group(domain_quant, ['product_id', 'quantity'], ['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.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':
|
||||
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 = BytesIO()
|
||||
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_dt = self.start_date
|
||||
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_dt = self.end_date
|
||||
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 = base64.b64encode(file_data.read())
|
||||
self.write({
|
||||
'export_filename': filename,
|
||||
'export_file': export_file_b64,
|
||||
})
|
||||
action = {
|
||||
'name': _('Stock Variation XLSX'),
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': "web/content/?model=%s&id=%d&filename_field=export_filename&"
|
||||
"field=export_file&download=true&filename=%s" % (
|
||||
self._name, self.id, self.export_filename),
|
||||
'target': 'new',
|
||||
}
|
||||
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,55 +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="company_id" groups="base.group_multi_company"/>
|
||||
<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>
|
||||
<footer>
|
||||
<button name="generate" type="object" class="btn-primary" string="Generate"/>
|
||||
<button special="cancel" string="Close" class="btn-default"/>
|
||||
</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