Compare commits

..

42 Commits

Author SHA1 Message Date
Kev-Roche
e12cca8dd0 [ADD] values tree view on product attribute 2022-03-11 18:34:44 +01:00
Alexis de Lattre
f9a7983d71 account_usability: update move line search view for reconcile filters 2022-03-01 15:55:03 +01:00
David Beal
870746b965 UPD stk_no_prd_tmpl: make installable 2022-02-28 18:06:05 +01:00
Alexis de Lattre
f22c6522d5 Add industry_id in account.invoice.report 2022-02-23 00:22:54 +01:00
Alexis de Lattre
1ef97629b7 [FIX] account_usability: fix cash in computation of sale_dates 2022-02-22 00:59:48 +01:00
Alexis de Lattre
77a372b3ca account_usability: Add date in outstanding payment widget 2022-02-08 20:39:11 +01:00
Alexis de Lattre
221b090cc8 sale_usability: pivot view by default on sale.report 2022-02-08 07:54:50 +01:00
Raphaël Valyi
e4a2ff5bd4 Merge pull request #153 from akretion/14.0-web_tab_title
[14.0][ADD] web_tab_title
2022-01-26 10:51:31 -03:00
Alexis de Lattre
c1d334b109 account_usability: small code cleanup 2022-01-06 23:34:44 +01:00
Sébastien BEAU
c7c9b2d341 [IMP] add tracking on category 2022-01-04 17:21:46 +01:00
Alexis de Lattre
eddab6020a account_usability: improve view for analytic account and analytic account group 2021-12-31 00:33:48 +01:00
Alexis de Lattre
d1181ca91d account_usability: improve view of account.group
Add group by on group_id in search view of account.account
2021-12-31 00:21:42 +01:00
Alexis de Lattre
bb83765ee2 account_usability: default value for user_type_id on journal accounts 2021-12-19 17:28:03 +01:00
Alexis de Lattre
88615a0774 Add barcode_code128 on product.product 2021-12-19 12:58:22 +01:00
Alexis de Lattre
05e649fa86 account_usability: avoir error when user doesn't have admin rights 2021-12-18 21:40:32 +01:00
Alexis de Lattre
08118ec4f5 Add support for config param usability.line_name_no_product_code
The same ir.config_parameter is used in:
- purchase_usability for purchase.order.line
- sale_usability for sale.order.line
- account_usability for account.move.line
2021-12-03 22:24:59 +01:00
Alexis de Lattre
6d496ba302 Add static legal terms on company for invoice and sale
Add product_supplier_code on purchase.order.line
2021-12-03 17:27:03 +00:00
Kev-Roche
40b79890fe [FIX] stock_reception_usability 2021-12-01 14:31:24 +01:00
Kévin Roche
58b8d300b8 Merge pull request #160 from akretion/14.0_purchase_usability_French_traduction
[TRAD][14.0] French traduction purchase_usability
2021-11-30 15:35:25 +01:00
Kev-Roche
cdcf4eb406 [TRAD][14.0] French traduction purchase_usability 2021-11-30 15:32:37 +01:00
Kevin.roche
a22f79ef44 [ADD] stock_reception_usability 2021-11-29 13:24:24 +01:00
Alexis de Lattre
99dd4de4f7 stock_valuation_xlsx: first port to v14 without price history (when you ask for a past price, you get the current price for the moment) 2021-11-26 23:57:22 +01:00
Alexis de Lattre
f3d6b67043 [MIG] link_tracker_usability from v10 to v14
Prepare port of mass_mailing_usability... but no real port for the moment
2021-11-18 17:18:08 +01:00
Alexis de Lattre
1963af114b mass_mailing_usability: Fix dependencies 2021-11-18 16:51:13 +01:00
Alexis de Lattre
1da4c40927 Add module mass_mailing_usability
Improve module link_tracker_usability
2021-11-18 16:51:13 +01:00
Alexis de Lattre
edc9db5839 New module link_tracker_usability 2021-11-18 16:51:13 +01:00
Alexis de Lattre
882d068f1a [MIG] base_partner_ref from v12 to v14 2021-11-18 16:20:49 +01:00
Alexis de Lattre
878db1d0a1 [MIG] crm_usability from v10 to v14
New module sales_team_usability (remove translation on crm.tag, which
was in crm_usability in v10)
2021-11-18 10:43:53 +01:00
Alexis de Lattre
600acd2f26 Add multi-company ir.rule for crm.lead 2021-11-18 10:21:09 +01:00
Alexis de Lattre
059c3b4a09 Add groupby on partner on opportunity search view 2021-11-18 10:21:09 +01:00
David Beal
895e1d9dd0 FIX branding 2021-11-18 10:21:09 +01:00
Alexis de Lattre
0b3ffc804f translate=False on 'name' field of crm.lead.tag and res.partner.category 2021-11-18 10:21:09 +01:00
Alexis de Lattre
c0a03dbb0e Work on usability of CRM 2021-11-18 10:21:09 +01:00
David Beal
6e5f263283 IMP add icons 2021-11-18 10:21:09 +01:00
Raphaël Valyi
d4ebbb28d9 Merge pull request #150 from akretion/14.0-imp-moves-domain-picking-view
[IMP] StockMoves locations domain in picking view
2021-11-16 13:09:50 -03:00
Raphaël Valyi
7dd204e57e Merge pull request #148 from akretion/14.0-product-usability-filter-domain-supinfo
product_usability: Improve filter domain on supplier product name/reference
2021-11-16 12:34:55 -03:00
Alexis de Lattre
e1a84973da account_usability: FIX display of decimal precision of sale price in product form view 2021-11-12 12:37:42 +01:00
Alexis de Lattre
13e68ac0f5 Remove modules pos_no_product_template_menu and sale_purchase_no_product_template_menu
We won't port those modules to v14
2021-11-04 12:37:34 +01:00
Alexis de Lattre
3b17c2e5fb Improve register payment wizard view 2021-11-04 11:16:24 +01:00
Raphaël Valyi
ce2255623d [ADD] web_tab_title 2021-10-26 22:06:48 -03:00
clementmbr
279dc7c6c0 [IMP] add domain on origin and destination location for stock.moves in picking view 2021-10-06 14:43:15 +02:00
clementmbr
45500f5bd8 product_usability: Improve filter domain on supplier product names 2021-09-03 15:25:55 +02:00
94 changed files with 1900 additions and 679 deletions

View File

@@ -52,6 +52,7 @@ This modules adds the following functions:
* don't attach PDF upon invoice report generation on supplier invoices/refunds
* Add filter on debit and credit amount for Move Lines
* Add supplier invoice number in invoice tree view
* Add date in outstanding payment widget on invoice form view (requires `odoo PR 84180 <https://github.com/odoo/odoo/pull/84180>`_)
Together with this module, I recommend the use of the following modules:

View File

@@ -19,6 +19,9 @@
'data': [
'views/account_account_type.xml',
'views/account_account.xml',
'views/account_group.xml',
'views/account_analytic_account.xml',
'views/account_analytic_group.xml',
'views/account_bank_statement.xml',
'views/account_invoice_report.xml',
'views/account_journal.xml',
@@ -28,10 +31,14 @@
'views/product.xml',
'views/res_config_settings.xml',
'views/res_partner.xml',
'views/res_company.xml',
'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',
'report/invoice_report.xml',
],
'qweb': ['static/src/xml/account_payment.xml'],
'installable': True,
}

View File

@@ -6,4 +6,6 @@ from . import account_journal
from . import account_move
from . import account_partial_reconcile
from . import res_partner
from . import res_company
from . import product
from . import account_invoice_report

View File

@@ -27,9 +27,7 @@ class AccountBankStatement(models.Model):
def _check_balance_end_real_same_as_computed(self):
for stmt in self:
if stmt.hide_bank_statement_balance:
continue
else:
if not stmt.hide_bank_statement_balance:
super(AccountBankStatement, stmt)._check_balance_end_real_same_as_computed()
return True
@@ -80,7 +78,8 @@ class AccountBankStatementLine(models.Model):
def show_account_move(self):
self.ensure_one()
action = self.env.ref('account.action_move_line_form').read()[0]
action = self.env["ir.actions.actions"]._for_xml_id(
'account.action_move_line_form')
# Note: this action is on account.move, not account.move.line !
action.update({
'views': False,

View File

@@ -0,0 +1,17 @@
# Copyright 2022 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
industry_id = fields.Many2one('res.partner.industry', string='Partner Industry', readonly=True)
@api.model
def _select(self):
res = super()._select()
res += ", COALESCE(partner.industry_id, commercial_partner.industry_id) AS industry_id"
return res

View File

@@ -16,6 +16,13 @@ class AccountJournal(models.Model):
"you don't want to enter the start/end balance manually: it "
"will prevent the display of wrong information in the accounting "
"dashboard and on bank statements.")
# Used to set default user_type_id on account fields
account_type_current_liabilities_id = fields.Many2one(
'account.account.type',
default=lambda self: self.env.ref('account.data_account_type_current_liabilities').id)
account_type_current_assets_id = fields.Many2one(
'account.account.type',
default=lambda self: self.env.ref('account.data_account_type_current_assets').id)
@api.depends(
'name', 'currency_id', 'company_id', 'company_id.currency_id', 'code')

View File

@@ -152,13 +152,13 @@ class AccountMove(models.Model):
""" French law requires to set sale order dates into invoice
returned string: "sale1 (date1), sale2 (date2) ..."
"""
for inv in self:
sales = inv.invoice_line_ids.mapped(
for move in self:
sales = move.invoice_line_ids.mapped(
'sale_line_ids').mapped('order_id')
dates = ["%s (%s)" % (
x.name, format_date(inv.env, self.date_order))
x.name, format_date(move.env, x.date_order))
for x in sales]
inv.sale_dates = ", ".join(dates)
move.sale_dates = ", ".join(dates)
# allow to manually create moves not only in general journals,
# but also in cash journal and check journals (= bank journals not linked to a bank account)
@@ -231,7 +231,8 @@ class AccountMoveLine(models.Model):
def show_account_move_form(self):
self.ensure_one()
action = self.env.ref('account.action_move_line_form').read()[0]
action = self.env["ir.actions.actions"]._for_xml_id(
'account.action_move_line_form')
action.update({
'res_id': self.move_id.id,
'view_id': False,
@@ -251,3 +252,14 @@ class AccountMoveLine(models.Model):
rec_str = ', '.join([
'a%d' % pr.id for pr in line.matched_debit_ids + line.matched_credit_ids])
line.reconcile_string = rec_str
def _get_computed_name(self):
# This is useful when you want to have the product code in a dedicated
# column in your customer invoice report
# The same ir.config_parameter is used in sale_usability,
# purchase_usability and account_usability
no_product_code_param = self.env['ir.config_parameter'].sudo().get_param(
'usability.line_name_no_product_code')
if no_product_code_param and no_product_code_param == 'True':
self = self.with_context(display_default_code=False)
return super()._get_computed_name()

View File

@@ -0,0 +1,21 @@
# Copyright 2021 Akretion France (https://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 ResCompany(models.Model):
_inherit = 'res.company'
# There is a native field invoice_terms which is displayed on res.config.settings
# when the ir.config_parameter account.use_invoice_terms is True
# But there are several problems with this native field:
# - it is copied on the 'narration' field of account.move => we don't want that
# - the text block is very small on the form view of res.config.settings
# So I decided to have our own field "fixed_invoice_terms"
# The native field can still be used when you need to customise some
# terms and conditions on each invoice (not very common, but...)
# To underline this different with the native field, I prefix it with 'static_'
static_invoice_terms = fields.Text(
translate=True, string="Legal Terms on Invoice")

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 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).
-->
<templates id="template" xml:space="preserve">
<!-- Requires https://github.com/odoo/odoo/pull/84180 -->
<t t-extend="ShowPaymentInfo" >
<t t-jquery="td:first" t-operation="after">
<td style="max-width: 25em;" id="outstanding-date">
<div class="oe_form_field" style="margin-right: 5px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"><t t-esc="line.date"></t></div>
</td>
</t>
</t>
</templates>

View File

@@ -31,6 +31,9 @@
<field name="name" position="after">
<field name="code" filter_domain="[('code', '=like', str(self)+'%')]" string="Code"/>
</field>
<filter name="accounttype" position="after">
<filter name="group_groupby" string="Group" context="{'group_by': 'group_id'}"/>
</filter>
</field>
</record>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015-2021 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_account_analytic_account_list" model="ir.ui.view">
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_list"/>
<field name="arch" type="xml">
<field name="code" position="after">
<field name="group_id" optional="show"/>
</field>
</field>
</record>
<record id="view_account_analytic_account_search" model="ir.ui.view">
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_search"/>
<field name="arch" type="xml">
<filter name="associatedpartner" position="before">
<filter name="group_groupby" string="Group" context="{'group_by': 'group_id'}"/>
</filter>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,22 @@
<?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="account_analytic_group_tree_view" model="ir.ui.view">
<field name="model">account.analytic.group</field>
<field name="inherit_id" ref="analytic.account_analytic_group_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="parent_id" optional="show"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<?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_account_group_form" model="ir.ui.view">
<field name="model">account.group</field>
<field name="inherit_id" ref="account.view_account_group_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="parent_id"/>
</field>
</field>
</record>
<record id="view_account_group_tree" model="ir.ui.view">
<field name="model">account.group</field>
<field name="inherit_id" ref="account.view_account_group_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="parent_id" optional="show"/>
</field>
</field>
</record>
</odoo>

View File

@@ -14,16 +14,26 @@
<field name="arch" type="xml">
<tree string="Invoices Analysis">
<field name="move_id"/>
<field name="journal_id" optional="hide"/>
<field name="company_id" optional="hide" groups="base.group_multi_company"/>
<field name="invoice_date"/>
<field name="invoice_date_due"/>
<field name="move_type"/>
<field name="commercial_partner_id"/>
<field name="partner_id" optional="hide"/>
<field name="country_id" optional="hide"/>
<field name="industry_id" optional="hide"/>
<field name="invoice_user_id"/>
<field name="fiscal_position_id" optional="hide"/>
<field name="product_id"/>
<field name="product_categ_id" optional="hide"/>
<field name="account_id" optional="hide"/>
<field name="analytic_account_id" optional="hide" groups="analytic.group_analytic_accounting"/>
<field name="quantity" sum="1"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="price_subtotal" sum="1"/>
<field name="state"/>
<field name="payment_state" optional="hide"/>
</tree>
</field>
</record>

View File

@@ -14,6 +14,17 @@
<field name="arch" type="xml">
<field name="bank_statements_source" position="after">
<field name="hide_bank_statement_balance" groups="account.group_account_readonly"/>
<field name="account_type_current_liabilities_id" invisible="1"/>
<field name="account_type_current_assets_id" invisible="1"/>
</field>
<field name="suspense_account_id" position="attributes">
<attribute name="context">{'default_user_type_id': account_type_current_liabilities_id, 'default_reconcile': True}</attribute>
</field>
<field name="payment_debit_account_id" position="attributes">
<attribute name="context">{'default_user_type_id': account_type_current_assets_id, 'default_reconcile': True}</attribute>
</field>
<field name="payment_credit_account_id" position="attributes">
<attribute name="context">{'default_user_type_id': account_type_current_assets_id, 'default_reconcile': True}</attribute>
</field>
</field>
</record>

View File

@@ -88,10 +88,11 @@
<field name="debit" filter_domain="['|', ('debit', '=', self), ('credit', '=', self)]" string="Debit or Credit"/>
</field>
<filter name="unreconciled" position="before">
<filter name="reconciled" string="Fully Reconciled" domain="[('full_reconcile_id', '!=', False)]"/>
<filter name="reconciled" string="Fully Reconciled" domain="[('reconciled', '=', True)]"/>
</filter>
<filter name="unreconciled" position="attributes">
<attribute name="string">Unreconciled or Partially Reconciled</attribute>
<attribute name="domain">[('reconciled', '=', False), ('balance', '!=', 0), ('account_id.reconcile', '=', True)]</attribute>
</filter>
<!--
<field name="name" position="attributes">

View File

@@ -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'}" class="oe_inline"/>
<field name="list_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" class="oe_inline"/>
<label for="sale_price_type" string=" "/>
<field name="sale_price_type"/>
</div>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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).
-->
<odoo>
<record id="view_company_form" model="ir.ui.view">
<field name="name">account_usability.res.company.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Legal Terms" name="legal_terms">
<group string="Invoice Legal Terms" name="static_invoice_terms">
<field name="static_invoice_terms" nolabel="1"/>
</group>
</page>
</notebook>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,23 @@
<?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>

View File

@@ -1 +1 @@
from . import partner
from . import models

View File

@@ -1,10 +1,10 @@
# Copyright 2017-2019 Akretion (http://www.akretion.com)
# 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).
{
'name': 'Base Partner Reference',
'version': '12.0.1.0.0',
'version': '14.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': ['partner_view.xml'],
'installable': False,
'data': ['views/res_partner.xml'],
'installable': True,
}

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright 2017-2019 Akretion
# Copyright 2017-2021 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('is_company', 'name', 'parent_id.name', 'type', 'company_name', 'ref', 'invalidate_display_name')
@api.depends('ref', 'invalidate_display_name')
def _compute_display_name(self):
super(ResPartner, self)._compute_display_name()
super()._compute_display_name()
def _get_name(self):
partner = self
@@ -32,12 +32,13 @@ 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 = u"[%s] %s" % (partner.parent_id.ref, company_name)
company_name = "[%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'):
@@ -47,7 +48,8 @@ class ResPartner(models.Model):
name = name.replace('\n\n', '\n')
name = name.replace('\n\n', '\n')
if self._context.get('address_inline'):
name = name.replace('\n', ', ')
splitted_names = name.split("\n")
name = ", ".join([n for n in splitted_names if n.strip()])
if self._context.get('show_email') and partner.email:
name = "%s <%s>" % (name, partner.email)
if self._context.get('html_format'):
@@ -63,5 +65,6 @@ class ResPartner(models.Model):
if name and operator == 'ilike':
recs = self.search([('ref', '=', name)] + args, limit=limit)
if recs:
return recs.name_get()
rec_childs = self.search([('id', 'child_of', recs.ids)])
return rec_childs.name_get()
return super().name_search(name=name, args=args, operator=operator, limit=limit)

View File

@@ -11,29 +11,34 @@
<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="replace"/>
<xpath expr="//page[@name='sales_purchases']//field[@name='ref']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</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"/>
<field name="ref" optional="hide"/>
</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>

View File

@@ -39,6 +39,9 @@
<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>

View File

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

View File

@@ -0,0 +1,25 @@
# 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,
}

View File

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

View File

@@ -0,0 +1,13 @@
# 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.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,22 @@
<?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>

View File

@@ -0,0 +1,27 @@
# 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,
}

View File

@@ -0,0 +1,45 @@
<?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>

View File

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

View File

@@ -0,0 +1,29 @@
# 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,
}

View File

@@ -0,0 +1,48 @@
<?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>

View File

@@ -1,28 +0,0 @@
# 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,
}

View File

@@ -1,16 +0,0 @@
<?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>

View File

@@ -32,6 +32,8 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'views/product_pricelist_item.xml',
'views/product_template_view.xml',
'views/product_product.xml',
'views/product_category_view.xml',
'views/product_attribute_view.xml',
],
'installable': True,
}

View File

@@ -2,3 +2,5 @@ from . import product_product
from . import product_template
from . import product_supplierinfo
from . import product_pricelist
from . import product_category
from . import product_attribute

View File

@@ -0,0 +1,27 @@
# Copyright (C) 2022 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 ProductAttribute(models.Model):
_inherit = "product.attribute"
values_count = fields.Integer(compute="_compute_values_count")
@api.depends("value_ids")
def _compute_values_count(self):
for attr in self:
attr.values_count = len(attr.value_ids)
def show_values_ids(self):
return {
"name": "Attributes Lines",
"type": "ir.actions.act_window",
"res_id": self.id,
"view_mode": "tree",
"res_model": "product.attribute.value",
"view_id":self.env.ref("product_usability.product_attribute_value_view_tree").id,
"target": "current",
"domain": [("id", "in", self.value_ids.ids)],
}

View File

@@ -0,0 +1,13 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ProductCategory(models.Model):
_inherit = ['product.category', "mail.thread", "mail.activity.mixin"]
_name = 'product.category'
name = fields.Char(tracking=10)
parent_id = fields.Many2one(tracking=20)

View File

@@ -1,21 +1,21 @@
# Copyright 2015-2020 Akretion (http://www.akretion.com)
# Copyright 2015-2021 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# @author Raphaël Valyi <rvalyi@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
from odoo import api, models, fields
class ProductProduct(models.Model):
_inherit = 'product.product'
default_code = fields.Char(copy=False)
# track_visibility='onchange',
# barcode = fields.Char(track_visibility='onchange',
# weight = fields.Float(track_visibility='onchange')
# active = fields.Boolean(track_visibility='onchange')
default_code = fields.Char(copy=False, tracking=10)
barcode = fields.Char(tracking=20)
weight = fields.Float(tracking=30)
active = fields.Boolean(tracking=40)
barcode_code128 = fields.Char(
compute='_compute_barcode_code128',
help="Barcode in Code128-B with start char, checksum and stop char")
_sql_constraints = [(
# Maybe it could be better to have a constrain per company
@@ -26,3 +26,32 @@ class ProductProduct(models.Model):
'default_code_uniq',
'unique(default_code)',
'This internal reference already exists!')]
@api.model
def _compute_code128_checksum(self, code):
# This is NOT a full implementation of code128 checksum
csum = 104 # Start B
i = 0
for char in code:
i += 1
char_val = ord(char) - 32
csum += char_val * i
remainder = csum % 103
checksum = chr(remainder + 32)
return checksum
@api.depends('barcode')
def _compute_barcode_code128(self):
# We use Code128-B. Useful info on code128:
# https://boowiki.info/art/codes-a-barres/code-128.html
# Use code128.ttf and copy it in /usr/local/share/fonts/
startb = chr(209)
stop = chr(211)
for product in self:
code128 = False
barcode = product.barcode
if barcode and all([32 <= ord(x) <= 127 for x in barcode]):
checksum = self._compute_code128_checksum(barcode)
if checksum:
code128 = startb + barcode + checksum + stop
product.barcode_code128 = code128

View File

@@ -1,4 +1,4 @@
# Copyright 2015-2020 Akretion (http://www.akretion.com)
# Copyright 2015-2021 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# @author Raphaël Valyi <rvalyi@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@@ -21,9 +21,12 @@ class ProductTemplate(models.Model):
# are only shown in the form view of product.template, not in the form
# view of product.product
name = fields.Char(tracking=10)
categ_id = fields.Many2one(tracking=20)
type = fields.Selection(tracking=30)
list_price = fields.Float(tracking=40)
sale_ok = fields.Boolean(tracking=50)
purchase_ok = fields.Boolean(tracking=60)
active = fields.Boolean(tracking=70)
barcode = fields.Char(tracking=20)
default_code = fields.Char(tracking=30)
categ_id = fields.Many2one(tracking=40)
type = fields.Selection(tracking=50)
list_price = fields.Float(tracking=60)
weight = fields.Float(tracking=70)
sale_ok = fields.Boolean(tracking=80)
purchase_ok = fields.Boolean(tracking=90)
active = fields.Boolean(tracking=100)

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="product_attribute_values_button" model="ir.ui.view">
<field name="model">product.attribute</field>
<field name="inherit_id" ref="product.product_attribute_view_form" />
<field name="arch" type="xml">
<group name="main_fields" position='before'>
<div class="oe_button_box" name="button_box">
<button
name="show_values_ids"
type="object"
class="oe_stat_button"
icon="fa-tasks"
>
<field
name="values_count"
widget="statinfo"
string="Attribute Values"
/>
</button>
</div>
</group>
</field>
</record>
<record model="ir.ui.view" id="product_attribute_value_view_tree">
<field name="name">product.attribute.value.view.tree</field>
<field name="model">product.attribute.value</field>
<field name="arch" type="xml">
<tree string="Attributes Values">
<field name="name"/>
<field name="is_custom"/>
</tree>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="product_category_form_view" model="ir.ui.view">
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view" />
<field name="arch" type="xml">
<sheet position="after">
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</sheet>
</field>
</record>
</odoo>

View File

@@ -12,7 +12,7 @@
<field name="inherit_id" ref="product.product_supplierinfo_search_view"/>
<field name="arch" type="xml">
<field name="product_tmpl_id" position="after">
<field name="product_code"/>
<field name="product_name" filter_domain="['|', ('product_code', 'ilike', self), ('product_name', 'ilike', self)]" />
</field>
</field>
</record>

View File

@@ -23,7 +23,6 @@ Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for
'purchase_usability',
],
'data': [
'views/purchase_order_views.xml',
'views/stock_picking.xml',
],
'installable': True,

View File

@@ -1,111 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_stock_usability
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-02 09:56+0000\n"
"PO-Revision-Date: 2021-11-02 09:56+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_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__picking_type_id
msgid "Deliver To"
msgstr "Livrer à"
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__display_name
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__received
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__received
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "Fully Received"
msgstr "Entièrement reçue"
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__id
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__id
msgid "ID"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__incoterm_id
msgid "Incoterm"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,help:purchase_stock_usability.field_purchase_order__incoterm_id
msgid ""
"International Commercial Terms are a series of predefined commercial terms "
"used in international transactions."
msgstr ""
"Les Incoterms sont une série prédéfinie de termes commerciaux utilisés dans "
"les transactions internationales."
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order____last_update
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__no
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__no
msgid "Nothing to Receive"
msgstr "Rien à recevoir"
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__partially_received
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__partially_received
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "Partially Received"
msgstr "Partiellement reçue"
#. module: purchase_stock_usability
#: model:ir.model,name:purchase_stock_usability.model_purchase_order
msgid "Purchase Order"
msgstr "Commande fournisseur"
#. module: purchase_stock_usability
#: model:ir.model,name:purchase_stock_usability.model_purchase_order_line
msgid "Purchase Order Line"
msgstr "Ligne de commande fournisseur"
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__cancel
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__cancel
msgid "Receipt Cancelled"
msgstr "Réception annulée"
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__picking_status
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__move_status
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
msgid "Reception Status"
msgstr "État de réception"
#. module: purchase_stock_usability
#: model:ir.model.fields,help:purchase_stock_usability.field_purchase_order__picking_type_id
msgid "This will determine operation type of incoming shipment"
msgstr "Cela déterminera le type d'opération des réceptions"
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__to_receive
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__to_receive
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "To Receive"
msgstr "À recevoir"

View File

@@ -1,109 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_stock_usability
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-02 09:56+0000\n"
"PO-Revision-Date: 2021-11-02 09:56+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_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__picking_type_id
msgid "Deliver To"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__display_name
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__display_name
msgid "Display Name"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__received
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__received
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "Fully Received"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__id
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__id
msgid "ID"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__incoterm_id
msgid "Incoterm"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,help:purchase_stock_usability.field_purchase_order__incoterm_id
msgid ""
"International Commercial Terms are a series of predefined commercial terms "
"used in international transactions."
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order____last_update
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line____last_update
msgid "Last Modified on"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__no
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__no
msgid "Nothing to Receive"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__partially_received
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__partially_received
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "Partially Received"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model,name:purchase_stock_usability.model_purchase_order
msgid "Purchase Order"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model,name:purchase_stock_usability.model_purchase_order_line
msgid "Purchase Order Line"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__cancel
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__cancel
msgid "Receipt Cancelled"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order__picking_status
#: model:ir.model.fields,field_description:purchase_stock_usability.field_purchase_order_line__move_status
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
msgid "Reception Status"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields,help:purchase_stock_usability.field_purchase_order__picking_type_id
msgid "This will determine operation type of incoming shipment"
msgstr ""
#. module: purchase_stock_usability
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order__picking_status__to_receive
#: model:ir.model.fields.selection,name:purchase_stock_usability.selection__purchase_order_line__move_status__to_receive
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase_stock_usability.purchase_order_view_search
msgid "To Receive"
msgstr ""

View File

@@ -6,95 +6,21 @@ from odoo import api, fields, models
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
_inherit = 'purchase.order'
picking_type_id = fields.Many2one(tracking=True)
incoterm_id = fields.Many2one(tracking=True)
picking_status = fields.Selection(
[
("received", "Fully Received"),
("partially_received", "Partially Received"),
("to_receive", "To Receive"),
("cancel", "Receipt Cancelled"),
("no", "Nothing to Receive"),
],
string="Reception Status",
compute="_compute_picking_status",
store=True,
default="no",
)
@api.depends("state", "picking_ids.state")
def _compute_picking_status(self):
for order in self:
line_ids = order.order_line
order.picking_status = line_ids.get_move_status()
# inherit compute method of the field delivery_partner_id
# defined in purchase_usability
@api.depends("dest_address_id", "picking_type_id")
@api.depends('dest_address_id', 'picking_type_id')
def _compute_delivery_partner_id(self):
for o in self:
delivery_partner_id = False
if o.dest_address_id:
delivery_partner_id = o.dest_address_id
elif (
o.picking_type_id.warehouse_id
and o.picking_type_id.warehouse_id.partner_id
):
o.picking_type_id.warehouse_id and
o.picking_type_id.warehouse_id.partner_id):
delivery_partner_id = o.picking_type_id.warehouse_id.partner_id
o.delivery_partner_id = delivery_partner_id
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
move_status = fields.Selection(
[
("received", "Fully Received"),
("partially_received", "Partially Received"),
("to_receive", "To Receive"),
("cancel", "Receipt Cancelled"),
("no", "Nothing to Receive"),
],
string="Reception Status",
compute="_compute_move_status",
store=True,
default="no",
)
def get_move_status(self):
"""
Returns the reception status of the related lines stock moves.
Possible statuses:
- no: if the PO is not in status 'purchase' nor 'done', we consider that
there is nothing to receive. This is also the default value if the
conditions of no other status is met.
- cancel: all stock moves are cancelled
- received: if all stock moves are done or cancel.
- partially_received: If at least one stock move is done.
- to_receive: if all stock moves are in confirmed, assigned, waiting or
cancel state.
"""
move_status = "no"
mstates = self.move_ids.mapped("state")
if all([state == "cancel" for state in mstates]):
move_status = "cancel"
elif all([state in ("done", "cancel") for state in mstates]):
move_status = "received"
elif any([state == "done" for state in mstates]):
move_status = "partially_received"
elif all(
[
state in ("confirmed", "assigned", "waiting", "cancel")
for state in mstates
]
):
move_status = "to_receive"
return move_status
@api.depends("state", "move_ids.state")
def _compute_move_status(self):
for line in self:
line.move_status = line.get_move_status()

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_order_view_tree" model="ir.ui.view">
<field name="name">purchase.order.view.tree (in purchase_stock_usability)</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="picking_status" decoration-success="picking_status == 'received'" decoration-info="picking_status == 'to_receive'" decoration-warning="picking_status == 'partially_received'" decoration-danger="picking_status == 'cancel'" widget="badge" optional="show"/>
</xpath>
</field>
</record>
<record id="purchase_order_view_search" model="ir.ui.view">
<field name="name">purchase.order.select (in purchase_stock_usability)</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_view_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='order_date']" position="before">
<filter name="received" string="Pickings fully received" domain="[('picking_status', '=', 'received')]"/>
<filter name="partially_received" string="Pickings partially received" domain="[('picking_status', '=', 'partially_received')]"/>
<filter name="to_receive" string="Pickings to receive" domain="[('picking_status', '=', 'to_receive')]"/>
<separator/>
</xpath>
</field>
</record>
<record id="purchase_order_line_tree" model="ir.ui.view">
<field name="name">purchase.order.line.view.tree (in purchase_stock_usability)</field>
<field name="model">purchase.order.line</field>
<field name="inherit_id" ref="purchase.purchase_order_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<field name="move_status" decoration-success="move_status == 'received'" decoration-info="move_status == 'to_receive'" decoration-warning="move_status == 'partially_received'" decoration-danger="move_status == 'cancel'" widget="badge" optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,185 @@
# 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 ""
#. 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 "Êtes-vous sûr de vouloir annuler cette commande?"
#. module: purchase_usability
#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter
msgid "Billing Status"
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 "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"
#. 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,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,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 ""
#. module: purchase_usability
#: model:ir.model,name:purchase_usability.model_product_template
msgid "Product Template"
msgstr "Modèle de produit"
#. 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 d'achat"
#. 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 ""
#. 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"

View File

@@ -73,3 +73,28 @@ class PurchaseOrderLine(models.Model):
# for optional display in tree view
product_barcode = fields.Char(related='product_id.barcode', string="Product Barcode")
product_supplier_code = fields.Char(
compute='_compute_product_supplier_code', string='Vendor Product Code')
def _compute_product_supplier_code(self):
for line in self:
code = False
if not line.display_type and line.product_id and line.order_id:
partner_id = line.order_id.partner_id.commercial_partner_id.id
if partner_id:
for supplier_info in line.product_id.seller_ids:
if supplier_info.name.id == partner_id:
code = supplier_info.product_code
break
line.product_supplier_code = code
def _get_product_purchase_description(self, product_lang):
# This is useful when you want to have the product code in a dedicated
# column in your purchase order report
# The same ir.config_parameter is used in sale_usability,
# purchase_usability and account_usability
no_product_code_param = self.env['ir.config_parameter'].sudo().get_param(
'usability.line_name_no_product_code')
if no_product_code_param and no_product_code_param == 'True':
product_lang = product_lang.with_context(display_default_code=False)
return super()._get_product_purchase_description(product_lang)

View File

@@ -35,6 +35,7 @@
<attribute name="groups">analytic.group_analytic_tags</attribute>
</xpath>
<xpath expr="//field[@name='order_line']/tree//field[@name='product_id']" position="after">
<field name="product_supplier_code" optional="hide"/>
<field name="product_barcode" optional="hide"/>
</xpath>
</field>

View File

@@ -1,29 +0,0 @@
# 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,
}

View File

@@ -1,33 +0,0 @@
# 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}"

View File

@@ -1,33 +0,0 @@
# 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 ""

View File

@@ -1,63 +0,0 @@
<?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>

View File

@@ -12,6 +12,7 @@
'website': 'http://www.akretion.com',
'depends': [
'sale',
'account_usability', # for company view
'base_view_inheritance_extension',
],
'data': [
@@ -20,6 +21,7 @@
'views/sale_report.xml',
'views/product_pricelist_item.xml',
'views/account_move.xml',
'views/res_company.xml',
],
'installable': True,
}

View File

@@ -2,3 +2,4 @@ from . import sale_order
from . import account_move
from . import product_template
from . import res_partner
from . import res_company

View File

@@ -0,0 +1,13 @@
# Copyright 2021 Akretion France (https://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 ResCompany(models.Model):
_inherit = 'res.company'
# Similar to the field static_invoice_terms in account_usability
static_sale_terms = fields.Text(
translate=True, string="Legal Terms on Quotation")

View File

@@ -124,3 +124,14 @@ class SaleOrderLine(models.Model):
self.env, new_price, currency_obj=pricelist.currency_id))
}
return res
def get_sale_order_line_multiline_description_sale(self, product):
# This is useful when you want to have the product code in a dedicated
# column in your sale order report
# The same ir.config_parameter is used in sale_usability,
# purchase_usability and account_usability
no_product_code_param = self.env['ir.config_parameter'].sudo().get_param(
'usability.line_name_no_product_code')
if no_product_code_param and no_product_code_param == 'True':
product = product.with_context(display_default_code=False)
return super().get_sale_order_line_multiline_description_sale(product)

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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).
-->
<odoo>
<record id="view_company_form" model="ir.ui.view">
<field name="name">sale_usability.res.company.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="account_usability.view_company_form"/>
<field name="arch" type="xml">
<group name="static_invoice_terms" position="after">
<group name="static_sale_terms" string="Quotation Legal Terms">
<field name="static_sale_terms" nolabel="1"/>
</group>
</group>
</field>
</record>
</odoo>

View File

@@ -54,4 +54,9 @@
</field>
</record>
<record id="sale.action_order_report_all" model="ir.actions.act_window">
<!-- native order is graph,pivot -->
<field name="view_mode">pivot,graph</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,27 @@
# 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,
}

View File

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

View File

@@ -0,0 +1,11 @@
# 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)

View File

@@ -22,5 +22,5 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'website': 'http://www.akretion.com',
'depends': ['stock'],
'data': ['view.xml'],
'installable': False,
'installable': True,
}

View File

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

View File

@@ -0,0 +1,24 @@
# 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": [],
}

View File

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

View File

@@ -0,0 +1,36 @@
# 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

View File

@@ -0,0 +1,17 @@
<?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>
</field>
</record>
</odoo>

View File

@@ -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"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" optional="show"/>
<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}"/>
</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>

View File

@@ -1 +1,2 @@
from . import models
from . import wizard

View File

@@ -1,11 +1,11 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# 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).
{
'name': 'Stock Valuation XLSX',
'version': '12.0.1.0.0',
'version': '14.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Generate XLSX reports for past or present stock levels',
@@ -37,8 +37,11 @@ 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': False,
'installable': True,
}

View File

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

View File

@@ -0,0 +1,35 @@
# 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.'
)]

View File

@@ -0,0 +1,5 @@
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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 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
4 access_stock_valuation_xlsx stock.valuation.xlsx wizard model_stock_valuation_xlsx stock.group_stock_user 1 1 1 0
5 access_stock_variation_xlsx stock.variation.xlsx wizard model_stock_variation_xlsx stock.group_stock_user 1 1 1 0

View File

@@ -0,0 +1,35 @@
<?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>

View File

@@ -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, 'default_location_id': location_id}"/>
context="{'default_source': 'inventory', 'default_inventory_id': active_id}"/>
</button>
</field>
</record>

View File

@@ -1 +1,2 @@
from . import stock_valuation_xlsx
from . import stock_variation_xlsx

View File

@@ -4,6 +4,7 @@
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
@@ -16,59 +17,61 @@ 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)
export_file = fields.Binary(string='XLSX Report', readonly=True, attachment=True)
export_filename = fields.Char(readonly=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)
company_id = fields.Many2one(
'res.company', string='Company', default=lambda self: self.env.company,
required=True)
warehouse_id = fields.Many2one(
'stock.warehouse', string='Warehouse',
states={'done': [('readonly', True)]})
'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'))],
default=lambda self: self._default_location(),
states={'done': [('readonly', 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 "
u"be taken in the valuation.")
"be taken in the valuation.")
categ_ids = fields.Many2many(
'product.category', string='Product Categories',
states={'done': [('readonly', True)]})
'product.category', string='Product Category Filter',
help="Leave this field empty to have a stock valuation for all your products.",
)
source = fields.Selection([
('inventory', 'Physical Inventory'),
('stock', 'Stock Levels'),
], string='Source data', default='stock', required=True,
states={'done': [('readonly', True)]})
], string='Source data', default='stock', required=True)
inventory_id = fields.Many2one(
'stock.inventory', string='Inventory', domain=[('state', '=', 'done')],
states={'done': [('readonly', True)]})
'stock.inventory', string='Inventory', check_company=True,
domain="[('state', '=', 'done'), ('company_id', '=', company_id)]")
stock_date_type = fields.Selection([
('present', 'Present'),
('past', 'Past'),
], string='Present or Past', default='present',
states={'done': [('readonly', True)]})
], string='Present or Past', default='present')
past_date = fields.Datetime(
string='Past Date', states={'done': [('readonly', True)]},
default=fields.Datetime.now)
string='Past Date', default=fields.Datetime.now)
categ_subtotal = fields.Boolean(
string='Subtotals per Categories', default=True,
states={'done': [('readonly', True)]},
help="Show a subtotal per product category")
help="Show a subtotal per product category.")
standard_price_date = fields.Selection([
('past', 'Past Date or Inventory Date'),
('present', 'Current'),
], default='past', string='Cost Price Date',
states={'done': [('readonly', True)]})
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)]})
], 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
@api.model
def _default_location(self):
@@ -123,28 +126,41 @@ 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:
# if not standard_price_past_date: # TODO
if True:
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:
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
# 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
else:
standard_price = p['standard_price']
product_id2data[p['id']] = {'standard_price': standard_price}
@@ -156,38 +172,56 @@ class StockValuationXlsx(models.TransientModel):
logger.debug('End compute_product_data')
return product_id2data
def id2name(self, product_ids):
logger.debug('Start id2name')
@api.model
def product_categ_id2name(self, categories):
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 self.categ_ids:
categ_domain = [('id', 'child_of', self.categ_ids.ids)]
if categories:
categ_domain = [('id', 'child_of', categories.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 hasattr(splo, 'expiry_date'):
if has_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']
logger.debug('End id2name')
return categ_id2name, uom_id2name, lot_id2data, loc_id2name
return loc_id2name
def compute_data_from_inventory(self, product_ids, prec_qty):
self.ensure_one()
@@ -275,7 +309,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):
uom_id2name, lot_id2data, loc_id2name, apply_depreciation):
logger.debug('Start stringify_and_sort_result')
res = []
for l in data:
@@ -284,17 +318,27 @@ class StockValuationXlsx(models.TransientModel):
standard_price = float_round(
product_id2data[product_id]['standard_price'],
precision_digits=prec_price)
subtotal = float_round(
subtotal_before_depreciation = 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']],
))
@@ -305,14 +349,19 @@ 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.env.user.company_id
company = self.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."))
@@ -335,15 +384,25 @@ 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, uom_id2name, lot_id2data, loc_id2name = self.id2name(product_ids)
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)
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)
categ_id2name, uom_id2name, lot_id2data, loc_id2name, apply_depreciation)
logger.debug('Start create XLSX workbook')
file_data = BytesIO()
@@ -356,12 +415,15 @@ class StockValuationXlsx(models.TransientModel):
if not split_by_lot:
cols.pop('lot_name', None)
cols.pop('expiry_date', None)
if not hasattr(splo, 'expiry_date'):
if not self.has_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']):
@@ -417,6 +479,9 @@ 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:
@@ -432,12 +497,20 @@ class StockValuationXlsx(models.TransientModel):
total += l['subtotal']
ctotal += l['subtotal']
categ_has_line = True
subtotal_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1)
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
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' and l[col_name]:
l[col_name] = fields.Date.from_string(l[col_name])
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
sheet.write(i, col['pos'], l[col_name], styles[col['style']])
if categ_subtotal:
if categ_has_line:
@@ -460,21 +533,17 @@ 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': 'self',
# }
action = self.env['ir.actions.act_window'].for_xml_id(
'stock_valuation_xlsx', 'stock_valuation_xlsx_action')
action['res_id'] = self.id
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',
}
return action
def _prepare_styles(self, workbook, company, prec_price):
@@ -482,8 +551,8 @@ class StockValuationXlsx(models.TransientModel):
categ_bg_color = '#e1daf5'
col_title_bg_color = '#fff9b4'
regular_font_size = 10
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)
currency_num_format = '# ### ##0.00 %s' % company.currency_id.symbol
price_currency_num_format = '# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol)
styles = {
'doc_title': workbook.add_format({
'bold': True, 'font_size': regular_font_size + 10,
@@ -503,6 +572,7 @@ 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({
@@ -527,8 +597,10 @@ 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': {'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')},
'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')},
}
return cols

View File

@@ -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="state" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="categ_ids" widget="many2many_tags"/>
<field name="warehouse_id"/>
<field name="location_id"/>
@@ -27,18 +27,14 @@
<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')]}"/>
</group>
<group name="done" states="done" string="Result">
<field name="export_file" filename="export_filename"/>
<field name="export_filename" invisible="1"/>
<field name="apply_depreciation" groups="stock.group_production_lot" attrs="{'invisible': ['|', '|', ('split_by_lot', '=', False), ('has_expiry_date', '=', False), '&amp;', ('source', '=', 'stock'), ('stock_date_type', '=', 'past')]}"/>
</group>
<footer>
<button name="generate" type="object" states="setup"
class="btn-primary" string="Generate"/>
<button special="cancel" string="Cancel" class="btn-default" states="setup"/>
<button special="cancel" string="Close" class="btn-default" states="done"/>
<button name="generate" type="object" class="btn-primary" string="Generate"/>
<button special="cancel" string="Close" class="btn-default"/>
</footer>
</form>
</field>
@@ -55,6 +51,7 @@
<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>

View File

@@ -0,0 +1,458 @@
# 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

View File

@@ -0,0 +1,55 @@
<?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>

View File

@@ -0,0 +1,23 @@
# Copyright 2021 Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Web Tab Title',
'description': """
Automatically set tab document.title when empty.
Important limitation: the tab will get its title only once you browse it.
""",
'version': '14.0.1.0.0',
'license': 'AGPL-3',
'author': 'Akretion',
'website': 'akretion.com',
'depends': [
'web',
],
'data': [
],
'demo': [
],
"data": ["views/web_tab_title.xml"],
"maintainers": ["rvalyi"],
}

View File

@@ -0,0 +1,29 @@
/* global vis, py */
odoo.define("web_tab_title.AbstractWebClient", function (require) {
"use strict";
var AbstractWebClient = require('web.AbstractWebClient');
var TabTitleAbstractWebClient = AbstractWebClient.include({
_title_changed: function () {
// like the original except we change the title
// only when it's different from "Odoo" to avoid
// resetting the tab title when switching tabs.
var parts = _.sortBy(_.keys(this.get("title_part")), function (x) { return x; });
var tmp = "";
_.each(parts, function (part) {
var str = this.get("title_part")[part];
if (str) {
tmp = tmp ? tmp + " - " + str : str;
}
}, this);
if (tmp != "Odoo") {
document.title = tmp;
}
},
});
return TabTitleAbstractWebClient;
});

View File

@@ -0,0 +1,31 @@
/* global vis, py */
odoo.define("web_tab_title.FormController", function (require) {
"use strict";
var FormController = require('web.FormController');
var TabTitleController = FormController.include({
on_attach_callback: function () {
this._super.apply(this, arguments);
if (document.title == "Odoo") {
var form_name_elem = $("div.oe_title>h1");
if (form_name_elem.length == 0) {
form_name_elem = $('span.o_field_char[name="name"]')
}
var title = form_name_elem.text();
if (title !== '') {
// alternatively we could access the record
// in views/basic/basic_model.js
// but we would also we miss the model name
document.title = title + " - Odoo";
}
}
},
});
return TabTitleController;
});

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template
id="assets_backend"
name="web_timeline assets"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/web_tab_title/static/src/js/form_controller.js"
/>
<script
type="text/javascript"
src="/web_tab_title/static/src/js/abstract_web_client.js"
/>
</xpath>
</template>
</odoo>