Compare commits

...

81 Commits

Author SHA1 Message Date
Chafique
b57485cf7c [FIX] call super() method in send_get_mail_to function 2020-10-02 09:58:30 +02:00
Chafique
20dcf1a333 [FIX] README.rst 2020-10-01 18:14:22 +02:00
Chafique
7c7cb2e8d2 [ADD] mail_single_send_several_recipients module 2020-09-28 12:06:38 +02:00
Alexis de Lattre
a748de39ec [FIX] stock_valuation_xlsx: fix report when categ_subtotal is false 2020-09-26 00:01:27 +02:00
Alexis de Lattre
446c2c1f1d stock_valuation_xlsx: Improve perfs
Add ability to force cost price to current
Improve headers in XLSX
2020-09-25 22:50:20 +02:00
Alexis de Lattre
063924fdc7 stock_valuation_xlsx: Speed-up for past valuation when few of the total products are in stock
Closer to PEP8
2020-09-25 12:19:26 +02:00
Alexis de Lattre
1e4de02259 stock_valuation_xlsx: Improve module description 2020-09-22 23:57:38 +02:00
Alexis de Lattre
4b432ec207 stock_valuation_xlsx: add possibility to add custom products fields in report 2020-09-22 15:25:54 +02:00
Alexis de Lattre
1b4f9dfab1 stock_valuation_xlsx: Add button on inventory form
Display expiry date as date
Add debug messages
Don't hardcode currency in style
Use decimal precision for price too
2020-09-22 10:42:45 +02:00
Alexis de Lattre
3043ad11a8 stock_inventory_xlsx: small fixes 2020-09-22 00:15:07 +02:00
Alexis de Lattre
de5470e5bd Add module stock_valuation_xlsx
stock_inventory_valuation_ods: fix module description
2020-09-21 23:36:02 +02:00
Chafique
31483abb99 add mail_follower_option module 2020-07-17 11:38:22 +02:00
Alexis de Lattre
6377f0984d stock_usability: Add 'set qty to 0' button on return wizard 2020-07-06 17:26:31 +02:00
Alexis de Lattre
284f0a1e73 partner_products_shortcut: don't use truck logo for something that doesn't point to a picking, use list logo instead 2020-07-06 11:03:16 +02:00
Alexis de Lattre
da81278b17 Add archive filter 2020-05-26 22:38:38 +02:00
Alexis de Lattre
f792979456 Add index=True on domain 2020-05-26 17:11:07 +02:00
Alexis de Lattre
d9a598a3c4 Add new module base_dynamic_list 2020-05-26 17:09:53 +02:00
Alexis de Lattre
2b74514230 mass_mailing_usability: Fix dependencies 2020-04-24 09:27:40 +02:00
Alexis de Lattre
38f1eacf8f sale_stock_usability: add methods for report
move form view in picking: add date field
account_usability: amount in tax lines readonly on customer invoices
2020-03-06 18:33:49 +01:00
Alexis de Lattre
aa9ab68ca3 Add incoterm in sale config menu, because incoterms is managed by sales manager in 99% of companies 2020-03-05 19:44:50 +01:00
Alexis de Lattre
97a83b0615 stock_usability: Add compute_sudo=True on some related fields of stock.inventory.line to avoid ACL issues 2020-01-29 22:37:36 +01:00
Alexis de Lattre
f6642639cf one2many_phone: Workaround a bug "Record does not exist or has been deleted." 2020-01-27 23:32:38 +01:00
Alexis de Lattre
cc11aca053 one2many_phone: Update search view 2020-01-27 21:50:04 +01:00
Alexis de Lattre
104ae274e9 Update translation for base_partner_one2many_phone 2020-01-27 19:03:52 +01:00
Alexis de Lattre
2baf9167a4 Add mobile in partner tree view 2020-01-13 09:57:10 +01:00
Alexis de Lattre
af6550fd2b base_partner_one2many_phone: improve mig script, add form view for res.partner.phone 2020-01-10 16:35:03 +01:00
Alexis de Lattre
bb23254830 Big update of base_partner_one2many_phone: new types, add email support
Migration script provided
2020-01-10 16:02:03 +01:00
Alexis de Lattre
cc0da43bdc commission_simple: improve view 2019-12-10 00:16:21 +01:00
Alexis de Lattre
57236ba173 Small fixes in commission modules 2019-12-10 00:08:39 +01:00
Alexis de Lattre
80480c99cc commission_simple: fix error in sql constraint 2019-12-09 23:01:06 +01:00
Alexis de Lattre
edb93dda3d Add module commission_simple and commission_simple_sale
Improve view inheritance in account_usability
2019-12-09 22:52:33 +01:00
Alexis de Lattre
9020ab18f6 Remove _rec_name from mrp.bom because there is now a native name_get() 2019-12-02 11:37:41 +01:00
Alexis de Lattre
01cfcbf80d sale_order_add_bom: fix related field definition 2019-12-02 11:21:59 +01:00
Alexis de Lattre
69f283f387 Add group on wizard entry 2019-11-28 17:10:27 +01:00
Alexis de Lattre
e884489c9b Add mass backtodraft wizard on account.move 2019-11-28 17:08:09 +01:00
Alexis de Lattre
4eb7969264 base_partner_ref: proper invalidation for display_name 2019-10-29 15:39:47 +01:00
Alexis de Lattre
a8019b2c80 Add option "hide_bank_statement_balance" on account.journal 2019-09-06 23:45:31 +02:00
Alexis de Lattre
f616e23985 Add code in journal tree view
Remove inherit of name_search, because it was already native in a
slightly different implementation
2019-08-07 11:31:49 +02:00
Alexis de Lattre
651cd27118 Merge branch '10.0' of github.com:akretion/odoo-usability into 10.0 2019-06-20 23:27:47 +02:00
Alexis de Lattre
acdddf0d08 Improve views for sale.report and purchase.report, and search view for sale order 2019-06-20 23:27:12 +02:00
David Beal
c6cb2e197f Merge pull request #95 from MindAndGo/10.0-bad-menu-reference
[10.0]FIX : Bad menu used
2019-06-17 16:51:30 +02:00
Florent THOMAS
363f781acf Cahnge the menu used 2019-06-12 16:48:24 +02:00
Pierrick Brun
1364748052 Merge pull request #88 from akretion/10-account_report_qweb_horizontal
[IMP] 10.0 account report qweb horizontal
2019-06-11 10:21:21 +02:00
Alexis de Lattre
73ee7d4dec Add module mass_mailing_usability
Improve module link_tracker_usability
2019-05-24 19:12:29 +02:00
Alexis de Lattre
5f207046fb New module link_tracker_usability 2019-05-17 20:27:52 +02:00
Alexis de Lattre
b4dfab1bf9 "Show Inventory Lines" renamed to "Inventory Lines" 2019-05-13 23:21:25 +02:00
Alexis de Lattre
e56343c181 account_move_line_filter_wizard: improve module description
Update version number
2019-05-13 20:02:52 +02:00
Alexis de Lattre
a61f1e385a account_move_line_filter_wizard: improve to add direct access to general ledger or open items report 2019-05-13 18:56:13 +02:00
Alexis de Lattre
d9c340e513 stock_usability: add link to Inventory Lines on product form view (menu "Action") 2019-05-13 16:10:20 +02:00
Alexis de Lattre
ba68bbecda crm_partner_prospect: avoid post-write -> improve perf 2019-05-10 23:57:00 +02:00
Alexis de Lattre
794d28468b Fix context in action for prospect 2019-05-10 23:49:46 +02:00
Alexis de Lattre
2dad83b91e Improve search and pivot view of account.move.line 2019-04-29 18:39:16 +02:00
Alexis de Lattre
aa2f90f0d1 account_usability: add script _fix_debit_credit_round_bug 2019-04-06 00:48:26 +02:00
Alexis de Lattre
16e550d0d5 base_usability: Add new options to _display_full_address() 2019-04-01 18:11:17 +02:00
Pierrick Brun
5a95b771f9 Merge pull request #87 from akretion/10-account_view_search_deb_cred
[IMP] 10.0 account.move.line: Add filter on both debit and credit
2019-04-01 11:47:53 +02:00
Pierrick Brun
d4d0666349 [IMP] make PDF reports horizontal
Pas pour la balance
2019-03-20 10:45:21 +01:00
Alexis de Lattre
8180da1d6e FIX crash in account_invoice_margin 2019-03-03 21:45:24 +01:00
Alexis de Lattre
aec54baec5 Improve list view of res.partner.bank
Improve name_get of res.bank
Add filter on product categ on pos_sale_report
2019-02-27 17:30:36 +01:00
Alexis de Lattre
e33bee1f05 account_usability: improve create group script 2019-02-27 10:47:08 +01:00
Alexis de Lattre
6f0f0b0a0d Private methods for scripts
Use sudo() in scripts that require admin access
2019-02-25 22:02:05 +01:00
Sébastien BEAU
6ce749c70f [FIX] fix helper 2019-02-25 15:57:56 +01:00
Sébastien BEAU
27fa44c68f [IMP] by default do not send an email when user_id is fill on object 2019-02-19 23:05:32 +01:00
Alexis de Lattre
6ecc01c108 On product.template+product.categ, all accounting fields are groups="account.group_account_invoice" 2019-02-15 14:53:39 +01:00
Alexis de Lattre
b2cda5e522 account_usability: remove access to form view of invoice lines via dedicated action until we have a proper readonly system for non-draft invoices 2019-02-15 00:13:34 +01:00
Alexis de Lattre
351fc2038d account_usability: hide "Accounting > Configuration > Financial Reports" 2019-02-14 20:53:14 +01:00
Alexis de Lattre
9505c2d5cd purchase_usability: purchase report is now a 3rd level menu entry like almost all clickable menu entries 2019-02-14 20:48:30 +01:00
Alexis de Lattre
e9b56ce5dd Add multi-company ir.rule for crm.lead 2019-02-14 20:34:21 +01:00
Alexis de Lattre
55b044a1fc Add module account_financial_report_qweb_usability 2019-02-14 18:00:55 +01:00
Alexis de Lattre
8a8780b810 account_usability: fix onchange on amount_currency: invert debit and credit 2019-02-14 17:43:48 +01:00
Alexis de Lattre
6b3d2263c7 account_usability: Improve onchange for amount_currency 2019-02-14 17:31:17 +01:00
Hpar
2b89e57d72 Merge pull request #84 from akretion/10.0-fix-purchase_order_buyer
[10][FIX] purchase_ordre_buyer
2019-02-13 11:17:41 +01:00
hparfr
9477219b71 Call super in onchanger() 2019-02-13 11:15:59 +01:00
Alexis de Lattre
d191aadc4e Add button to view the items of a pricelist full screen, to be able to search on items 2019-02-05 23:15:24 +01:00
Alexis de Lattre
ff97e81105 Hide default_credit and default_debit, now that it works well 2019-02-04 18:06:41 +01:00
Alexis de Lattre
286b80bf95 stock_usability: code cleanup 2019-02-04 15:33:08 +01:00
Alexis de Lattre
5775ad651f Fix string 2019-01-30 19:39:23 +01:00
Adrien Peiffer
bec0a50a72 [IMP] Journal entry should be visible in done state. (#50) 2019-01-29 10:50:24 +01:00
Florent Jouatte
99cacec19d [FIX] 'hr_expense_usability.generic_private_car_expense' does not exist (#76)
* [FIX] 'hr_expense_usability.generic_private_car_expense' does not exist

* [FIX] wrong xmlid, replace the module name by the right one
2019-01-29 10:47:31 +01:00
Pierrick Brun
1913556368 account.move.line: Add filter on both debit and credit (#82)
picked from @alexis-via (d4fcaa7d14)
2019-01-15 10:09:17 +01:00
Sébastien BEAU
ea402d4cbe [REF] refactor the code in order to split it in several file 2019-01-11 11:10:45 +01:00
Sébastien BEAU
a2564f48a3 [IMP] add record_id on mail.message to be able to access to the record 2019-01-11 10:56:31 +01:00
135 changed files with 3964 additions and 394 deletions

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import wizard

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# © 2015-2016 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Account Financial Report Qweb Usability',
'version': '10.0.1.0.0',
'category': 'Accounting & Finance',
'license': 'AGPL-3',
'summary': 'Small usability enhancements in account_financial_report_qweb module',
'description': """
Account Financial Report Usability
==================================
The usability enhancements include:
TODO
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
'account_financial_report_qweb',
],
'data': [
'views/reports.xml',
'views/layouts.xml',],
'installable': True,
}

View File

@@ -0,0 +1,3 @@
.list_table, .data_table, .totals_table, .list_table .act_as_row {
font-size:15px;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_specific" inherit_id="account_financial_report_qweb.assets_specific">
<xpath expr="." position="inside">
<link href="/account_financial_report_qweb_usability/static/src/css/reports.css" rel="stylesheet"/>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="report_qweb_paperformat_horizontal" model="report.paperformat">
<field name="name">Account financial report qweb horizontal paperformat</field>
<field name="default" eval="True"/>
<field name="format">custom</field>
<field name="page_height">297</field>
<field name="page_width">210</field>
<field name="orientation">Landscape</field>
<field name="margin_top">12</field>
<field name="margin_bottom">8</field>
<field name="margin_left">5</field>
<field name="margin_right">5</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">10</field>
<field name="dpi">110</field>
</record>
<record id="account_financial_report_qweb.action_report_general_ledger_qweb" model="ir.actions.report.xml">
<field name="paperformat_id" ref="report_qweb_paperformat_horizontal"/>
</record>
<record id="account_financial_report_qweb.action_report_journal_qweb" model="ir.actions.report.xml">
<field name="paperformat_id" ref="report_qweb_paperformat_horizontal"/>
</record>
<record id="account_financial_report_qweb.action_report_open_items_qweb" model="ir.actions.report.xml">
<field name="paperformat_id" ref="report_qweb_paperformat_horizontal"/>
</record>
</odoo>

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import general_ledger_wizard
from . import open_items_wizard

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class GeneralLedgerReportWizard(models.TransientModel):
_inherit = 'general.ledger.report.wizard'
foreign_currency = fields.Boolean(default=False)
def onchange_partner_ids(self):
# Neutralize native onchange method
return

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class OpenItemsReportWizard(models.TransientModel):
_inherit = "open.items.report.wizard"
foreign_currency = fields.Boolean(default=False)

View File

@@ -83,8 +83,9 @@ class AccountInvoiceLine(models.Model):
std_price = pp.standard_price
inv_uom_id = vals.get('uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
std_price = self.env['product.uom']._compute_price(
pp.uom_id.id, std_price, inv_uom_id)
inv_uom = self.env['product.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super(AccountInvoiceLine, self).create(vals)

View File

@@ -5,7 +5,7 @@
{
'name': 'Account Move Line Filter Wizard',
'version': '10.0.1.0.0',
'version': '10.0.2.0.0',
'category': 'Accounting',
'license': 'AGPL-3',
'summary': 'Easy and fast access to the details of an account',
@@ -13,13 +13,21 @@
Account Move Line Filter Wizard
===============================
This module adds a wizard in Accounting > ... >
This module adds a *Show Account* wizard under *Accounting > Adviser*. This wizard gives an easy and fast access to the details of an account:
* access to the General Ledger Report,
* access to the Open Items Report (if the user selected a reconciliable account and the Unreconciled filter),
* access to the Journal Items view.
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['account_usability'],
'depends': [
'account_usability',
'account_financial_report_qweb',
'account_fiscal_year',
],
'data': ['wizard/account_move_line_filter_view.xml'],
'installable': True,
}

View File

@@ -3,13 +3,16 @@
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AccountMoveLineFilterWizard(models.TransientModel):
_name = 'account.move.line.filter.wizard'
_description = 'Wizard for easy and fast access to account move lines'
date_range_id = fields.Many2one(
'date.range', string='Date Range (only for General Ledger)')
partner_id = fields.Many2one(
'res.partner', string='Partner', domain=[('parent_id', '=', False)])
account_id = fields.Many2one(
@@ -23,6 +26,27 @@ class AccountMoveLineFilterWizard(models.TransientModel):
# ('partial_reconciled', 'Partially Reconciled'),
], string='Reconciliation Filter')
@api.model
def default_get(self, fields_list):
res = super(AccountMoveLineFilterWizard, self).default_get(fields_list)
today = fields.Date.context_today(self)
fy_type_id = self.env.ref('account_fiscal_year.fiscalyear').id
dro = self.env['date.range']
date_range = dro.search([
('type_id', '=', fy_type_id),
('company_id', '=', self.env.user.company_id.id),
('date_start', '<=', today),
('date_end', '>=', today)
], limit=1)
if not date_range:
date_range = dro.search([
('type_id', '=', fy_type_id),
('company_id', '=', self.env.user.company_id.id),
], order='date_start desc', limit=1)
if date_range:
res['date_range_id'] = date_range.id
return res
@api.onchange('partner_id')
def partner_id_change(self):
if self.partner_id:
@@ -44,3 +68,33 @@ class AccountMoveLineFilterWizard(models.TransientModel):
if self.reconcile:
action['context']['search_default_%s' % self.reconcile] = True
return action
def show_report_general_ledger(self):
self.ensure_one()
if self.account_reconcile:
assert self.reconcile != 'unreconciled'
if not self.date_range_id:
raise UserError(_(
"Select a date range to show the General Ledger report."))
wvals = {
'account_ids': [(6, 0, [self.account_id.id])],
'date_from': self.date_range_id.date_start,
'date_to': self.date_range_id.date_end,
}
if self.partner_id:
wvals['partner_ids'] = [(6, 0, [self.partner_id.id])]
wiz = self.env['general.ledger.report.wizard'].create(wvals)
action = wiz.button_export_html()
return action
def show_report_open_items(self):
self.ensure_one()
assert self.account_reconcile and self.reconcile == 'unreconciled'
wvals = {
'account_ids': [(6, 0, [self.account_id.id])],
}
if self.partner_id:
wvals['partner_ids'] = [(6, 0, [self.partner_id.id])]
wiz = self.env['open.items.report.wizard'].create(wvals)
action = wiz.button_export_html()
return action

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016-2018 Akretion (http://www.akretion.com/)
Copyright (C) 2016-2019 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
@@ -12,16 +12,19 @@
<field name="name">account_move_line_filter_wizard_form</field>
<field name="model">account.move.line.filter.wizard</field>
<field name="arch" type="xml">
<form string="Account Move Lines">
<form string="Show Account">
<group name="filters" string="Filters">
<field name="partner_id"/>
<field name="account_id"/>
<field name="account_reconcile" invisible="1"/>
<field name="reconcile"
attrs="{'invisible': [('account_reconcile', '!=', True)]}"/>
<field name="date_range_id" attrs="{'invisible': [('account_reconcile', '=', True), ('reconcile', '=', 'unreconciled')]}"/>
</group>
<footer>
<button type="object" name="go" string="Go" class="btn-primary"/>
<button type="object" name="show_report_general_ledger" string="General Ledger Report" class="btn-primary" attrs="{'invisible': [('account_reconcile', '=', True), ('reconcile', '=', 'unreconciled')]}"/>
<button type="object" name="show_report_open_items" string="Open Items Report" class="btn-primary" attrs="{'invisible': ['|', ('account_reconcile', '=', False), ('reconcile', '!=', 'unreconciled')]}"/>
<button type="object" name="go" string="Journal Items" class="btn-primary"/>
<button special="cancel" string="Cancel" class="btn-default"/>
</footer>
</form>
@@ -29,7 +32,7 @@
</record>
<record id="account_move_line_filter_wizard_action" model="ir.actions.act_window">
<field name="name">Journal Items of Account</field>
<field name="name">Show Account</field>
<field name="res_model">account.move.line.filter.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>

View File

@@ -42,6 +42,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'partner_view.xml',
'product_view.xml',
'wizard/account_invoice_mark_sent_view.xml',
'wizard/account_move_backtodraft_view.xml',
],
'installable': True,
}

View File

@@ -4,7 +4,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo.tools import float_compare, float_is_zero
from odoo.tools import float_compare, float_is_zero, float_round
from odoo.tools.misc import formatLang
from odoo.exceptions import UserError, ValidationError
from odoo import SUPERUSER_ID
@@ -121,7 +121,8 @@ class AccountInvoice(models.Model):
lines.unlink()
return True
def fix_invoice_attachment_filename(self):
@api.model
def _fix_invoice_attachment_filename(self):
# This script is designed to fix attachment of invoices
# badly generated by Odoo v8. I found this problem in Nov 2018 at
# Encres Dubuit when investigating a bug where Odoo would create a
@@ -169,6 +170,15 @@ class AccountInvoiceLine(models.Model):
class AccountJournal(models.Model):
_inherit = 'account.journal'
hide_bank_statement_balance = fields.Boolean(
string='Hide Bank Statement Balance',
help="You may want to enable this option when your bank "
"journal is generated from a bank statement file that "
"doesn't handle start/end balance (QIF for instance) and "
"you don't want to enter the start/end balance manually: it "
"will prevent the display of wrong information in the accounting "
"dashboard and on bank statements.")
@api.multi
@api.depends(
'name', 'currency_id', 'company_id', 'company_id.currency_id', 'code')
@@ -187,20 +197,6 @@ class AccountJournal(models.Model):
res.append((journal.id, name))
return res
# Also search on start of 'code', not only on 'name'
@api.model
def name_search(
self, name='', args=None, operator='ilike', limit=80):
if args is None:
args = []
if name:
jrls = self.search(
[('code', '=ilike', name + '%')] + args, limit=limit)
if jrls:
return jrls.name_get()
return super(AccountJournal, self).name_search(
name=name, args=args, operator=operator, limit=limit)
@api.constrains('default_credit_account_id', 'default_debit_account_id')
def _check_account_type_on_bank_journal(self):
bank_acc_type = self.env.ref('account.data_account_type_liquidity')
@@ -240,19 +236,19 @@ class AccountAccount(models.Model):
return super(AccountAccount, self).name_get()
# https://github.com/odoo/odoo/issues/23040
def fix_bank_account_types(self):
aao = self.env['account.account']
@api.model
def _fix_bank_account_types(self):
companies = self.env['res.company'].search([])
if len(companies) > 1 and self.env.user.id != SUPERUSER_ID:
raise UserError(
"In multi-company setups, you should run this "
"script as admin user")
logger.info(
"Multi-company setup detected, running script with sudo ")
self = self.sudo()
logger.info("START the script 'fix bank and cash account types'")
bank_type = self.env.ref('account.data_account_type_liquidity')
asset_type = self.env.ref('account.data_account_type_current_assets')
journals = self.env['account.journal'].search(
[('type', 'in', ('bank', 'cash'))], order='company_id')
journal_accounts_bank_type = aao
journal_accounts_bank_type = self
for journal in journals:
for account in [
journal.default_credit_account_id,
@@ -266,7 +262,7 @@ class AccountAccount(models.Model):
account.company_id.display_name, account.code)
if account not in journal_accounts_bank_type:
journal_accounts_bank_type += account
accounts = aao.search([
accounts = self.search([
('user_type_id', '=', bank_type.id)], order='company_id, code')
for account in accounts:
if account not in journal_accounts_bank_type:
@@ -278,7 +274,7 @@ class AccountAccount(models.Model):
return True
@api.model
def create_account_groups(self, level=2, name_prefix=u'Comptes '):
def _create_account_groups(self, level=2, name_prefix=u'Comptes '):
'''Should be launched by a script. Make sure the account_group module is installed
(the account_usability module doesn't depend on it currently'''
assert level >= 1
@@ -296,7 +292,13 @@ class AccountAccount(models.Model):
accounts = self.search([])
struct = {'childs': {}}
for account in accounts:
assert len(account.code) > level
if len(account.code) <= level:
logger.warning(
"Account '%s' in company '%s' is smaller than "
"level (%d).",
account.display_name, account.company_id.display_name,
level)
continue
n = 1
parent = struct
gparent = False
@@ -367,6 +369,58 @@ class AccountMove(models.Model):
move.default_credit = default_credit
move.default_debit = default_debit
@api.model
def _fix_debit_credit_round_bug(self):
logger.info('START script _fix_debit_credit_round_bug')
moves = self.sudo().search([]) # sudo to search in all companies
bug_move_ids = []
for move in moves:
buggy = False
for l in move.line_ids:
if not float_is_zero(l.debit, precision_digits=2):
debit_rounded = float_round(l.debit, precision_digits=2)
if float_compare(l.debit, debit_rounded, precision_digits=6):
logger.info('Bad move to fix ID %d company_id %d name %s ref %s date %s journal %s (line ID %d debit=%s)', move.id, move.company_id.id, move.name, move.ref, move.date, move.journal_id.code, l.id, l.debit)
buggy = True
break
else:
credit_rounded = float_round(l.credit, precision_digits=2)
if float_compare(l.credit, credit_rounded, precision_digits=6):
logger.info('Bad move to fix ID %d company_id %d name %s ref %s date %s journal %s (line ID %d credit=%s)', move.id, move.company_id.id, move.name, move.ref, move.date, move.journal_id.code, l.id, l.credit)
buggy = True
break
if buggy:
bug_move_ids.append(move.id)
bal = 0.0
max_credit = (False, 0)
for l in move.line_ids:
if not float_is_zero(l.debit, precision_digits=2):
new_debit = float_round(l.debit, precision_digits=2)
self._cr.execute(
'UPDATE account_move_line set debit=%s, balance=%s where id=%s',
(new_debit, new_debit, l.id))
bal -= new_debit
elif not float_is_zero(l.credit, precision_digits=2):
new_credit = float_round(l.credit, precision_digits=2)
self._cr.execute(
'UPDATE account_move_line set credit=%s, balance=%s where id=%s',
(new_credit, new_credit * -1, l.id))
bal += new_credit
if new_credit > max_credit[1]:
max_credit = (l, new_credit)
if not float_is_zero(bal, precision_digits=2):
assert abs(bal) < 0.05
l = max_credit[0]
new_credit = max_credit[1]
new_new_credit = float_round(new_credit - bal, precision_digits=2)
assert new_new_credit > 0
self._cr.execute(
'UPDATE account_move_line set credit=%s, balance=%s where id=%s',
(new_new_credit, new_new_credit * -1, l.id))
logger.info('Move ID %d fixed', move.id)
logger.info('%d buggy moves fixed (IDs: %s)', len(bug_move_ids), bug_move_ids)
logger.info('END detect_equilibre_bug')
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
@@ -409,9 +463,7 @@ class AccountMoveLine(models.Model):
prec = self.env['decimal.precision'].precision_get('Account')
if (
self.currency_id and
self.amount_currency and
float_is_zero(self.credit, precision_digits=prec) and
float_is_zero(self.debit, precision_digits=prec)):
not float_is_zero(self.amount_currency, precision_digits=prec)):
date = self.date or None
amount_company_currency = self.currency_id.with_context(
date=date).compute(
@@ -420,9 +472,9 @@ class AccountMoveLine(models.Model):
if float_compare(
amount_company_currency, 0,
precision_digits=precision) == -1:
self.debit = amount_company_currency * -1
self.credit = amount_company_currency * -1
else:
self.credit = amount_company_currency
self.debit = amount_company_currency
def show_account_move_form(self):
self.ensure_one()
@@ -474,6 +526,8 @@ class AccountBankStatement(models.Model):
end_date = fields.Date(
compute='_compute_dates', string='End Date', readonly=True,
store=True)
hide_bank_statement_balance = fields.Boolean(
related='journal_id.hide_bank_statement_balance', readonly=True)
@api.multi
@api.depends('line_ids.date')

View File

@@ -41,6 +41,10 @@
<xpath expr="//field[@name='tax_line_ids']/tree/field[@name='amount']" position="before">
<field name="base" readonly="1"/>
</xpath>
<!-- Don't allow to force tax amount on CUSTOMER invoices -->
<xpath expr="//field[@name='tax_line_ids']/tree/field[@name='amount']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
<!-- Warning: there are 2 invoice_print buttons in the native view... probably a bug -->
<xpath expr="//button[@name='invoice_print']" position="attributes">
<attribute name="attrs">{'invisible': [('state', 'not in', ('open', 'paid'))]}</attribute>
@@ -148,7 +152,7 @@ module -->
<record id="out_invoice_line_action" model="ir.actions.act_window">
<field name="name">Customer Invoice Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', '=', 'out_invoice')]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -156,7 +160,7 @@ module -->
<record id="out_refund_line_action" model="ir.actions.act_window">
<field name="name">Customer Refund Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', '=', 'out_refund')]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -164,7 +168,7 @@ module -->
<record id="out_invoice_refund_line_action" model="ir.actions.act_window">
<field name="name">Customer Invoice Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', 'in', ('out_invoice', 'out_refund'))]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -172,7 +176,7 @@ module -->
<record id="in_invoice_line_action" model="ir.actions.act_window">
<field name="name">Supplier Invoice Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', '=', 'in_invoice')]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -180,7 +184,7 @@ module -->
<record id="in_refund_line_action" model="ir.actions.act_window">
<field name="name">Supplier Refund Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', '=', 'in_refund')]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -188,7 +192,7 @@ module -->
<record id="in_invoice_refund_line_action" model="ir.actions.act_window">
<field name="name">Supplier Invoice Lines</field>
<field name="res_model">account.invoice.line</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree</field> <!-- no access to form view until we have a proper readonly system for non draft invoices -->
<field name="domain">[('invoice_type', 'in', ('in_invoice', 'in_refund'))]</field>
<field name="context">{'show_invoice_fields': True}</field>
</record>
@@ -296,8 +300,8 @@ module -->
<field name="ref" position="after">
<field name="default_move_line_name"/>
<field name="default_account_id" invisible="1"/>
<field name="default_credit" invisible="0"/>
<field name="default_debit" invisible="0"/>
<field name="default_credit" invisible="1"/>
<field name="default_debit" invisible="1"/>
</field>
<xpath expr="//field[@name='line_ids']" position="attributes">
<attribute name="context" operation="python_dict" key="default_name">default_move_line_name</attribute>
@@ -316,6 +320,11 @@ module -->
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_account_move_line_filter"/>
<field name="arch" type="xml">
<filter domain="[('move_id.state','=','draft')]" position="before">
<filter name="current_year" string="Current Year" domain="[('date', '&gt;=', (context_today().strftime('%Y-01-01'))), ('date', '&lt;=', (context_today().strftime('%Y-12-31')))]"/>
<filter name="previous_year" string="Previous Year" domain="[('date', '&gt;=', (context_today() + relativedelta(day=1, month=1, years=-1)).strftime('%Y-%m-%d')), ('date', '&lt;=', (context_today() + relativedelta(day=31, month=12, years=-1)).strftime('%Y-%m-%d'))]"/>
<separator/>
</filter>
<field name="partner_id" position="after">
<field name="reconcile_string" />
<field name="debit" filter_domain="['|', ('debit', '=', self), ('credit', '=', self)]" string="Debit or Credit"/>
@@ -369,8 +378,9 @@ module -->
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_tree"/>
<field name="arch" type="xml">
<!-- Move reconcile_id to a better position -->
<field name="full_reconcile_id" position="replace"/>
<field name="full_reconcile_id" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="credit" position="after">
<field name="balance" sum="Total Balance"/>
<field name="reconcile_string"/>
@@ -381,6 +391,22 @@ module -->
</field>
</record>
<!-- By default, the pivot view displays the journal as row
which is really not interesting from an accountant point of view
So I prefer to display account_id by default on row.
The only drawback is that it makes quite a big pivot table
by default -->
<record id="view_move_line_pivot" model="ir.ui.view">
<field name="name">usability.account.move.line.pivot</field>
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_pivot"/>
<field name="arch" type="xml">
<field name="journal_id" position="replace">
<field name="account_id" type="row"/>
</field>
</field>
</record>
<record id="view_account_move_filter" model="ir.ui.view">
<field name="name">account_usability.account_move_search</field>
<field name="model">account.move</field>
@@ -417,6 +443,42 @@ module -->
</field>
</record>
<record id="view_account_journal_form" model="ir.ui.view">
<field name="name">usability.account.journal.form</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml">
<field name="bank_statements_source" position="after">
<field name="hide_bank_statement_balance"/>
</field>
</field>
</record>
<record id="account_journal_dashboard_kanban_view" model="ir.ui.view">
<field name="name">usability.account.journal.dashboard</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/>
<field name="arch" type="xml">
<field name="show_on_dashboard" position="after">
<field name="hide_bank_statement_balance"/>
</field>
<xpath expr="//div[@name='latest_statement']/.." position="attributes">
<attribute name="t-if">dashboard.last_balance != dashboard.account_balance &amp;&amp; !record.hide_bank_statement_balance.raw_value</attribute>
</xpath>
</field>
</record>
<record id="view_account_journal_tree" model="ir.ui.view">
<field name="name">usability.account.journal.tree</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="code"/>
</field>
</field>
</record>
<record id="view_account_journal_search" model="ir.ui.view">
<field name="name">usability.account.journal.search</field>
<field name="model">account.journal</field>
@@ -447,10 +509,26 @@ module -->
<field name="date" position="after">
<field name="start_date"/>
<field name="end_date"/>
<field name="hide_bank_statement_balance" invisible="1"/>
</field>
<field name="date" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<label for="balance_start" position="attributes">
<attribute name="attrs">{'invisible': [('hide_bank_statement_balance', '=', True)]}</attribute>
</label>
<label for="balance_end_real" position="attributes">
<attribute name="attrs">{'invisible': [('hide_bank_statement_balance', '=', True)]}</attribute>
</label>
<xpath expr="//field[@name='balance_start']/.." position="attributes">
<attribute name="attrs">{'invisible': [('hide_bank_statement_balance', '=', True)]}</attribute>
</xpath>
<xpath expr="//field[@name='balance_end_real']/.." position="attributes">
<attribute name="attrs">{'invisible': [('hide_bank_statement_balance', '=', True)]}</attribute>
</xpath>
<group name="sale_total" position="attributes">
<attribute name="attrs">{'invisible': [('hide_bank_statement_balance', '=', True)]}</attribute>
</group>
</field>
</record>
@@ -539,6 +617,11 @@ because it is useless and confusing -->
<field name="groups_id" eval="[(6, 0, [ref('base_usability.group_nobody')])]"/>
</record>
<!-- Also hide the corresponding configuration menu "Accounting > Configuration > Financial Reports" -->
<record id="account.menu_account_reports" model="ir.ui.menu">
<field name="groups_id" eval="[(6, 0, [ref('base_usability.group_nobody')])]"/>
</record>
<!-- Duplicate the menu "Sales > Configuration > Contacts > Bank Accounts"
under "Accounting > Configuration", because most users will try to find it there -->
<menuitem id="bank_account_account_config_menu" name="Bank Accounts" parent="account.menu_finance_configuration" sequence="9"/>

View File

@@ -7,13 +7,31 @@
<odoo>
<!-- In the official account module, on product category and product template,
some fields/groups are on account.group_account_invoice, some on
account.group_account_user and some on account.group_account_manager
Here, we set all those fields on account.group_account_invoice
-->
<record id="product_template_form_view" model="ir.ui.view">
<field name="name">account_usability.product.template.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<field name="property_account_income_id" position="attributes">
<attribute name="groups">account.group_account_invoice</attribute>
</field>
<field name="property_account_expense_id" position="attributes">
<attribute name="groups">account.group_account_invoice</attribute>
</field>
</field>
</record>
<record id="view_category_property_form" model="ir.ui.view">
<field name="name">account_usability.product.category.form</field>
<field name="model">product.category</field>
<field name="inherit_id" ref="account.view_category_property_form"/>
<field name="arch" type="xml">
<!-- On product form view, the group for Invoicing tab is limited to account.group_account_invoice... but on product category form, it is limited to account.group_account_manager -> we fix this and also use account.group_account_invoice -->
<group name="account_property" position="attributes">
<attribute name="groups">account.group_account_invoice</attribute>
</group>

View File

@@ -2,3 +2,4 @@
from . import account_invoice_mark_sent
from . import account_move_reversal
from . import account_move_backtodraft

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, _
from odoo.exceptions import UserError
class AccountMoveBacktodraft(models.TransientModel):
_name = 'account.move.backtodraft'
_description = 'Account Move Unpost'
def backtodraft(self):
assert self._context.get('active_model') == 'account.move'
amo = self.env['account.move']
moves = amo.browse(self._context.get('active_ids'))
moves_backtodraft = moves.filtered(lambda x: x.state == 'posted')
if not moves_backtodraft:
raise UserError(_(
'There is no journal items in posted state to unpost.'))
moves_backtodraft.button_cancel()
return True

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_move_backtodraft_form" model="ir.ui.view">
<field name="name">Unpost Journal Entries</field>
<field name="model">account.move.backtodraft</field>
<field name="arch" type="xml">
<form string="Unpost Journal Entries">
<label string="All selected journal entries will be unposted (if allowed by the journal configuration)."/>
<footer>
<button string="Unpost Journal Entries" name="backtodraft" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<act_window id="account_move_backtodraft_action"
multi="True"
key2="client_action_multi"
name="Unpost Journal Entries"
res_model="account.move.backtodraft"
src_model="account.move"
groups="account.group_account_user"
view_mode="form"
target="new" />
</odoo>

View File

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

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Base Dynamic List',
'version': '10.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Dynamic lists',
'description': """
Base Dynamic List
=================
Very often during an Odoo implementation, we need to add selection fields on a native objet, and we don't want to have a hard-coded selection list (fields.Selection), but a selection list that can be changed by users (Many2one field). For that, the developper needs to add a new object (with just a 'name' and 'sequence' field) with a form/tree view. The goal of this module is to speed-up this process by defining a dynamic list object that already has all the required views.
This module provides several ready-to-go objects:
* simple list : fields *name*, *sequence* and *active*
* translatable list : fields *name* with translate=True, *sequence* and *active*
* code list : fields *code* (unique), *name*, *sequence* and *active*
* translatable code list : fields *code* (unique), *name* with translate=True, *sequence* and *active*
These objects are readable by the employee group. The system group has full rights on it.
To use it, you need to do 2 or 3 things :
1) Add an entry in the domain field and the object you selected:
domain = fields.Selection(selection_add=[('risk.type', "Risk Type")])
2) Add the many2one field on your object:
risk_type_id = fields.Many2one(
'dynamic.list', string="Risk Type",
ondelete='restrict', domain=[('domain', '=', 'risk.type')])
3) Optionally, you can add a dedicated action and a menu entry (otherwize, you can use the generic menu entry under *Settings > Technical > Dynamic Lists*:
<record id="dynamic_list_risk_type_action" model="ir.actions.act_window">
<field name="name">Risk Type</field>
<field name="res_model">dynamic.list</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('domain', '=', 'risk.type')]</field>
<field name="context">{'default_domain': 'risk.type'}</field>
</record>
<menuitem id="dynamic_list_risk_type_menu" action="dynamic_list_risk_type_action"
parent="parent_menu_xmlid"/>
Limitation: when you want to have different access rights on these lists depending on the source object, you should prefer to use dedicated objects.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'views/dynamic_list.xml',
],
'installable': True,
}

View File

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

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class DynamicList(models.Model):
_name = 'dynamic.list'
_description = 'Dynamic List (non translatable)'
_order = 'sequence, id'
name = fields.Char(required=True)
sequence = fields.Integer()
active = fields.Boolean(default=True)
domain = fields.Selection([], string='Domain', required=True, index=True)
_sql_constraint = [(
'domain_name_uniq',
'unique(domain, name)',
'This entry already exists!'
)]
class DynamicListTranslate(models.Model):
_name = 'dynamic.list.translate'
_description = 'Translatable Dynamic List'
_order = 'sequence, id'
name = fields.Char(translate=True, required=True)
sequence = fields.Integer()
active = fields.Boolean(default=True)
domain = fields.Selection([], string='Domain', required=True, index=True)
_sql_constraint = [(
'domain_name_uniq',
'unique(domain, name)',
'This entry already exists!'
)]
class DynamicListCode(models.Model):
_name = 'dynamic.list.code'
_description = 'Dynamic list with code'
_order = 'sequence, id'
code = fields.Char(required=True)
name = fields.Char(translate=True, required=True)
sequence = fields.Integer()
active = fields.Boolean(default=True)
domain = fields.Selection([], string='Domain', required=True, index=True)
_sql_constraint = [(
'domain_code_uniq',
'unique(domain, code)',
'This code already exists!'
)]
@api.depends('code', 'name')
def name_get(self):
res = []
for rec in self:
res.append((rec.id, u'[%s] %s' % (rec.code, rec.name)))
return res
@api.model
def name_search(
self, name='', args=None, operator='ilike', limit=80):
if args is None:
args = []
if name and operator == 'ilike':
recs = self.search(
[('code', '=', name)] + args, limit=limit)
if recs:
return recs.name_get()
return super(DynamicListCode, self).name_search(
name=name, args=args, operator=operator, limit=limit)
class DynamicListCodeTranslate(models.Model):
_name = 'dynamic.list.code.translate'
_description = 'Translatable dynamic list with code'
_order = 'sequence, id'
code = fields.Char(required=True)
name = fields.Char(translate=True, required=True)
sequence = fields.Integer()
active = fields.Boolean(default=True)
domain = fields.Selection([], string='Domain', required=True, index=True)
_sql_constraint = [(
'domain_code_uniq',
'unique(domain, code)',
'This code already exists!'
)]
@api.depends('code', 'name')
def name_get(self):
res = []
for rec in self:
res.append((rec.id, u'[%s] %s' % (rec.code, rec.name)))
return res
@api.model
def name_search(
self, name='', args=None, operator='ilike', limit=80):
if args is None:
args = []
if name and operator == 'ilike':
recs = self.search(
[('code', '=', name)] + args, limit=limit)
if recs:
return recs.name_get()
return super(DynamicListCodeTranslate, self).name_search(
name=name, args=args, operator=operator, limit=limit)

View File

@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_dynamic_list_read,Read access on dynamic.list to employees,model_dynamic_list,base.group_user,1,0,0,0
access_dynamic_list_full,Full access to dynamic.list to System group,model_dynamic_list,base.group_system,1,1,1,1
access_dynamic_list_translate_read,Read access on dynamic.list.translate to employees,model_dynamic_list_translate,base.group_user,1,0,0,0
access_dynamic_list_translate_full,Full access to dynamic.list.translate to System group,model_dynamic_list_translate,base.group_system,1,1,1,1
access_dynamic_list_code_read,Read access on dynamic.list.code to employees,model_dynamic_list_code,base.group_user,1,0,0,0
access_dynamic_list_code_full,Full access to dynamic.list.code to System group,model_dynamic_list_code,base.group_system,1,1,1,1
access_dynamic_list_code_translate_read,Read access on dynamic.list.code.translate to employees,model_dynamic_list_code_translate,base.group_user,1,0,0,0
access_dynamic_list_code_translate_full,Full access to dynamic.list.code.translate to System group,model_dynamic_list_code_translate,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_dynamic_list_read Read access on dynamic.list to employees model_dynamic_list base.group_user 1 0 0 0
3 access_dynamic_list_full Full access to dynamic.list to System group model_dynamic_list base.group_system 1 1 1 1
4 access_dynamic_list_translate_read Read access on dynamic.list.translate to employees model_dynamic_list_translate base.group_user 1 0 0 0
5 access_dynamic_list_translate_full Full access to dynamic.list.translate to System group model_dynamic_list_translate base.group_system 1 1 1 1
6 access_dynamic_list_code_read Read access on dynamic.list.code to employees model_dynamic_list_code base.group_user 1 0 0 0
7 access_dynamic_list_code_full Full access to dynamic.list.code to System group model_dynamic_list_code base.group_system 1 1 1 1
8 access_dynamic_list_code_translate_read Read access on dynamic.list.code.translate to employees model_dynamic_list_code_translate base.group_user 1 0 0 0
9 access_dynamic_list_code_translate_full Full access to dynamic.list.code.translate to System group model_dynamic_list_code_translate base.group_system 1 1 1 1

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<menuitem id="dynamic_list_root_menu" name="Dynamic Lists" parent="base.menu_custom" sequence="100"/>
<record id="dynamic_list_form" model="ir.ui.view">
<field name="model">dynamic.list</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_main_view')"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="dynamic_list_tree" model="ir.ui.view">
<field name="model">dynamic.list</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_main_view')"/>
</tree>
</field>
</record>
<record id="dynamic_list_search" model="ir.ui.view">
<field name="model">dynamic.list</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group string="Group By" name="groupby">
<filter name="domain_groupby" string="Domain" context="{'group_by': 'domain'}"/>
</group>
</search>
</field>
</record>
<record id="dynamic_list_action" model="ir.actions.act_window">
<field name="name">Simple List</field>
<field name="res_model">dynamic.list</field>
<field name="view_mode">tree,form</field>
<field name="context">{'dynamic_list_main_view': True, 'search_default_domain_groupby': True}</field>
</record>
<menuitem id="dynamic_list_menu" action="dynamic_list_action" parent="dynamic_list_root_menu" sequence="10"/>
<record id="dynamic_list_translate_form" model="ir.ui.view">
<field name="model">dynamic.list.translate</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_translate_main_view')"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="dynamic_list_translate_tree" model="ir.ui.view">
<field name="model">dynamic.list.translate</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_translate_main_view')"/>
</tree>
</field>
</record>
<record id="dynamic_list_translate_search" model="ir.ui.view">
<field name="model">dynamic.list.translate</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group string="Group By" name="groupby">
<filter name="domain_groupby" string="Domain" context="{'group_by': 'domain'}"/>
</group>
</search>
</field>
</record>
<record id="dynamic_list_translate_action" model="ir.actions.act_window">
<field name="name">Translatable Simple List</field>
<field name="res_model">dynamic.list.translate</field>
<field name="view_mode">tree,form</field>
<field name="context">{'dynamic_list_translate_main_view': True, 'search_default_domain_groupby': True}</field>
</record>
<menuitem id="dynamic_list_translate_menu" action="dynamic_list_translate_action" parent="dynamic_list_root_menu" sequence="20"/>
<record id="dynamic_list_code_form" model="ir.ui.view">
<field name="model">dynamic.list.code</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="code"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_code_main_view')"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="dynamic_list_code_tree" model="ir.ui.view">
<field name="model">dynamic.list.code</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_code_main_view')"/>
</tree>
</field>
</record>
<record id="dynamic_list_code_search" model="ir.ui.view">
<field name="model">dynamic.list.code</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Name or Code" filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<field name="code"/>
<group string="Group By" name="groupby">
<filter name="domain_groupby" string="Domain" context="{'group_by': 'domain'}"/>
</group>
</search>
</field>
</record>
<record id="dynamic_list_code_action" model="ir.actions.act_window">
<field name="name">Code List</field>
<field name="res_model">dynamic.list.code</field>
<field name="view_mode">tree,form</field>
<field name="context">{'dynamic_list_code_main_view': True, 'search_default_domain_groupby': True}</field>
</record>
<menuitem id="dynamic_list_code_menu" action="dynamic_list_code_action" parent="dynamic_list_root_menu" sequence="30"/>
<record id="dynamic_list_code_translate_form" model="ir.ui.view">
<field name="model">dynamic.list.code.translate</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="code"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_code_translate_main_view')"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="dynamic_list_code_translate_tree" model="ir.ui.view">
<field name="model">dynamic.list.code.translate</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="domain" invisible="not context.get('dynamic_list_code_translate_main_view')"/>
</tree>
</field>
</record>
<record id="dynamic_list_code_translate_search" model="ir.ui.view">
<field name="model">dynamic.list.code.translate</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Name or Code" filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
<field name="code"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group string="Group By" name="groupby">
<filter name="domain_groupby" string="Domain" context="{'group_by': 'domain'}"/>
</group>
</search>
</field>
</record>
<record id="dynamic_list_code_translate_action" model="ir.actions.act_window">
<field name="name">Translatable Code List</field>
<field name="res_model">dynamic.list.code.translate</field>
<field name="view_mode">tree,form</field>
<field name="context">{'dynamic_list_code_translate_main_view': True, 'search_default_domain_groupby': True}</field>
</record>
<menuitem id="dynamic_list_code_translate_menu" action="dynamic_list_code_translate_action" parent="dynamic_list_root_menu" sequence="40"/>
</odoo>

View File

@@ -1,24 +1,3 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Base Partner One2many Phone module for OpenERP
# Copyright (C) 2014 Artisanat Monastique de Provence
# (http://www.barroux.org)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import partner_phone
from .post_install import migrate_to_partner_phone

View File

@@ -7,21 +7,21 @@
{
'name': 'Base Partner One2many Phone',
'version': '10.0.1.0.0',
'version': '10.0.2.0.0',
'category': 'Phone',
'license': 'AGPL-3',
'summary': 'One2many link between partners and phone numbers',
'summary': 'One2many link between partners and phone numbers/emails',
'description': """
Base Partner One2many Phone
===========================
With this module, one partner can have N phone numbers. It adds a new table dedicated to phone numbers and a one2many link between partners and phone numbers.
With this module, one partner can have several phone numbers and several emails. It adds a new table dedicated to phone numbers and emails and a one2many link between partners and phone numbers. This module keeps compatibility with the native behavior of Odoo on phone numbers and emails.
It has been developped by brother Bernard from Barroux Abbey and Alexis de Lattre from Akretion.
""",
'author': 'Barroux',
'website': 'http://www.barroux.org',
'depends': ['base_phone', 'sales_team'],
'author': 'Akretion',
'website': 'https://akretion.com/',
'depends': ['base_phone', 'sales_team', 'base_usability'],
'data': [
'partner_phone_view.xml',
'security/ir.model.access.csv',

View File

@@ -1,13 +1,13 @@
# Translation of OpenERP Server.
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_partner_one2many_phone
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 8.0alpha1\n"
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-02 13:35+0000\n"
"PO-Revision-Date: 2014-07-02 13:35+0000\n"
"POT-Creation-Date: 2020-01-27 18:03+0000\n"
"PO-Revision-Date: 2020-01-27 18:03+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -16,73 +16,157 @@ msgstr ""
"Plural-Forms: \n"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Home"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_uid
msgid "Created by"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Home Fax"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_date
msgid "Created on"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Mobile"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_display_name
msgid "Display Name"
msgstr ""
#. module: base_partner_one2many_phone
#: field:res.partner.phone,note:0
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_email
msgid "E-Mail"
msgstr ""
#. module: base_partner_one2many_phone
#: code:addons/base_partner_one2many_phone/partner_phone.py:61
#, python-format
msgid "E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
msgstr ""
#. module: base_partner_one2many_phone
#: code:addons/base_partner_one2many_phone/partner_phone.py:51
#, python-format
msgid "E-mail field must have a value when type is Primary E-mail or Secondary E-mail."
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_id
msgid "ID"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone___last_update
msgid "Last Modified on"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_uid
msgid "Last Updated by"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_date
msgid "Last Updated on"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_note
msgid "Note"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Office"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Office Fax"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Other"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
msgid "Partner"
msgstr ""
#. module: base_partner_one2many_phone
#: field:res.partner.phone,phone:0
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_phone
msgid "Phone"
msgstr ""
#. module: base_partner_one2many_phone
#: field:res.partner.phone,type:0
msgid "Phone Type"
#: code:addons/base_partner_one2many_phone/partner_phone.py:54
#, python-format
msgid "Phone field must be empty when type is Primary E-mail or Secondary E-mail."
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Phone/fax Home"
#: code:addons/base_partner_one2many_phone/partner_phone.py:58
#, python-format
msgid "Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_ids
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users_phone_ids
msgid "Phones"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
msgid "Phones and E-mail"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.actions.act_window,name:base_partner_one2many_phone.res_partner_phone_action
#: model:ir.ui.menu,name:base_partner_one2many_phone.res_partner_phone_menu
#: field:res.partner,phone_ids:0
#: view:res.partner.phone:0
msgid "Phones"
msgid "Phones/E-mails"
msgstr ""
#. module: base_partner_one2many_phone
#: field:res.partner.phone,partner_id:0
#: selection:res.partner.phone,type:0
msgid "Primary E-mail"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Fax"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Mobile"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Phone"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_partner_id
msgid "Related Partner"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
msgid "Search Phones/E-mail"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary E-mail"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Fax"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Mobile"
msgstr ""
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Phone"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_type
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
msgid "Type"
msgstr ""
#. module: base_partner_one2many_phone
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
msgid "res.partner.phone"

View File

@@ -1,89 +1,174 @@
# Translation of OpenERP Server.
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_partner_one2many_phone
# * base_partner_one2many_phone
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 8.0alpha1\n"
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-02 13:37+0000\n"
"PO-Revision-Date: 2014-07-02 15:50+0100\n"
"POT-Creation-Date: 2020-01-27 17:56+0000\n"
"PO-Revision-Date: 2020-01-27 17:56+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
"X-Generator: Poedit 1.5.7\n"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Home"
msgstr "Domicile"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_uid
msgid "Created by"
msgstr "Créé par"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Home Fax"
msgstr "Fax domicile"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_create_date
msgid "Created on"
msgstr "Créé le"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Mobile"
msgstr "Mobile"
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_display_name
msgid "Display Name"
msgstr "Nom à afficher"
#. module: base_partner_one2many_phone
#: field:res.partner.phone,note:0
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_email
msgid "E-Mail"
msgstr "Courriel"
#. module: base_partner_one2many_phone
#: code:addons/base_partner_one2many_phone/partner_phone.py:61
#, python-format
msgid "E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
msgstr "Le champ courriel doit être vide quand le type est tél. primaire/secondaire, portable primaire/secondaire ou fax primaire/secondaire."
#. module: base_partner_one2many_phone
#: code:addons/base_partner_one2many_phone/partner_phone.py:51
#, python-format
msgid "E-mail field must have a value when type is Primary E-mail or Secondary E-mail."
msgstr "Le champ courriel doit être renseigné quand le type est courriel primaire ou courriel secondaire."
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_id
msgid "ID"
msgstr "ID"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone___last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_note
msgid "Note"
msgstr "Note"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Office"
msgstr "Bureau"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Office Fax"
msgstr "Fax bureau"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Other"
msgstr "Autre"
#. module: base_partner_one2many_phone
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner
msgid "Partner"
msgstr "Partenaire"
#. module: base_partner_one2many_phone
#: field:res.partner.phone,phone:0
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_phone
msgid "Phone"
msgstr "Numéro de tél."
msgstr "Téléphone"
#. module: base_partner_one2many_phone
#: field:res.partner.phone,type:0
msgid "Phone Type"
msgstr "Type de téléphone"
#: code:addons/base_partner_one2many_phone/partner_phone.py:54
#, python-format
msgid "Phone field must be empty when type is Primary E-mail or Secondary E-mail."
msgstr "Le champ téléphone doit être vide quand le type est courriel primaire ou courriel secondaire."
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Phone/fax Home"
msgstr "Domicile tél./fax"
#: code:addons/base_partner_one2many_phone/partner_phone.py:58
#, python-format
msgid "Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."
msgstr "Le champ téléphone doit être renseigné quand le type est tél. primaire/secondaire, portable primaire/secondaire ou fax primaire/secondaire.."
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_ids
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_users_phone_ids
msgid "Phones"
msgstr "Téléphones"
#. module: base_partner_one2many_phone
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_tree
msgid "Phones and E-mail"
msgstr "Téls et courriels"
#. module: base_partner_one2many_phone
#: model:ir.actions.act_window,name:base_partner_one2many_phone.res_partner_phone_action
#: model:ir.ui.menu,name:base_partner_one2many_phone.res_partner_phone_menu
#: field:res.partner,phone_ids:0 view:res.partner.phone:0
msgid "Phones"
msgstr "Numéro de tél."
msgid "Phones/E-mails"
msgstr "Téls/Courriels"
#. module: base_partner_one2many_phone
#: field:res.partner.phone,partner_id:0
#: selection:res.partner.phone,type:0
msgid "Primary E-mail"
msgstr "Courriel principal"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Fax"
msgstr "Fax principal"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Mobile"
msgstr "Portable principal"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Primary Phone"
msgstr "Tél principal"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_partner_id
msgid "Related Partner"
msgstr "Partenaire lié"
msgstr "Partenaire associé"
#. module: base_partner_one2many_phone
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
msgid "Search Phones/E-mail"
msgstr "Search Phones/E-mail"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary E-mail"
msgstr "Courriel secondaire"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Fax"
msgstr "Fax secondaire"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Mobile"
msgstr "Portable secondaire"
#. module: base_partner_one2many_phone
#: selection:res.partner.phone,type:0
msgid "Secondary Phone"
msgstr "Tél. secondaire"
#. module: base_partner_one2many_phone
#: model:ir.model.fields,field_description:base_partner_one2many_phone.field_res_partner_phone_type
#: model:ir.ui.view,arch_db:base_partner_one2many_phone.res_partner_phone_search
msgid "Type"
msgstr "Type"
#. module: base_partner_one2many_phone
#: model:ir.model,name:base_partner_one2many_phone.model_res_partner_phone
msgid "res.partner.phone"
msgstr "res.partner.phone"

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, SUPERUSER_ID
oldtype2label = {
'1_home': 'Ancien type : Maison',
# '2_mobile': 'Ancien type : Portable',
'3_office': 'Ancien type : Bureau',
'4_home_fax': 'Ancien type : Fax maison',
'5_office_fax': 'Ancien type : Fax bureau',
'6_phone_fax_home': u'Ancien type : Tél/fax maison',
'7_other': 'Ancien type : Autre',
}
def migrate(cr, version):
if not version:
return
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
rppo = env['res.partner.phone']
wdict = {} # key = partnerID, values = {id: {'type': '1_home', 'phone': '+33'}}
for rec in rppo.search_read([('type', '!=', False)], ['type', 'phone', 'partner_id', 'note']):
if rec['partner_id'][0] not in wdict:
wdict[rec['partner_id'][0]] = {}
wdict[rec['partner_id'][0]][rec['id']] = rec
# first pass for primary phone
for partner_id, xdict in wdict.items():
mig_phone_entries(cr, xdict, '3_phone_primary', '4_phone_secondary', ['1_home', '6_phone_fax_home', '3_office', '7_other'])
mig_phone_entries(cr, xdict, '5_mobile_primary', '6_mobile_secondary', ['2_mobile'])
mig_phone_entries(cr, xdict, '7_fax_primary', '8_fax_secondary', ['4_home_fax', '5_office_fax'])
cr.execute('select id, email from res_partner where email is not null order by id')
for partner in cr.dictfetchall():
print('partner_id=', partner['id'])
old_email = partner['email'].strip()
if old_email:
email_split = old_email.split(',')
clean_email_split = [x.strip() for x in email_split if x.strip()]
# primary:
email_primary = clean_email_split.pop(0)
rppo.create({
'type': '1_email_primary',
'partner_id': partner['id'],
'email': email_primary,
})
cr.execute('UPDATE res_partner set email=%s where id=%s', (email_primary, partner['id']))
for email_sec in clean_email_split:
email_sec = email_sec.strip()
if email_sec:
rppo.create({
'type': '2_email_secondary',
'partner_id': partner['id'],
'email': email_sec.strip(),
})
def mig_phone_entries(cr, xdict, new_type_primary, new_type_secondary, old_type_list):
zdict = {}
for phone_id, values in xdict.items():
if values['type'] in old_type_list:
zdict[phone_id] = values
if zdict:
values_sorted = sorted(zdict.values(), key=lambda x: x['type'])
primary_phone_val = values_sorted[0]
cr.execute("""UPDATE res_partner_phone SET type=%s WHERE id=%s""", (new_type_primary, primary_phone_val['id']))
if not primary_phone_val.get('note') and oldtype2label.get(primary_phone_val['type']):
cr.execute("""UPDATE res_partner_phone SET note=%s WHERE id=%s""", (oldtype2label[primary_phone_val['type']], primary_phone_val['id']))
zdict.pop(primary_phone_val['id'])
for secondary_phone_val in zdict.values():
cr.execute("""UPDATE res_partner_phone SET type=%s WHERE id=%s""", (new_type_secondary, secondary_phone_val['id']))
if not secondary_phone_val.get('note') and oldtype2label.get(secondary_phone_val['type']):
cr.execute("""UPDATE res_partner_phone SET note=%s WHERE id=%s""", (oldtype2label[secondary_phone_val['type']], secondary_phone_val['id']))

View File

@@ -5,9 +5,12 @@
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from odoo.addons.base_phone.fields import Phone, Fax
import phonenumbers
EMAIL_TYPES = ('1_email_primary', '2_email_secondary')
PHONE_TYPES = ('3_phone_primary', '4_phone_secondary', '5_mobile_primary', '6_mobile_secondary', '7_fax_primary', '8_fax_secondary')
class ResPartnerPhone(models.Model):
@@ -15,25 +18,55 @@ class ResPartnerPhone(models.Model):
_order = 'partner_id, type'
_phone_name_sequence = 8
partner_id = fields.Many2one('res.partner', string='Related Partner')
partner_id = fields.Many2one(
'res.partner', string='Related Partner', index=True, ondelete='cascade')
type = fields.Selection([
('1_home', 'Home'),
('2_mobile', 'Mobile'),
('3_office', 'Office'),
('4_home_fax', 'Home Fax'),
('5_office_fax', 'Office Fax'),
('6_phone_fax_home', 'Phone/fax Home'),
('7_other', 'Other')],
string='Phone Type', required=True)
phone = Phone('Phone', required=True, partner_field='partner_id')
('1_email_primary', 'Primary E-mail'),
('2_email_secondary', 'Secondary E-mail'),
('3_phone_primary', 'Primary Phone'),
('4_phone_secondary', 'Secondary Phone'),
('5_mobile_primary', 'Primary Mobile'),
('6_mobile_secondary', 'Secondary Mobile'),
('7_fax_primary', 'Primary Fax'),
('8_fax_secondary', 'Secondary Fax'),
],
string='Type', required=True, index=True)
phone = Phone('Phone', required=False, partner_field='partner_id')
email = fields.Char(string='E-Mail')
note = fields.Char('Note')
@api.onchange('type')
def type_change(self):
if self.type:
if self.type in EMAIL_TYPES:
self.phone = False
elif self.type in PHONE_TYPES:
self.email = False
@api.constrains('type', 'phone', 'email')
def _check_partner_phone(self):
for rec in self:
if rec.type in EMAIL_TYPES:
if not rec.email:
raise ValidationError(_(
"E-mail field must have a value when type is Primary E-mail or Secondary E-mail."))
if rec.phone:
raise ValidationError(_(
"Phone field must be empty when type is Primary E-mail or Secondary E-mail."))
elif rec.type in PHONE_TYPES:
if not rec.phone:
raise ValidationError(_(
"Phone field must have a value when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."))
if rec.email:
raise ValidationError(_(
"E-mail field must be empty when type is Primary/Secondary Phone, Primary/Secondary Mobile or Primary/Secondary Fax."))
def name_get(self):
res = []
for pphone in self:
if pphone.partner_id:
if self._context.get('callerid'):
name = pphone.partner_id.name_get()[0][1]
name = pphone.partner_id.display_name
else:
name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name)
else:
@@ -41,45 +74,41 @@ class ResPartnerPhone(models.Model):
res.append((pphone.id, name))
return res
@api.model_cr
def init(self):
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_email_primary
ON res_partner_phone (partner_id, type)
WHERE (type='1_email_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_phone_primary
ON res_partner_phone (partner_id, type)
WHERE (type='3_phone_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_mobile_primary
ON res_partner_phone (partner_id, type)
WHERE (type='5_mobile_primary')
''')
self._cr.execute('''
CREATE UNIQUE INDEX IF NOT EXISTS single_fax_primary
ON res_partner_phone (partner_id, type)
WHERE (type='7_fax_primary')
''')
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def convert_from_international_to_e164(self, phone_num):
res = False
try:
res_parse = phonenumbers.parse(phone_num)
res = phonenumbers.format_number(
res_parse, phonenumbers.PhoneNumberFormat.E164)
except:
pass
return res
# without this convert, we would have in DB:
# E.164 format in res_partner_phone table
# phonenumbers.PhoneNumberFormat.INTERNATIONAL in res_partner
# TODO bug: but even with this, it doesn't work, the format
# is stored in international format in res_partner
# => I'll try to find the reason later
@api.multi
@api.depends('phone_ids.phone', 'phone_ids.type')
def _compute_partner_phone(self):
for partner in self:
phone = mobile = fax = False
for partner_phone in partner.phone_ids:
num_e164 = self.convert_from_international_to_e164(
partner_phone.phone)
if num_e164:
if partner_phone.type == '2_mobile':
mobile = num_e164
elif partner_phone.type in ('5_office_fax', '4_home_fax'):
fax = num_e164
else:
phone = num_e164
partner.phone = phone
partner.mobile = mobile
partner.fax = fax
# in v10, we are supposed to have in DB E.164 format
# with the current implementation, we have:
# in res.partner : PhoneNumberFormat.INTERNATIONAL
# in res.partner.phone : E.164
# It is not good, but it is not a big bug and it's complex to fix
# so let's let it like that. In v12, we store in
# PhoneNumberFormat.INTERNATIONAL, so this bug is kind of an anticipation
# for the future :)
phone_ids = fields.One2many(
'res.partner.phone', 'partner_id', string='Phones')
@@ -89,3 +118,74 @@ class ResPartner(models.Model):
compute='_compute_partner_phone', store=True, readonly=True)
fax = Fax(
compute='_compute_partner_phone', store=True, readonly=True)
email = fields.Char(
compute='_compute_partner_phone', store=True, readonly=True)
@api.depends('phone_ids.phone', 'phone_ids.type', 'phone_ids.email')
def _compute_partner_phone(self):
for partner in self:
phone = mobile = fax = email = False
for pphone in partner.phone_ids:
if pphone.type == '1_email_primary' and pphone.email:
email = pphone.email
elif pphone.phone:
if pphone.type == '5_mobile_primary':
mobile = pphone.phone
elif pphone.type == '7_fax_primary':
fax = pphone.phone
elif pphone.type == '3_phone_primary':
phone = pphone.phone
partner.phone = phone
partner.mobile = mobile
partner.fax = fax
partner.email = email
def _update_create_vals(
self, vals, type, partner_field, partner_phone_field):
if vals.get(partner_field):
vals['phone_ids'].append(
(0, 0, {'type': type, partner_phone_field: vals[partner_field]}))
@api.model
def create(self, vals):
if 'phone_ids' not in vals:
vals['phone_ids'] = []
self._update_create_vals(vals, '1_email_primary', 'email', 'email')
self._update_create_vals(vals, '3_phone_primary', 'phone', 'phone')
self._update_create_vals(vals, '5_mobile_primary', 'mobile', 'phone')
self._update_create_vals(vals, '7_fax_primary', 'fax', 'phone')
return super(ResPartner, self).create(vals)
def _update_write_vals(
self, vals, type, partner_field, partner_phone_field):
self.ensure_one()
rppo = self.env['res.partner.phone']
if partner_field in vals:
pphone = rppo.search([
('partner_id', '=', self.id),
('type', '=', type)], limit=1)
if vals[partner_field]:
if pphone:
vals['phone_ids'].append((1, pphone.id, {
partner_phone_field: vals[partner_field]}))
else:
vals['phone_ids'].append((0, 0, {
'type': type,
partner_phone_field: vals[partner_field],
}))
else:
if pphone:
vals['phone_ids'].append((2, pphone.id))
def write(self, vals):
if 'phone_ids' not in vals:
for rec in self:
vals['phone_ids'] = []
rec._update_write_vals(vals, '1_email_primary', 'email', 'email')
rec._update_write_vals(vals, '3_phone_primary', 'phone', 'phone')
rec._update_write_vals(vals, '5_mobile_primary', 'mobile', 'phone')
rec._update_write_vals(vals, '7_fax_primary', 'fax', 'phone')
super(ResPartner, rec).write(vals)
return True
else:
return super(ResPartner, self).write(vals)

View File

@@ -14,27 +14,48 @@
<field name="name">res.partner.phone.tree</field>
<field name="model">res.partner.phone</field>
<field name="arch" type="xml">
<tree string="Phones" editable="bottom">
<tree string="Phones and E-mail" editable="bottom">
<field name="partner_id" invisible="not context.get('partner_phone_main_view')"/>
<field name="type"/>
<field name="phone" widget="phone"/>
<field name="phone" widget="phone" attrs="{'required': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'readonly': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="email" widget="email" attrs="{'readonly': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'required': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="note"/>
</tree>
</field>
</record>
<record id="res_partner_phone_form" model="ir.ui.view">
<field name="name">res.partner.phone.form</field>
<field name="model">res.partner.phone</field>
<field name="arch" type="xml">
<form string="Phone and E-mail">
<group name="main">
<field name="partner_id" invisible="not context.get('partner_phone_main_view')"/>
<field name="type"/>
<field name="phone" widget="phone" attrs="{'required': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'invisible': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="email" widget="email" attrs="{'invisible': [('type', 'not in', ('1_email_primary', '2_email_secondary'))], 'required': [('type', 'in', ('1_email_primary', '2_email_secondary'))]}"/>
<field name="note"/>
</group>
</form>
</field>
</record>
<record id="res_partner_phone_search" model="ir.ui.view">
<field name="name">res.partner.phone.search</field>
<field name="model">res.partner.phone</field>
<field name="arch" type="xml">
<search string="Search Phones">
<search string="Search Phones/E-mail">
<field name="phone" />
<field name="email" />
<group name="groupby">
<filter name="type_groupby" string="Type" context="{'group_by': 'type'}"/>
</group>
</search>
</field>
</record>
<record id="res_partner_phone_action" model="ir.actions.act_window">
<field name="name">Phones</field>
<field name="name">Phones/E-mails</field>
<field name="res_model">res.partner.phone</field>
<field name="view_mode">tree</field>
<field name="context">{'partner_phone_main_view': True}</field>
@@ -43,6 +64,7 @@
<menuitem id="res_partner_phone_menu" action="res_partner_phone_action"
parent="sales_team.menu_sales" sequence="10"/>
<!-- PARTNER views -->
<record id="view_partner_form" model="ir.ui.view">
<field name="name">add.phone_ids.on.partner.form</field>
<field name="model">res.partner</field>
@@ -60,14 +82,27 @@
<field name="fax" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<xpath expr="//field[@name='child_ids']/form//field[@name='phone']" position="after">
<field name="phone_ids" nolabel="1" colspan="2"/>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='phone']" position="attributes">
<field name="email" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<!-- I can't display phone_ids in the Contacts
because there is a very strange thing in the web client: if
you have a res.partner.phone on one of the fields,
it will send to write {'child_ids': [1, ID_child, {'phone_ids': [[5], [4, id_phone_child]]}]}
=> it will delete res.partner.phone and then try to re-create it,
which triggers the message 'Record does not exist or has been deleted.'
<xpath expr="//field[@name='child_ids']/form//field[@name='phone']" position="after">
<field name="phone_ids" nolabel="1" colspan="2" widget="many2many_tags"/>
</xpath>
-->
<xpath expr="//field[@name='child_ids']/form//field[@name='phone']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='mobile']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='email']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
@@ -86,8 +121,21 @@
<field name="mobile" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="email" position="attributes">
<attribute name="invisible">1</attribute>
</field>
</field>
</record>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">phone.one2many.res.partner.search</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base_usability.view_res_partner_filter"/>
<field name="arch" type="xml">
<field name="name" position="attributes">
<attribute name="filter_domain">['|', '|', ('display_name', 'ilike', self), ('ref', '=ilike', self + '%'), ('phone_ids.email', 'ilike', self)]</attribute>
</field>
</field>
</record>
</odoo>

View File

@@ -21,26 +21,33 @@ def create_partner_phone(cr, phone_field, phone_type):
return to_create
def create_partner_email(cr):
cr.execute('SELECT id, email FROM res_partner WHERE email IS NOT null')
to_create = []
for partner in cr.fetchall():
to_create.append({
'partner_id': partner[0],
'type': '1_email_primary',
'email': partner[1],
})
return to_create
def migrate_to_partner_phone(cr, registry):
"""This post_install script is required because, when the module
is installed, Odoo creates the column in the DB and compute the field
and THEN it loads the file data/res_country_department_data.yml...
So, when it computes the field on module installation, the
departments are not available in the DB, so the department_id field
on res.partner stays null. This post_install script fixes this."""
logger.info('start data migration for one2many_phone')
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
rppo = env['res.partner.phone']
to_create = []
to_create += create_partner_phone(cr, 'phone', '1_home')
to_create += create_partner_phone(cr, 'mobile', '2_mobile')
to_create += create_partner_phone(cr, 'fax', '5_office_fax')
to_create += create_partner_phone(cr, 'phone', '3_phone_primary')
to_create += create_partner_phone(cr, 'mobile', '5_mobile_primary')
to_create += create_partner_phone(cr, 'fax', '7_fax_primary')
to_create += create_partner_email(cr)
# I need to create all at the end for invalidation purposes
for vals in to_create:
rppo.create(vals)
logger.info(
'partner_phone type %s phone %s created for partner ID %d',
vals['type'], vals['phone'], vals['partner_id'])
'partner_phone type %s phone %s email %s created for partner ID %d',
vals['type'], vals.get('phone'), vals.get('mail'), vals['partner_id'])
logger.info('end data migration for one2many_phone')
return

View File

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

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Barroux Abbey
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
class TestPartnerPhone(TransactionCase):
def setUp(self):
super(TestPartnerPhone, self).setUp()
def _check_result(self, partner, result):
rppo = self.env['res.partner.phone']
pphone_email = rppo.search(
[('type', '=', '1_email_primary'), ('partner_id', '=', partner.id)])
if result['email']:
self.assertEquals(partner.email, result['email'])
self.assertEquals(len(pphone_email), 1)
self.assertEquals(pphone_email.email, result['email'])
else:
self.assertFalse(partner.email)
self.assertFalse(pphone_email)
if result['phone']:
self.assertEquals(partner.phone.replace(u'\xa0', ''), result['phone'])
else:
self.assertFalse(partner.phone)
if result['mobile']:
self.assertEquals(partner.mobile.replace(u'\xa0', ''), result['mobile'])
else:
self.assertFalse(partner.mobile)
if result['fax']:
self.assertEquals(partner.fax.replace(u'\xa0', ''), result['fax'])
else:
self.assertFalse(partner.fax)
field2type = {
'phone': '3_phone_primary',
'mobile': '5_mobile_primary',
'fax': '7_fax_primary',
}
for field, value in result.items():
if field in field2type:
type = field2type[field]
pphone = rppo.search(
[('type', '=', type), ('partner_id', '=', partner.id)])
if value:
self.assertEquals(len(pphone), 1)
self.assertEquals(pphone.phone.replace(u'\xa0', ''), value)
else:
self.assertFalse(pphone)
def test_create_partner(self):
rpo = self.env['res.partner']
p = rpo.create({
'name': 'Test Me',
'email': 'testme@example.com',
'phone': '0198089246',
'mobile': '0198089247',
'fax': '0198089248',
})
result = {
'email': 'testme@example.com',
'phone': '+33198089246',
'mobile': '+33198089247',
'fax': '+33198089248',
}
self._check_result(p, result)
p2 = rpo.create({
'name': 'Test me now',
'email': 'testmenow@example.com',
'phone': '0972727272',
})
result = {
'email': 'testmenow@example.com',
'phone': '+33972727272',
'mobile': False,
'fax': False,
}
self._check_result(p2, result)
p3 = rpo.create({
'name': 'Test me now',
'phone_ids': [
(0, 0, {'type': '3_phone_primary', 'phone': '0972727272'}),
(0, 0, {'type': '1_email_primary', 'email': 'tutu@example.fr'})],
})
result = {
'email': 'tutu@example.fr',
'phone': '+33972727272',
'mobile': False,
'fax': False,
}
self._check_result(p3, result)
def test_write_partner(self):
p1 = self.env['res.partner'].create({
'name': 'test me now',
'country_id': self.env.ref('base.fr').id,
})
result_none = {
'email': False,
'phone': False,
'mobile': False,
'fax': False,
}
self._check_result(p1, result_none)
p1.write({
'mobile': '0198089247',
'email': 'testmenow@example.com',
})
result = {
'email': 'testmenow@example.com',
'phone': False,
'mobile': '+33198089247',
'fax': False,
}
self._check_result(p1, result)
p1.write({
'email': 'testmenow2@example.com',
'phone': False,
'mobile': '04.72.72.72.72',
})
result = {
'email': 'testmenow2@example.com',
'phone': False,
'mobile': '+33472727272',
'fax': False,
}
self._check_result(p1, result)
p1.write({
'phone': False,
'mobile': False,
'email': False,
})
self._check_result(p1, result_none)
p2 = self.env['res.partner'].create({'name': 'Toto', 'email': 'toto@example.com'})
p_multi = p1 + p2
p_multi.write({'email': 'all@example.com', 'phone': '05.60.60.60.70'})
result = {
'email': 'all@example.com',
'phone': '+33560606070',
'mobile': False,
'fax': False,
}
self._check_result(p1, result)
self._check_result(p2, result)

View File

@@ -33,7 +33,7 @@
<field name="name">Prospects</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form,kanban</field>
<field name="context">{'default_prospect': 1, 'search_default_prospect': 1}</field>
<field name="context">{'default_prospect': 1, 'default_customer': False, 'search_default_prospect': 1}</field>
</record>
<!-- I don't add a menu entry ; it should be added in custom module if needed -->

View File

@@ -2,7 +2,7 @@
# © 2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo import models, fields, api
class ResPartner(models.Model):
@@ -16,6 +16,15 @@ class ResPartner(models.Model):
'A partner already exists with this internal reference!'
)]
# in v10, display_name is store=True by default
# so, when we inherit name_get() and use additionnal fields, we
# have to inherit @api.depends of _compute_display_name() too
@api.depends(
'is_company', 'name', 'parent_id.name', 'type', 'company_name',
'ref', 'parent_id.ref')
def _compute_display_name(self):
super(ResPartner, self)._compute_display_name()
@api.multi
def name_get(self):
res = []

View File

@@ -2,6 +2,7 @@
from . import users
from . import partner
from . import bank
from . import company
from . import mail
from . import misc

20
base_usability/bank.py Normal file
View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api
class ResBank(models.Model):
_inherit = 'res.bank'
@api.multi
@api.depends('name', 'bic')
def name_get(self):
result = []
for bank in self:
name = bank.name
if bank.bic:
name = u'[%s] %s' % (bank.bic, name)
result.append((bank.id, name))
return result

View File

@@ -73,10 +73,16 @@ class ResPartner(models.Model):
if self.is_company:
company = self.name
name = False
name_no_title = False
title = False
title_short = False
else:
name = self.name_title
company = self.parent_id and self.parent_id.is_company and\
self.parent_id.name or False
name = self.name_title
name_no_title = self.name
title = self.title.name
title_short = self.title.shortcut
options = {
'name': {
'value': name,
@@ -84,6 +90,15 @@ class ResPartner(models.Model):
'company': {
'value': company,
},
'title': {
'value': title,
},
'title_short': {
'value': title_short,
},
'name_no_title': {
'value': name_no_title,
},
'phone': {
'value': self.phone,
# http://www.fileformat.info/info/unicode/char/1f4de/index.htm

View File

@@ -16,6 +16,12 @@
<attribute name="invisible">0</attribute>
<attribute name="widget">handle</attribute>
</field>
<field name="bank_name" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="bank_name" position="after">
<field name="bank_id"/>
</field>
</field>
</record>

View File

@@ -45,6 +45,11 @@
<field name="country_id" position="attributes">
<attribute name="invisible">0</attribute>
</field>
<!-- There aren't many fields in this tree view, so there is room
to add a few more -->
<field name="phone" position="after">
<field name="mobile"/>
</field>
<field name="country_id" position="before">
<field name="city"/>
</field>

View File

@@ -25,7 +25,7 @@ class ResUsers(models.Model):
@api.model
def _script_partners_linked_to_users_no_company(self):
if self.env.user.id != SUPERUSER_ID:
raise UserError(_('You must run this script as admin user'))
self = self.sudo()
logger.info(
'START to set company_id=False on partners related to users')
users = self.search(

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Commission Simple',
'version': '10.0.1.0.0',
'category': 'Sales',
'license': 'AGPL-3',
'summary': 'Compute commissions for salesman',
'description': """
Commission Simple
=================
This module is a **simple** module to compute commission for salesman. From my experience, companies often use very specific methods to compute commissions and it's impossible to develop a module that can support all of them. So the goal of this module is just to have a simple base to build the company-specific commissionning system by inheriting this simple module.
Here is a short description of this module:
* create commission profiles using rules (per product category, per product, per product and customer, etc.),
* the commission rules can have a start and end date (optional),
* commissionning can happen on invoicing or on payment,
* each invoice line can only be commissionned to one salesman,
* commission reports are stored in Odoo.
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
'account',
'date_range',
# this uses some related fields on account.invoice.line
'account_usability',
],
'data': [
'data/decimal_precision.xml',
'views/commission.xml',
'views/res_users.xml',
'views/account_config_settings.xml',
'wizard/commission_compute_view.xml',
'security/ir.model.access.csv',
'security/rule.xml',
],
'installable': True,
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record forcecreate="True" id="commission_rate" model="decimal.precision">
<field name="name">Commission Rate</field>
<field name="digits">2</field>
</record>
</odoo>

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import commission
from . import res_users
from . import res_company
from . import account_config_settings
from . import account_invoice_line

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountConfigSettings(models.TransientModel):
_inherit = 'account.config.settings'
commission_date_range_type_id = fields.Many2one(
related='company_id.commission_date_range_type_id', readonly=False)

View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
import odoo.addons.decimal_precision as dp
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
user_id = fields.Many2one(
related='invoice_id.user_id', store=True, readonly=True)
product_categ_id = fields.Many2one(
related='product_id.product_tmpl_id.categ_id', store=True, readonly=True)
commission_result_id = fields.Many2one(
'commission.result', string='Commission Result')
commission_rule_id = fields.Many2one(
'commission.rule', 'Matched Commission Rule', ondelete='restrict')
commission_base = fields.Monetary('Commission Base', currency_field='company_currency_id')
commission_rate = fields.Float('Commission Rate', digits=dp.get_precision('Commission Rate'))
commission_amount = fields.Monetary(
string='Commission Amount', currency_field='company_currency_id',
readonly=True, compute='_compute_commission_amount', store=True)
@api.depends('commission_rate', 'commission_base')
def _compute_commission_amount(self):
for line in self:
line.commission_amount = line.company_currency_id.round(
line.commission_rate * line.commission_base / 100.0)
def compute_commission_for_one_user(self, user, date_range, rules):
profile = user.commission_profile_id
company = profile.company_id
company_currency = company.currency_id
assert profile
domain = [
('invoice_type', 'in', ('out_invoice', 'out_refund')),
('date_invoice', '<=', date_range.date_end),
('company_id', '=', company.id),
('user_id', '=', user.id),
('commission_result_id', '=', False),
]
if profile.trigger_type == 'invoice':
domain.append(('state', 'in', ('open', 'paid')))
elif profile.trigger_type == 'payment':
# TODO : for this trigger, we would need to filter
# out the invoices paid after the end date of the period compute
domain.append(('state', '=', 'paid'))
else:
raise
ilines = self.search(domain, order='date_invoice, invoice_id, sequence')
com_result = self.env['commission.result'].create({
'user_id': user.id,
'profile_id': profile.id,
'date_range_id': date_range.id,
})
total = 0.0
for iline in ilines:
rule = iline._match_commission_rule(rules[profile.id])
if rule:
lvals = iline._prepare_commission_data(rule, com_result)
if lvals:
iline.write(lvals)
total += company_currency.round(
lvals['commission_rate'] * lvals['commission_base']
/ 100.0)
com_result.amount_total = total
return com_result
def _match_commission_rule(self, rules):
# commission rules are already in the right order
self.ensure_one()
for rule in rules:
if rule['date_start'] and rule['date_start'] > self.date_invoice:
continue
if rule['date_end'] and rule['date_end'] < self.date_invoice:
continue
if rule['applied_on'] == '0_customer_product':
if (
self.commercial_partner_id.id in
rule['partner_ids'] and
self.product_id.id in rule['product_ids']):
return rule
elif rule['applied_on'] == '1_customer_product_category':
if (
self.commercial_partner_id.id in
rule['partner_ids'] and
self.product_categ_id.id in rule['product_categ_ids']):
return rule
elif rule['applied_on'] == '2_product':
if self.product_id.id in rule['product_ids']:
return rule
elif rule['applied_on'] == '3_product_category':
if self.product_categ_id.id in rule['product_categ_ids']:
return rule
elif rule['applied_on'] == '4_global':
return rule
return False
def _prepare_commission_data(self, rule, commission_result):
self.ensure_one()
lvals = {
'commission_result_id': commission_result.id,
'commission_rule_id': rule['id'],
# company currency
'commission_base': self.price_subtotal_signed,
'commission_rate': rule['rate'],
}
return lvals

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
# Copyright Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models, api
import odoo.addons.decimal_precision as dp
class CommissionProfile(models.Model):
_name = 'commission.profile'
_description = 'Commission Profile'
name = fields.Char(string='Name of the Profile', required=True)
active = fields.Boolean(string='Active', default=True)
company_id = fields.Many2one(
'res.company', string='Company',
required=True,
default=lambda self: self.env['res.company']._company_default_get())
line_ids = fields.One2many(
'commission.rule', 'profile_id', string='Commission Rules')
trigger_type = fields.Selection([
('invoice', 'Invoicing'),
('payment', 'Payment'),
], default='invoice', string='Trigger', required=True)
class CommissionRule(models.Model):
_name = 'commission.rule'
_description = 'Commission Rule'
_order = 'profile_id, applied_on'
partner_ids = fields.Many2many(
'res.partner', string='Customers',
domain=[('parent_id', '=', False), ('customer', '=', True)])
product_categ_ids = fields.Many2many(
'product.category', string="Product Categories",
domain=[('type', '=', 'normal')])
product_ids = fields.Many2many('product.product', string='Products')
date_start = fields.Date('Start Date')
date_end = fields.Date('End Date')
profile_id = fields.Many2one(
'commission.profile', string='Profile', ondelete='cascade')
company_id = fields.Many2one(
related='profile_id.company_id', store=True, readonly=True)
rate = fields.Float(
'Commission Rate', digits=dp.get_precision('Commission Rate'),
copy=False)
applied_on = fields.Selection([
('0_customer_product', 'Products and Customers'),
('1_customer_product_category', "Product Categories and Customers"),
('2_product', "Products"),
('3_product_category', "Product Categories"),
('4_global', u'Global')],
string='Apply On', default='4_global', required=True)
active = fields.Boolean(string='Active', default=True)
@api.model
def load_all_rules(self):
rules = self.search_read()
res = {} # key = profile, value = [rule1 recordset, rule2]
for rule in rules:
if rule['profile_id']:
if rule['profile_id'][0] not in res:
res[rule['profile_id'][0]] = [rule]
else:
res[rule['profile_id'][0]].append(rule)
return res
_sql_constraints = [(
'rate_positive',
'CHECK(rate >= 0)',
'Rate must be positive !')]
class CommissionResult(models.Model):
_name = 'commission.result'
_description = "Commission Result"
_order = 'date_start desc'
user_id = fields.Many2one(
'res.users', 'Salesman', required=True, ondelete='restrict',
readonly=True)
profile_id = fields.Many2one(
'commission.profile', string='Commission Profile',
readonly=True)
company_id = fields.Many2one(
'res.company', string='Company',
required=True, readonly=True,
default=lambda self: self.env['res.company']._company_default_get())
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency',
readonly=True, store=True)
date_range_id = fields.Many2one(
'date.range', required=True, string='Period', readonly=True)
date_start = fields.Date(
related='date_range_id.date_start', readonly=True, store=True)
date_end = fields.Date(
related='date_range_id.date_end', readonly=True, store=True)
line_ids = fields.One2many(
'account.invoice.line', 'commission_result_id', 'Commission Lines',
readonly=True)
amount_total = fields.Monetary(
string='Commission Total', currency_field='company_currency_id',
help='This is the total amount at the date of the computation of the commission',
readonly=True)
def name_get(self):
res = []
for result in self:
name = '%s (%s)' % (result.user_id.name, result.date_range_id.name)
res.append((result.id, name))
return res
_sql_constraints = [(
'salesman_period_company_unique',
'unique(company_id, user_id, date_range_id)',
'A commission result already exists for this salesman for '
'the same period')]

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
commission_date_range_type_id = fields.Many2one(
'date.range.type', string='Commission Periodicity')

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
commission_profile_id = fields.Many2one(
'commission.profile', string='Commission Profile',
company_dependant=True)

View File

@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_commission_profile_read,Read access on commission.profile for employees,model_commission_profile,base.group_user,1,0,0,0
access_commission_profile_full,Full access on commission.profile for financial manager,model_commission_profile,account.group_account_manager,1,1,1,1
access_commission_rule_full,Full access on commission.rule for financial manager,model_commission_rule,account.group_account_manager,1,1,1,1
access_commission_rule_read,Read access on commission.rule for invoicing group,model_commission_rule,account.group_account_invoice,1,0,0,0
access_commission_result_full,Full access on commission.result to accountant,model_commission_result,account.group_account_user,1,1,1,1
access_commission_result_read,Read access on commission.result to invoicing grp,model_commission_result,account.group_account_invoice,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_commission_profile_read Read access on commission.profile for employees model_commission_profile base.group_user 1 0 0 0
3 access_commission_profile_full Full access on commission.profile for financial manager model_commission_profile account.group_account_manager 1 1 1 1
4 access_commission_rule_full Full access on commission.rule for financial manager model_commission_rule account.group_account_manager 1 1 1 1
5 access_commission_rule_read Read access on commission.rule for invoicing group model_commission_rule account.group_account_invoice 1 0 0 0
6 access_commission_result_full Full access on commission.result to accountant model_commission_result account.group_account_user 1 1 1 1
7 access_commission_result_read Read access on commission.result to invoicing grp model_commission_result account.group_account_invoice 1 0 0 0

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo noupdate="1">
<record id="commission_profile_rule" model="ir.rule">
<field name="name">Commission Profile multi-company</field>
<field name="model_id" ref="model_commission_profile"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
<record id="commission_rule_rule" model="ir.rule">
<field name="name">Commission Rule multi-company</field>
<field name="model_id" ref="model_commission_rule"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
<record id="commission_result_rule" model="ir.rule">
<field name="name">Commission Result multi-company</field>
<field name="model_id" ref="model_commission_result"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</odoo>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France
@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_config_settings" model="ir.ui.view">
<field name="name">commission.account.config.settings.form</field>
<field name="model">account.config.settings</field>
<field name="inherit_id" ref="account.view_account_config_settings" />
<field name="arch" type="xml">
<xpath expr="//div[@name='invoice_taxes']/.." position="after">
<group name="commission">
<label for="id" string="Commission"/>
<div name="commission">
<div name="commission_date_range_type">
<label for="commission_date_range_type_id"/>
<field name="commission_date_range_type_id" class="oe_inline"/>
</div>
</div>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo>
<menuitem id="commission_root" name="Commissions" parent="account.menu_finance" sequence="11"/>
<menuitem id="commission_config_root" name="Commissions" parent="account.menu_finance_configuration" sequence="110"/>
<!-- PROFILE -->
<record id="commission_profile_form" model="ir.ui.view">
<field name="name">commission.profile.form</field>
<field name="model">commission.profile</field>
<field name="arch" type="xml">
<form string="Commission Profile">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="trigger_type"/>
</group>
<group name="lines" string="Rules">
<field name="line_ids" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="commission_profile_tree" model="ir.ui.view">
<field name="name">commission.profile.tree</field>
<field name="model">commission.profile</field>
<field name="arch" type="xml">
<tree string="Commission Profiles">
<field name="name"/>
<field name="trigger_type"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="commission_profile_action" model="ir.actions.act_window">
<field name="name">Commission Profiles</field>
<field name="res_model">commission.profile</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="commission_profile_menu" action="commission_profile_action" parent="commission_config_root" sequence="18"/>
<!-- RULE -->
<record id="commission_rule_form" model="ir.ui.view">
<field name="name">commission.rule.form</field>
<field name="model">commission.rule</field>
<field name="arch" type="xml">
<form string="Commission Rules">
<sheet>
<group name="main">
<field name="profile_id" invisible="not context.get('commission_rule_main_view')"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="applied_on" widget="radio"/>
</group>
<group name="match" string="Match">
<field name="partner_ids" attrs="{'invisible': [('applied_on', 'not in', ('0_customer_product', '1_customer_product_category'))], 'required': [('applied_on', 'in', ('0_customer_product', '1_customer_product_category'))]}"/>
<field name="product_categ_ids" attrs="{'invisible': [('applied_on', 'not in', ('1_customer_product_category', '3_product_category'))], 'required': [('applied_on', 'in', ('1_customer_product_category', '3_product_category'))]}"/>
<field name="product_ids" attrs="{'invisible': [('applied_on', 'not in', ('0_customer_product', '2_product'))], 'required': [('applied_on', 'in', ('0_customer_product', '2_product'))]}"/>
<field name="date_start"/>
<field name="date_end"/>
</group>
<group name="compute" string="Compute">
<label for="rate"/>
<div name="rate">
<field name="rate" class="oe_inline"/> %
</div>
</group>
</sheet>
</form>
</field>
</record>
<record id="commission_rule_tree" model="ir.ui.view">
<field name="name">commission.rule.tree</field>
<field name="model">commission.rule</field>
<field name="arch" type="xml">
<tree string="Commission Rules">
<field name="profile_id" invisible="not context.get('commission_rule_main_view')"/>
<field name="applied_on"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="rate" string="Rate (%)"/>
</tree>
</field>
</record>
<record id="commission_rule_search" model="ir.ui.view">
<field name="name">commission.rule.search</field>
<field name="model">commission.rule</field>
<field name="arch" type="xml">
<search string="Search in Commission Rules">
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group name="groupby">
<filter name="profile_groupby" string="Profile" context="{'group_by': 'profile_id'}"/>
</group>
</search>
</field>
</record>
<record id="commission_rule_action" model="ir.actions.act_window">
<field name="name">Commission Rules</field>
<field name="res_model">commission.rule</field>
<field name="view_mode">tree,form</field>
<field name="context">{'commission_rule_main_view': True}</field>
</record>
<menuitem id="commission_rule_menu" action="commission_rule_action" parent="commission_config_root" sequence="20"/>
<!-- RESULT -->
<record id="commission_result_form" model="ir.ui.view">
<field name="name">commission.result.form</field>
<field name="model">commission.result</field>
<field name="arch" type="xml">
<form string="Commission Result">
<group name="main">
<group name="main-left">
<field name="user_id"/>
<field name="profile_id" groups="account.group_account_manager"/>
<field name="company_currency_id" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="amount_total"/>
</group>
<group name="main-right">
<field name="date_range_id"/>
<field name="date_start"/>
<field name="date_end"/>
</group>
</group>
<group name="lines" string="Invoice Lines">
<field nolabel="1" name="line_ids">
<tree>
<field name="invoice_number"/>
<field name="date_invoice"/>
<field name="commercial_partner_id" string="Customer"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="price_unit"/>
<field name="currency_id"/>
<field name="discount"/>
<field name="price_subtotal_signed" string="Amount w/o tax in company cur."/>
<field name="state"/>
<field name="commission_base"/>
<field name="commission_rate" string="Rate (%)"/>
<field name="commission_amount" sum="1"/>
<field name="commission_rule_id" string="Commission Rule"/>
<field name="company_currency_id" invisible="1"/>
</tree>
</field>
</group>
</form>
</field>
</record>
<record id="commission_result_tree" model="ir.ui.view">
<field name="name">commission.result.tree</field>
<field name="model">commission.result</field>
<field name="arch" type="xml">
<tree string="Commission Results">
<field name="date_range_id"/>
<field name="user_id"/>
<field name="profile_id" groups="account.group_account_manager"/>
<field name="company_currency_id" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="amount_total" sum="Total"/>
</tree>
</field>
</record>
<record id="commission_result_search" model="ir.ui.view">
<field name="name">commission.result.search</field>
<field name="model">commission.result</field>
<field name="arch" type="xml">
<search string="Search Commission Results">
<field name="user_id"/>
<field name="date_range_id"/>
<group name="groupby">
<filter name="user_groupby" string="Salesman" context="{'group_by': 'user_id'}"/>
<filter name="date_range_groupby" string="Period" context="{'group_by': 'date_range_id'}"/>
</group>
</search>
</field>
</record>
<record id="commission_result_action" model="ir.actions.act_window">
<field name="name">Commissions</field>
<field name="res_model">commission.result</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="commission_result_menu" action="commission_result_action" parent="commission_root" sequence="10"/>
</odoo>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo>
<record id="view_users_form" model="ir.ui.view">
<field name="name">commission.res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='action_id']/.." position="after">
<group name="commission" string="Commission">
<field name="commission_profile_id"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import commission_compute

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError
import logging
logger = logging.getLogger(__name__)
class CommissionCompute(models.TransientModel):
_name = 'commission.compute'
_description = 'Compute Commissoins'
@api.model
def _default_date_range(self):
drange_type = self.env.user.company_id.commission_date_range_type_id
if not drange_type:
return False
today = fields.Date.from_string(fields.Date.context_today(self))
first_day_last_month = today + relativedelta(months=-1, day=1)
dranges = self.env['date.range'].search([
'|', ('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False),
('type_id', '=', drange_type.id),
('date_start', '=', fields.Date.to_string(first_day_last_month))
])
return dranges and dranges[0] or dranges
date_range_id = fields.Many2one(
'date.range', required=True, string='Period',
default=lambda self: self._default_date_range())
date_start = fields.Date(related='date_range_id.date_start', readonly=True)
date_end = fields.Date(related='date_range_id.date_end', readonly=True)
def run(self):
self.ensure_one()
creso = self.env['commission.result']
ruo = self.env['res.users']
date_range = self.date_range_id
existing_res = creso.search([('date_range_id', '=', date_range.id)])
if existing_res:
raise UserError(
u'Il existe déjà des commissions pour cette période.')
com_result_ids = self.core_compute()
if not com_result_ids:
raise UserError(_('No commission generated.'))
action = self.env['ir.actions.act_window'].for_xml_id(
'commission_simple', 'commission_result_action')
action.update({
'views': False,
'domain': "[('id', 'in', %s)]" % com_result_ids,
})
return action
def core_compute(self):
rules = self.env['commission.rule'].load_all_rules()
ailo = self.env['account.invoice.line']
ruo = self.env['res.users']
com_result_ids = []
for user in ruo.with_context(active_test=False).search([]):
if user.commission_profile_id:
if user.commission_profile_id.id not in rules:
raise UserError(_(
"The commission profile '%s' doesn't have any rules.")
% user.commission_profile_id.name)
com_result = ailo.compute_commission_for_one_user(user, self.date_range_id, rules)
if com_result:
com_result_ids.append(com_result.id)
else:
logger.debug(
"Commission computation: salesman '%s' "
"doesn't have a commission profile",
user.name)
return com_result_ids

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="commission_compute_form" model="ir.ui.view">
<field name="name">commission.compute.form</field>
<field name="model">commission.compute</field>
<field name="arch" type="xml">
<form string="Compute Commissions">
<group name="main">
<field name="date_range_id"/>
<field name="date_start"/>
<field name="date_end"/>
</group>
<footer>
<button name="run" type="object" string="Compute"
class="btn-primary"/>
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="commission_compute_action" model="ir.actions.act_window">
<field name="name">Compute Commissions</field>
<field name="res_model">commission.compute</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="commission_compute_menu" action="commission_compute_action" parent="commission_root" sequence="15" groups="account.group_account_user"/>
</odoo>

View File

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Commission Simple Sale',
'version': '10.0.1.0.0',
'category': 'Sales',
'license': 'AGPL-3',
'summary': 'Give access to commission results to Salesman',
'description': """
Commission Simple Sale
======================
This module allows salesman to see their commissions in Odoo, under the Sales menu.
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
'sale',
'commission_simple',
],
'data': [
'views/commission.xml',
'security/rule.xml',
'security/ir.model.access.csv',
],
'installable': True,
}

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_commission_result_salesman_read,Read access on commission.result to salesman,commission_simple.model_commission_result,sales_team.group_sale_salesman,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_commission_result_salesman_read Read access on commission.result to salesman commission_simple.model_commission_result sales_team.group_sale_salesman 1 0 0 0

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo noupdate="1">
<record id="commission_result_salesman_rule" model="ir.rule">
<field name="name">Commission Result for Salesman</field>
<field name="model_id" ref="commission_simple.model_commission_result"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
<record id="commission_result_see_all_rule" model="ir.rule">
<field name="name">Commission Result for Sales Manager (see all)</field>
<field name="model_id" ref="commission_simple.model_commission_result"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion France (http://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo>
<menuitem id="commission_sale_root" name="Commissions" parent="sales_team.menu_base_partner" sequence="50"/>
<menuitem id="commission_result_sale_menu" action="commission_simple.commission_result_action" parent="commission_sale_root" sequence="10" groups="sales_team.group_sale_salesman"/>
</odoo>

View File

@@ -10,7 +10,8 @@ class CrmLead(models.Model):
@api.multi
def _lead_create_contact(self, name, is_company, parent_id=False):
partner = super(CrmLead, self)._lead_create_contact(
self_ctx = self.with_context(
default_customer=False, default_prospect=True)
partner = super(CrmLead, self_ctx)._lead_create_contact(
name, is_company, parent_id=parent_id)
partner.write({'prospect': True, 'customer': False})
return partner

View File

@@ -26,6 +26,7 @@ This module has been written by Alexis de Lattre from Akretion
'depends': ['crm'],
'data': [
#'wizard/base_partner_merge_view.xml',
'security/crm_security.xml',
'crm_view.xml',
],
'installable': True,

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="crm_lead_multi_company_rule" model="ir.rule">
<field name="name">CRM Lead multi-company</field>
<field name="model_id" ref="model_crm_lead"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</odoo>

View File

@@ -22,6 +22,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'website': 'http://www.akretion.com',
'depends': ['delivery'],
'data': [
'security/ir.model.access.csv',
'delivery_view.xml',
'sale_view.xml',
'stock_view.xml',

View File

@@ -18,4 +18,8 @@
</field>
</record>
<!-- In most companies, Incoterms are managed by sales people, not by stock guys
So I give access to Incoterms to Sales Manager : update ACL and add menu -->
<menuitem id="stock_incoterms_sale_config_menu" action="stock.action_incoterms_tree" parent="delivery.sale_menu_delivery" sequence="100"/>
</odoo>

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_incoterms_sale_manager,Full access on incoterms to sale manager,stock.model_stock_incoterms,sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_incoterms_sale_manager Full access on incoterms to sale manager stock.model_stock_incoterms sales_team.group_sale_manager 1 1 1 1

View File

@@ -52,7 +52,7 @@ class HrEmployee(models.Model):
def compute_private_car_total_km_this_year(self):
res = {}
private_car_product_id = self.env.ref(
'hr_expense_usability.generic_private_car_expense').id
'hr_expense_private_car.generic_private_car_expense').id
today = fields.Date.context_today(self)
today_dt = fields.Date.from_string(today)
self._cr.execute(
@@ -105,7 +105,7 @@ class HrExpense(models.Model):
@api.onchange('product_id')
def _onchange_product_id(self):
private_car_product = self.env.ref(
'hr_expense_usability.generic_private_car_expense')
'hr_expense_private_car.generic_private_car_expense')
if (
self.product_id and
self.product_id == private_car_product and
@@ -176,7 +176,7 @@ class HrExpense(models.Model):
'product_id', 'private_car_plate', 'private_car_km_price_id')
def _check_expense(self):
generic_private_car_product = self.env.ref(
'hr_expense_usability.generic_private_car_expense')
'hr_expense_private_car.generic_private_car_expense')
for exp in self:
if exp.product_id == generic_private_car_product:
if not exp.private_car_plate:

View File

@@ -116,7 +116,7 @@
<field name="account_move_id" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="groups">account.group_account_user</attribute>
<attribute name="states">post</attribute>
<attribute name="states">post,done</attribute>
<attribute name="readonly">1</attribute>
</field>
<button name="action_open_journal_entries" position="attributes">

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -0,0 +1,28 @@
# -*- coding: 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).
{
'name': 'Link Tracker Usability',
'version': '10.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': [
'link_tracker_view.xml',
],
'installable': True,
}

View File

@@ -0,0 +1,54 @@
<?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="link_tracker.action_view_click_statistics" model="ir.actions.act_window">
<field name="view_mode">graph,tree,form</field>
</record>
<record id="view_link_tracker_click_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.view_link_tracker_click_tree"/>
<field name="arch" type="xml">
<field name="country_id" position="after">
<field name="create_date" string="Click Date"/>
</field>
</field>
</record>
<record id="view_link_tracker_click_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.view_link_tracker_click_form"/>
<field name="arch" type="xml">
<field name="country_id" position="after">
<field name="create_date" string="Click Date"/>
</field>
</field>
</record>
<!-- There is no native search form -->
<record id="link_tracker_click_search" model="ir.ui.view">
<field name="name">usability.link.tracker.click.search</field>
<field name="model">link.tracker.click</field>
<field name="arch" type="xml">
<search>
<field name="link_id"/>
<group name="groupby">
<filter name="create_date_groupby" string="Click Date" context="{'group_by': 'create_date'}"/>
<filter name="link_groupby" string="Link" context="{'group_by': 'link_id'}"/>
<filter name="country_groupby" string="Country" context="{'group_by': 'country_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,13 @@
Mail Follower Option
====================
This module ensures that the addition of followers on a model is not automatic but that it is an option on each model.
By default, the option is unchecked on each model, so that the followers are not added.
Credits
=======
Contributors
------------
* Chafique Delli (chafique.delli@akretion.com)

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
{
'name': 'Mail Follower Option',
'version': '10.0.1.0.0',
'category': 'Base',
'license': 'AGPL-3',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['mail'],
'data': [
'views/ir_model_view.xml',
],
'installable': True,
}

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import mail_follower
from . import ir_model

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class IrModel(models.Model):
_inherit = 'ir.model'
mail_follower = fields.Boolean(string='Follow', default=False,
help='Check if you want create followers'
' on this model')

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class Followers(models.Model):
_inherit = 'mail.followers'
@api.model
def create(self, vals):
# Do not implicitly create followers on an object
model = self.env['ir.model'].search([
('model', '=', vals['res_model']),
('mail_follower', '=', True),
], limit=1)
if not model:
return
return super(Followers, self).create(vals)

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_model_form" model="ir.ui.view">
<field name="model">ir.model</field>
<field name="inherit_id" ref="base.view_model_form"/>
<field name="arch" type="xml">
<field name="transient" position="after">
<field name="mail_follower"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,13 @@
Mail Single Send with Several Recipients
========================================
With this module, when there are several recipients,
Odoo sends only one single e-mail with all the addressees instead of one email per address.
Credits
=======
Contributors
------------
* Chafique Delli (chafique.delli@akretion.com)

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
{
'name': 'Mail Single Send with Several Recipients',
'version': '10.0.1.0.0',
'category': 'Mail',
'license': 'AGPL-3',
'summary': "When there are several recipients, always send only one e-mail for all the addressees",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['mail'],
'installable': True,
}

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import mail_mail

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from odoo import models, api
from email.utils import formataddr
class MailMail(models.Model):
_inherit = 'mail.mail'
@api.multi
def send(self, auto_commit=False, raise_exception=False):
for mail in self:
email_to = []
for partner in mail.recipient_ids:
for partner_email in [formataddr((partner.name, partner.email))]:
email_to.append(partner_email)
mail.email_to = email_to
return super(MailMail, self).send(auto_commit=auto_commit,
raise_exception=raise_exception)
@api.multi
def send_get_mail_to(self, partner=None):
super(MailMail, self).send_get_mail_to(partner=partner)
self.ensure_one()
email_to = []
for partner in self.recipient_ids:
email_to.append(formataddr((partner.name, partner.email)))
self.recipient_ids = [(6, 0, [])]
return email_to

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from . import mail
from . import tools
from . import mail_template
from . import models
from . import wizard

View File

@@ -26,8 +26,9 @@ Small usability improvements on mails:
'website': 'http://www.akretion.com',
'depends': ['mail'],
'data': [
'mail_view.xml',
'mail_data.xml',
'views/mail_view.xml',
'data/mail_data.xml',
'wizard/email_template_preview_view.xml',
],
'installable': True,
}

View File

@@ -1,108 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
class ResPartner(models.Model):
_inherit = 'res.partner'
notify_email = fields.Selection(
selection_add=[
('all_except_notification', 'All Messages Except Notifications')],
default='all_except_notification')
def _should_be_notify_by_email(self, message):
if message.message_type == 'notification':
if self.notify_email == 'always':
return True
else:
return False
else:
return True
def _notify_by_email(
self, message, force_send=False, send_after_commit=True,
user_signature=True):
# use an empty layout for notification by default
if not self._context.get('custom_layout'):
self = self.with_context(
custom_layout='mail_usability.mail_template_notification')
# Filter the partner that should receive the notification
filtered_partners = self.filtered(
lambda p: p._should_be_notify_by_email(message)
)
return super(ResPartner, filtered_partners)._notify_by_email(
message, force_send=force_send,
send_after_commit=send_after_commit,
user_signature=user_signature)
def _notify_prepare_email_values(self, message):
res = super(ResPartner, self)._notify_prepare_email_values(message)
# Never auto delete notification email
# fucking to hard to debug when message have been delete
res['auto_delete'] = False
return res
class TemplatePreview(models.TransientModel):
_inherit = "email_template.preview"
res_id = fields.Integer(compute='_compute_res_id')
object_id = fields.Reference(selection='_reference_models')
@api.model
def default_get(self, fields):
result = super(TemplatePreview, self).default_get(fields)
if result.get('model_id'):
model = self.env['ir.model'].browse(result['model_id'])
result['object_id'] = model.model
return result
def _reference_models(self):
result = self.default_get(['model_id'])
if result.get('model_id'):
model = self.env['ir.model'].browse(result['model_id'])
return [(model.model, model.name)]
else:
models = self.env['ir.model'].search([('state', '!=', 'manual')])
return [(model.model, model.name)
for model in models
if not model.model.startswith('ir.')]
@api.depends('object_id')
def _compute_res_id(self):
for record in self:
if self.object_id:
record.res_id = self.object_id.id
def send(self):
template = self.env['mail.template'].browse(
self._context['template_id'])
template.send_mail(
self.res_id, force_send=True, raise_exception=True)
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
@api.multi
@api.returns('self', lambda value: value.id)
def message_post(self, body='', subject=None, message_type='notification',
subtype=None, parent_id=False, attachments=None,
content_subtype='html', **kwargs):
if not 'mail_create_nosubscribe' in self._context:
# Do not implicitly follow an object by just sending a message
self = self.with_context(mail_create_nosubscribe=True)
return super(MailThread,
self.with_context(mail_create_nosubscribe=True)
).message_post(
body=body, subject=subject, message_type=message_type,
subtype=subtype, parent_id=parent_id, attachments=attachments,
content_subtype=content_subtype, **kwargs)

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import mail
from . import tools
from . import mail_template
from . import mail_message
from . import res_partner

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api
import logging
_logger = logging.getLogger(__name__)
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
def _active_message_auto_subscribe_notify(self):
_logger.debug('Skip automatic subscribe notification')
return False
def _message_auto_subscribe_notify(self, partner_ids):
if self._active_message_auto_subscribe_notify():
return super(MailThread, self)._message_auto_subscribe_notify(
partner_ids)
else:
return True
@api.multi
@api.returns('self', lambda value: value.id)
def message_post(self, body='', subject=None, message_type='notification',
subtype=None, parent_id=False, attachments=None,
content_subtype='html', **kwargs):
if not 'mail_create_nosubscribe' in self._context:
# Do not implicitly follow an object by just sending a message
self = self.with_context(mail_create_nosubscribe=True)
return super(MailThread,
self.with_context(mail_create_nosubscribe=True)
).message_post(
body=body, subject=subject, message_type=message_type,
subtype=subtype, parent_id=parent_id, attachments=attachments,
content_subtype=content_subtype, **kwargs)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, fields, models
class MailMessage(models.Model):
_inherit = 'mail.message'
@property
def record_id(self):
# we do not use a reference field here as mail message
# are used everywhere and many model are not yet loaded
# so odoo raise exception
if self:
self.ensure_one()
return self.env[self.model].browse(self.res_id)
return None

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com)
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
notify_email = fields.Selection(
selection_add=[
('all_except_notification', 'All Messages Except Notifications')],
default='all_except_notification')
opt_out = fields.Boolean(track_visibility='onchange')
def _should_be_notify_by_email(self, message):
if message.message_type == 'notification':
if self.notify_email == 'always':
return True
else:
return False
else:
return True
def _notify_by_email(
self, message, force_send=False, send_after_commit=True,
user_signature=True):
# use an empty layout for notification by default
if not self._context.get('custom_layout'):
self = self.with_context(
custom_layout='mail_usability.mail_template_notification')
# Filter the partner that should receive the notification
filtered_partners = self.filtered(
lambda p: p._should_be_notify_by_email(message)
)
return super(ResPartner, filtered_partners)._notify_by_email(
message, force_send=force_send,
send_after_commit=send_after_commit,
user_signature=user_signature)
def _notify_prepare_email_values(self, message):
res = super(ResPartner, self)._notify_prepare_email_values(message)
# Never auto delete notification email
# fucking to hard to debug when message have been delete
res['auto_delete'] = False
return res

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_mail_tree" model="ir.ui.view">
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_tree"/>
<field name="arch" type="xml">
<field name="email_from" position="replace"/>
<field name="date" position="after">
<field name="email_from"/>
<field name="email_to"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import email_template_preview

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