Compare commits

...

149 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
Pierrick brun
29481d91b7 account.move.line: Add filter on both debit and credit
picked from @alexis-via (d4fcaa7d14)
2019-01-14 16:28:04 +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
Sébastien BEAU
fc45c44280 [IMP] add some extra style css support and add a debugger mode. Update readme 2019-01-08 23:27:23 +01:00
Sébastien BEAU
a370c9100c [IMP] remove the fucking auto_delete!!! 2019-01-08 23:26:12 +01:00
Sébastien BEAU
e5d6ef4bad [REF] refactor the code to make it simplifier and avoid hacking the _notify method 2019-01-08 23:24:59 +01:00
David Beal
c3f1d30bdd Merge pull request #81 from akretion/10.0-sale_partner_shipping_filter
10.0 sale partner shipping filter
2019-01-04 12:48:23 +01:00
Pierrick brun
6405be3fcf [MIG] 10.0 2019-01-04 11:11:39 +01:00
chafique-delli
7a65c63f99 replace partner_parent_id by commercial_partner_id 2019-01-04 11:00:33 +01:00
chafique-delli
13d8100d98 fix after @bealdav's comment 2019-01-04 11:00:33 +01:00
chafique-delli
0151c6a6e3 domain improvement 2019-01-04 11:00:33 +01:00
chafique-delli
ea5fdcf4df fix domain 2019-01-04 11:00:33 +01:00
chafique-delli
4ff06be1a6 add sale_partner_shipping_filter_with_customer module 2019-01-04 11:00:33 +01:00
David Beal
6c3c6cd43b Merge pull request #72 from akretion/10-fix-py3o-lines-sorting
[FIX] keep the order line sorted when creating the layout_lines dict
2019-01-02 18:56:18 +01:00
Benoît Guillot
3fef559aac [IMP] add domain on partner_id field in account_move and account_move_line search views (#73) 2019-01-02 18:55:15 +01:00
Alexis de Lattre
75d7b7eac4 Add script fix_invoice_attachment_filename 2018-12-20 11:20:31 +01:00
Sébastien BEAU
4a2fff177c [IMP] add balance in view 2018-12-18 23:25:23 +01:00
Sébastien BEAU
e83238becd [IMP] hide odoo report menu and hide the button_cancel on bacnk_statement as the code do not allow to cancel it 2018-12-18 19:53:39 +01:00
Alexis de Lattre
8bebd1e2ef Add @api.model on method that should use it
Improve log msg
2018-12-17 11:17:43 +01:00
Alexis de Lattre
b23f03f79d Add src and dest location on prodlot selection popup 2018-12-05 21:23:43 +01:00
Mourad EL HADJ MIMOUNE
dc8363d6d1 [FIX] add start_date,end_date in statement.display_name 2018-12-04 16:39:57 +01:00
Alexis de Lattre
ad850024ec Script for account.group now works in multi-company envir 2018-12-04 16:33:23 +01:00
Sébastien BEAU
03564a20b2 [IMP] add readme, remove auto following when sending an email, use light version of email notification to avoid injecting useless link in the mail sent 2018-12-04 11:55:57 +01:00
Alexis de Lattre
c1618166fb Improve account group generation 2018-11-29 21:57:19 +01:00
Alexis de Lattre
cf2464dfa4 Add module sale_order_full_dropship 2018-11-29 12:34:34 +01:00
Alexis de Lattre
80191002b8 Fix typo and code cleanup 2018-11-28 17:15:43 +01:00
Alexis de Lattre
694c800df3 Fix visibility of invoice_print button on invoice form 2018-11-28 16:04:18 +01:00
Alexis de Lattre
bdf51029c7 Add script to create account groups 2018-11-22 21:25:40 +01:00
Alexis de Lattre
478ab1fc2b account_usability: improve display of reconcile information, in particular partial reconcile
Warning: on existing big databases, this upgrade will take a long time
because there is a new computed stored field on account.move.line. But it is
required to keep good perfs on tree view of move lines.
2018-11-22 16:26:27 +01:00
Alexis de Lattre
362dba5f90 Restore drill-through on sale and invoice reports 2018-11-21 18:15:25 +01:00
Alexis de Lattre
ac2b70b66e Add margin in sale.report 2018-11-21 16:44:05 +01:00
Alexis de Lattre
b75a2e47a2 Add margin in invoice report
Consequence: no more need for module account_invoice_margin_report
2018-11-21 16:22:10 +01:00
Pierrick Brun
3f73f15e4a [ADD] confirm on reset_real_qty for stock.inventory 2018-11-20 15:27:01 +01:00
Alexis de Lattre
c196343ec0 Improve usability of account.move creation/edition
Default value for account_id, debit, credit, similar to v8 behavior
2018-11-19 19:21:23 +01:00
Alexis de Lattre
7d359d6730 Fix typo 2018-11-19 11:09:37 +01:00
Alexis de Lattre
904bf6c4f4 Cut name_get() of invoice if too long (which screws-up the invoice form view because of the ariane thread at the top) 2018-11-19 10:52:04 +01:00
Alexis de Lattre
e13a2aba3d Remove menu entry for analytic tags
Hide field tag_ids on form view of analytic accounts
2018-11-12 18:27:02 +01:00
Alexis de Lattre
8cb880771e Add module stock_user_default_warehouse_mrp
Fix module stock_user_default_warehouse_purchase
2018-11-02 11:54:21 +01:00
Alexis de Lattre
087bb1fde2 MRP production form: move src/dest loc to the top 2018-10-26 15:46:17 +02:00
Pierrick Brun
806b1b4a86 Merge pull request #68 from yvaucher/10.0-account_invoice_update_wizard-analytic
[10.0] account_invoice_update_wizard analytic fields
2018-10-22 13:56:46 +02:00
Yannick Vaucher
23519f4027 Merge pull request #1 from akretion/10.0-account_invoice_update_wizard-analytic
[FIX] only show analytics to users in group
2018-10-19 14:57:56 +02:00
Pierrick Brun
b54ec10f10 Merge pull request #63 from akretion/10.0-mig-product_search_supplier_code
[MIG] 10.0 product_search_supplier_code
2018-10-17 10:08:06 +02:00
Benoit
118dd2a5c0 [FIX] keep invoice lines sorted when creating the layout_lines dict 2018-09-28 17:42:31 +02:00
Hpar
0afe9a39a6 Merge pull request #57 from akretion/10-product_unit_manager_group-migration
10 product unit manager group migration
2018-09-25 11:56:47 +02:00
Hpar
159f163da5 Merge pull request #61 from akretion/10-add-purchase_buyer
Add purchase buyer module
2018-09-21 14:30:29 +02:00
Hpar
d0620a4c83 Update README.rst 2018-09-21 14:30:18 +02:00
Alexis de Lattre
5d2d5b1e63 Revert my previous commit: use partner_bank_active instead
The module partner_bank_active is avail in OCA/partner-contact
2018-09-18 23:19:09 +02:00
Alexis de Lattre
ab1144850b Fix crash when several quotes are linked to the opportunity 2018-09-18 23:03:21 +02:00
Alexis de Lattre
4995403bf5 Add active field on res.partner.bank 2018-09-18 21:32:16 +02:00
Alexis de Lattre
a415744f11 Add widget=handle on sequence of res.partner.bank 2018-09-17 11:18:01 +02:00
Benoit
c943b4cd33 [FIX] keep the order line sorted when creating the layout_lines dict 2018-09-14 17:46:15 +02:00
Alexis de Lattre
8800b94e5b Fix bad port of name_get() of account.analytic.account to v10 2018-09-13 11:56:37 +02:00
beau sebastien
ca5238a03c Merge pull request #70 from akretion/10.0-better-wizard-for-mail-test
[IMP] improve the wizard for testing email, allow to search on object and to send email for real check
2018-09-04 15:54:59 +02:00
Sébastien BEAU
5f15d83c3c [IMP] improve the wizard for testing email, allow to search on object and to send email for real check 2018-09-04 15:50:02 +02:00
Alexis de Lattre
6370dc0ec8 Port base_user_auth_log to v10 2018-09-02 22:39:49 +02:00
Alexis de Lattre
0d27c0a830 Add module base_user_auth_log 2018-09-02 22:21:05 +02:00
Alexis de Lattre
5f6107f2e8 Add a patch to have analytic in case of writeoff in the register payment
wizard
2018-07-23 16:20:38 +02:00
Alexis de Lattre
ebb8f1ad86 carrier_id not readonly on done picking (add tracking on it)
fix for formatLang inherit in base_usability
2018-07-18 14:27:21 +02:00
Alexis de Lattre
04118bbf46 account_usability: Add copy=False on some fields 2018-07-16 17:16:15 +02:00
Alexis de Lattre
9cba31b68a Show title not only on Contacts 2018-07-13 15:43:46 +02:00
Alexis de Lattre
21d1454ab9 Use untaxed amount in name_get of purchase orders
Add sum for qty in operation lines
2018-07-12 23:53:26 +02:00
Alexis de Lattre
d4e673103e Show 'base' field in tax lines on invoice form view 2018-07-09 17:44:55 +02:00
Alexis de Lattre
9ebf7cdb4c FIX my previous commit: related_sudo -> compute_sudo 2018-07-09 11:43:22 +02:00
Alexis de Lattre
f34a731d95 Add related_sudo where it may be needed
PEP8 fix
2018-07-09 10:41:53 +02:00
Alexis de Lattre
6ef322be4c Improve invoice line view 2018-07-06 22:26:38 +02:00
Alexis de Lattre
dd15e3d194 Add search on supplier on product search view
Move margin fields to sale order line form view (instead of tree view)
2018-07-06 19:33:35 +02:00
Pierrick Brun
e3fc2764fb [MIG] 10.0 product_search_supplier_code 2018-04-26 14:55:26 +02:00
hparfr
f0bb02edf1 Add purchase buyer module 2018-04-11 10:49:41 +02:00
hparfr
dc05ed3b5d Migration to v10 2018-02-14 11:28:03 +01:00
chafique-delli
2c1b8fe657 Moves product_unit_manager_group from 8.0 2018-02-14 11:27:22 +01:00
211 changed files with 5433 additions and 531 deletions

View File

@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
from . import report
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

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import account_invoice
from . import account_invoice_report

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

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright 2018 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 AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# why digits=0 ??? Why is it like that in the native "account" module
user_currency_margin = fields.Float(
string="Margin", compute='_compute_user_currency_margin', digits=0)
_depends = {
'account.invoice': [
'account_id', 'amount_total_company_signed',
'commercial_partner_id', 'company_id',
'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id',
'residual', 'state', 'type', 'user_id',
],
'account.invoice.line': [
'account_id', 'invoice_id', 'price_subtotal', 'product_id',
'quantity', 'uom_id', 'account_analytic_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'product.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.depends('currency_id', 'date', 'margin')
def _compute_user_currency_margin(self):
context = dict(self._context or {})
user_currency_id = self.env.user.company_id.currency_id
currency_rate_id = self.env['res.currency.rate'].search([
('rate', '=', 1),
'|',
('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False)], limit=1)
base_currency_id = currency_rate_id.currency_id
ctx = context.copy()
for record in self:
ctx['date'] = record.date
record.user_currency_margin = base_currency_id.with_context(
ctx).compute(record.margin, user_currency_id)
# TODO check for refunds
def _sub_select(self):
select_str = super(AccountInvoiceReport, self)._sub_select()
select_str += ", SUM(ail.margin_company_currency) AS margin"
return select_str
def _select(self):
select_str = super(AccountInvoiceReport, self)._select()
select_str += ", sub.margin AS margin"
return select_str

View File

@@ -12,17 +12,21 @@
<field name="model">account.invoice.line</field>
<field name="inherit_id" ref="account.view_invoice_line_form"/>
<field name="arch" type="xml">
<field name="discount" position="after">
<field name="company_id" position="after">
<field name="standard_price_company_currency"
groups="account.group_account_user"/>
groups="base.group_no_one"/>
<field name="standard_price_invoice_currency"
widget="monetary"
options="{'currency_field': 'currency_id'}"
groups="account.group_account_user"/>
groups="base.group_no_one"/>
<field name="margin_invoice_currency"
groups="account.group_account_user"/>
groups="base.group_no_one"/>
<field name="margin_company_currency"
groups="account.group_account_user"/>
groups="base.group_no_one"/>
<label for="margin_rate" groups="base.group_no_one"/>
<div name="margin_rate" groups="base.group_no_one">
<field name="margin_rate" class="oe_inline"/> %
</div>
</field>
</field>
</record>
@@ -34,18 +38,10 @@
<field name="arch" type="xml">
<field name="move_id" position="after">
<field name="margin_invoice_currency"
string="Margin" groups="account.group_account_user"/>
string="Margin" groups="base.group_no_one"/>
<field name="margin_company_currency"
groups="account.group_account_user"/>
groups="base.group_no_one"/>
</field>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='price_subtotal']" position="after">
<field name="standard_price_invoice_currency" groups="base.group_no_one" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="standard_price_company_currency" groups="base.group_no_one" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
<field name="margin_invoice_currency" groups="base.group_no_one"/>
<field name="margin_company_currency" groups="base.group_no_one"/>
<field name="margin_rate" groups="base.group_no_one"/>
<field name="company_currency_id" invisible="1"/>
</xpath>
</field>
</record>

View File

@@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Account Invoice Margin Report module for Odoo
# Copyright (C) 2015 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Account Invoice Margin Report',
'version': '0.1',
'category': 'Accounting & Finance',
'license': 'AGPL-3',
'summary': 'Add margin measure in Invoices Analysis',
'description': """
This module adds the measure *Margin* in the Invoices Analysis pivot table. It is in a separate module because it depends on the module *bi_invoice_company_currency* (in which I re-wrote the Invoice Analysis pivot table).
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['account_invoice_margin', 'bi_invoice_company_currency'],
'data': [],
'installable': False,
}

View File

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

View File

@@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Account Invoice Margin Report module for Odoo
# Copyright (C) 2015 Akretion (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields
import openerp.addons.decimal_precision as dp
class AccountInvoiceReportBi(models.Model):
_inherit = "account.invoice.report.bi"
margin_company_currency = fields.Float(
string='Margin', readonly=True,
digits=dp.get_precision('Account'))
def _select(self):
select = super(AccountInvoiceReportBi, self)._select()
select += """
, sum(ail.margin_company_currency) AS margin_company_currency
"""
return select

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(
@@ -18,11 +21,32 @@ class AccountMoveLineFilterWizard(models.TransientModel):
account_reconcile = fields.Boolean(
related='account_id.reconcile', readonly=True)
reconcile = fields.Selection([
('unreconciled', 'Unreconciled'),
('unreconciled', 'Unreconciled or Partially Reconciled'),
('reconciled', 'Fully Reconciled'),
# ('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

@@ -19,7 +19,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
""",
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['account'],
'depends': ['account', 'base_usability'],
'data': ['account_view.xml'],
'installable': True,
}

View File

@@ -42,5 +42,20 @@
</field>
</record>
<!-- ANALYTIC ACCOUNT -->
<record id="view_account_analytic_account_form" model="ir.ui.view">
<field name="name">account_no_analytic_tags.analytic.account.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="arch" type="xml">
<field name="tag_ids" position="attributes">
<attribute name="invisible">1</attribute>
</field>
</field>
</record>
<record id="account.account_analytic_tag_menu" model="ir.ui.menu">
<field name="groups_id" eval="[(6, 0, [ref('base_usability.group_nobody')])]"/>
</record>
</odoo>

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from . import account
from . import account_invoice_report
from . import partner
from . import wizard

View File

@@ -31,15 +31,18 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'website': 'http://www.akretion.com',
'depends': [
'account',
'base_view_inheritance_extension',
'base_usability', # needed only to access base_usability.group_nobody
# in v12, I may create a module only for group_nobody
],
'data': [
'account_view.xml',
'account_report.xml',
'account_invoice_report_view.xml',
'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,8 @@
# 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
import logging
@@ -43,7 +44,6 @@ class AccountInvoice(models.Model):
compute='_compute_has_attachment',
search='_search_has_attachment', readonly=True)
@api.multi
def _compute_has_discount(self):
prec = self.env['decimal.precision'].precision_get('Discount')
for inv in self:
@@ -78,6 +78,25 @@ class AccountInvoice(models.Model):
res = [('id', value and 'in' or 'not in', att_inv_ids.keys())]
return res
# when you have an invoice created from a lot of sale orders, the 'name'
# field is very large, which makes the name_get() of that invoice very big
# which screws-up the form view of that invoice because of the link at the
# top of the screen
# That's why we have to cut the name_get() when it's too long
def name_get(self):
old_res = super(AccountInvoice, self).name_get()
res = []
for old_re in old_res:
name = old_re[1]
if name and len(name) > 100:
# nice cut
name = u'%s ...' % ', '.join(name.split(', ')[:3])
# if not enough, hard cut
if len(name) > 120:
name = u'%s ...' % old_re[1][:120]
res.append((old_re[0], name))
return res
# I really hate to see a "/" in the 'name' field of the account.move.line
# generated from customer invoices linked to the partners' account because:
# 1) the label of an account move line is an important field, we can't
@@ -102,6 +121,31 @@ class AccountInvoice(models.Model):
lines.unlink()
return True
@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
# new attachment when printing an old invoice that already had the
# PDF of the invoice as attachment
logger.info('START fix customer invoice attachment filename')
# Run this script as admin to fix problem in all companies
self = self.sudo()
attachs = self.env['ir.attachment'].search([
('res_model', '=', 'account.invoice'),
('res_id', '!=', False),
('type', '=', 'binary'),
('name', '=like', 'INV%.pdf'),
('datas_fname', '=like', 'INV%.pdf.pdf')])
for attach in attachs:
inv = self.browse(attach.res_id)
if inv.type in ('out_invoice', 'out_refund'):
attach.datas_fname = attach.name
logger.info(
'Fixed field datas_fname of attachment ID %s name %s',
attach.id, attach.name)
logger.info('END fix customer invoice attachment filename')
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
@@ -114,7 +158,7 @@ class AccountInvoiceLine(models.Model):
related='invoice_id.date_invoice', store=True, readonly=True)
commercial_partner_id = fields.Many2one(
related='invoice_id.partner_id.commercial_partner_id',
store=True, readonly=True)
store=True, readonly=True, compute_sudo=True)
state = fields.Selection(
related='invoice_id.state', store=True, readonly=True,
string='Invoice State')
@@ -126,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')
@@ -144,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')
@@ -197,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,
@@ -223,9 +262,9 @@ 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 aao.search([('user_type_id', '=', bank_type.id)]):
for account in accounts:
if account not in journal_accounts_bank_type:
account.user_type_id = asset_type.id
logger.info(
@@ -234,6 +273,49 @@ class AccountAccount(models.Model):
logger.info("END of the script 'fix bank and cash account types'")
return True
@api.model
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
assert isinstance(level, int)
companies = self.env['res.company'].search([])
if len(companies) > 1:
logger.info(
'Multi-company detected: running script create_account_groups '
'as admin')
self = self.sudo()
ago = self.env['account.group']
groups = ago.search([])
if groups:
raise UserError(_("Some account groups already exists"))
accounts = self.search([])
struct = {'childs': {}}
for account in accounts:
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
while n <= level:
group_code = account.code[:n]
if group_code not in parent['childs']:
new_group = ago.create({
'name': u'%s%s' % (name_prefix or '', group_code),
'code_prefix': group_code,
'parent_id': gparent and gparent.id or False,
})
parent['childs'][group_code] = {'obj': new_group, 'childs': {}}
parent = parent['childs'][group_code]
gparent = parent['obj']
n += 1
account.group_id = gparent.id
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
@@ -243,9 +325,7 @@ class AccountAnalyticAccount(models.Model):
if self._context.get('analytic_account_show_code_only'):
res = []
for record in self:
res.append((
record.id,
record.code or record._get_one_full_name(record)))
res.append((record.id, record.code or record.name))
return res
else:
return super(AccountAnalyticAccount, self).name_get()
@@ -265,6 +345,81 @@ class AccountMove(models.Model):
# By default, we can still modify "ref" when account move is posted
# which seems a bit lazy for me...
ref = fields.Char(states={'posted': [('readonly', True)]})
date = fields.Date(copy=False)
default_account_id = fields.Many2one(
related='journal_id.default_debit_account_id', readonly=True)
default_credit = fields.Float(
compute='_compute_default_credit_debit', readonly=True)
default_debit = fields.Float(
compute='_compute_default_credit_debit', readonly=True)
@api.depends('line_ids.credit', 'line_ids.debit')
def _compute_default_credit_debit(self):
for move in self:
total_debit = total_credit = default_debit = default_credit = 0.0
for l in move.line_ids:
total_debit += l.debit
total_credit += l.credit
# I could use float_compare, but I don't think it's really needed
# in this context
if total_debit > total_credit:
default_credit = total_debit - total_credit
else:
default_debit = total_credit - total_debit
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):
@@ -278,6 +433,14 @@ class AccountMoveLine(models.Model):
# Update field only to add a string (there is no string in account module)
invoice_id = fields.Many2one(string='Invoice')
date_maturity = fields.Date(copy=False)
account_reconcile = fields.Boolean(
related='account_id.reconcile', readonly=True)
full_reconcile_id = fields.Many2one(string='Full Reconcile')
matched_debit_ids = fields.One2many(string='Partial Reconcile Debit')
matched_credit_ids = fields.One2many(string='Partial Reconcile Credit')
reconcile_string = fields.Char(
compute='_compute_reconcile_string', string='Reconcile', store=True)
@api.onchange('credit')
def _credit_onchange(self):
@@ -300,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(
@@ -311,11 +472,10 @@ 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
@api.multi
def show_account_move_form(self):
self.ensure_one()
action = self.env['ir.actions.act_window'].for_xml_id(
@@ -328,6 +488,34 @@ class AccountMoveLine(models.Model):
})
return action
@api.depends(
'full_reconcile_id', 'matched_debit_ids', 'matched_credit_ids')
def _compute_reconcile_string(self):
for line in self:
rec_str = False
if line.full_reconcile_id:
rec_str = line.full_reconcile_id.name
else:
rec_str = ', '.join([
'a%d' % pr.id for pr in line.matched_debit_ids + line.matched_credit_ids])
line.reconcile_string = rec_str
class AccountPartialReconcile(models.Model):
_inherit = "account.partial.reconcile"
_rec_name = "id"
def name_get(self):
res = []
for rec in self:
# There is no seq for partial rec, so I simulate one with the ID
# Prefix for full rec: 'A' (upper case)
# Prefix for partial rec: 'a' (lower case)
amount_fmt = formatLang(self.env, rec.amount, currency_obj=rec.company_currency_id)
name = 'a%d (%s)' % (rec.id, amount_fmt)
res.append((rec.id, name))
return res
class AccountBankStatement(models.Model):
_inherit = 'account.bank.statement'
@@ -338,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')
@@ -347,6 +537,16 @@ class AccountBankStatement(models.Model):
st.start_date = dates and min(dates) or False
st.end_date = dates and max(dates) or False
@api.multi
@api.depends('name', 'start_date', 'end_date')
def name_get(self):
res = []
for statement in self:
name = "%s (%s => %s)" % (
statement.name, statement.start_date, statement.end_date)
res.append((statement.id, name))
return res
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
number = fields.Char(string="Number", readonly=True)
def _sub_select(self):
select_str = super(AccountInvoiceReport, self)._sub_select()
select_str += ", ai.number"
return select_str
def _select(self):
select_str = super(AccountInvoiceReport, self)._select()
select_str += ", sub.number"
return select_str
def _group_by(self):
group_by_str = super(AccountInvoiceReport, self)._group_by()
group_by_str += ", ai.number"
return group_by_str

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 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="account_invoice_report_tree" model="ir.ui.view">
<field name="name">usability.account.invoice.report.tree</field>
<field name="model">account.invoice.report</field>
<field name="arch" type="xml">
<tree string="Invoices Analysis">
<field name="number"/>
<field name="date"/>
<field name="date_due"/>
<field name="type"/>
<field name="commercial_partner_id"/>
<field name="user_id"/>
<field name="product_id"/>
<field name="product_qty" sum="1"/>
<field name="uom_name" groups="product.group_uom"/>
<field name="price_total" sum="1"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="account.action_account_invoice_report_all_supp" model="ir.actions.act_window">
<field name="context">{'search_default_current': 1, 'search_default_supplier': 1, 'search_default_year': 1}</field> <!-- Remove group_by_no_leaf, which breaks tree view -->
</record>
<record id="account.action_account_invoice_report_all" model="ir.actions.act_window">
<field name="context">{'search_default_current': 1, 'search_default_customer': 1, 'search_default_year': 1}</field> <!-- Remove group_by_no_leaf, which breaks tree view -->
</record>
<record id="view_account_invoice_report_pivot" model="ir.ui.view">
<field name="name">usability.account.invoice.report</field>
<field name="model">account.invoice.report</field>
<field name="inherit_id" ref="account.view_account_invoice_report_pivot"/>
<field name="arch" type="xml">
<pivot position="attributes">
<attribute name="disable_linking"></attribute>
</pivot>
</field>
</record>
</odoo>

View File

@@ -19,6 +19,9 @@
<field name="invoice_line_ids" position="before">
<button name="delete_lines_qty_zero" states="draft" string="⇒ Delete lines qty=0" type="object" class="oe_link oe_right" groups="account.group_account_invoice"/>
</field>
<xpath expr="//field[@name='tax_line_ids']/tree/field[@name='amount']" position="before">
<field name="base" readonly="1"/>
</xpath>
</field>
</record>
@@ -35,6 +38,20 @@
<field name="move_id" position="before">
<field name="sent"/>
</field>
<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>
</xpath>
<xpath expr="//button[@name='invoice_print'][2]" position="attributes">
<attribute name="attrs">{'invisible': True}</attribute>
</xpath>
</field>
</record>
@@ -135,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>
@@ -143,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>
@@ -151,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>
@@ -159,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>
@@ -167,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>
@@ -175,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>
@@ -241,6 +258,17 @@ module -->
</field>
</record>
<record id="view_account_payment_form" model="ir.ui.view">
<field name="name">usability.account.payment.form</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml">
<field name="communication" position="after">
<field name="payment_reference"/>
</field>
</field>
</record>
<!-- model account.move.line / Journal Items -->
<record id="account.action_account_moves_all_a" model="ir.actions.act_window">
<field name="limit">200</field>
@@ -271,12 +299,18 @@ module -->
<field name="arch" type="xml">
<field name="ref" position="after">
<field name="default_move_line_name"/>
<field name="default_account_id" invisible="1"/>
<field name="default_credit" invisible="1"/>
<field name="default_debit" invisible="1"/>
</field>
<xpath expr="//field[@name='line_ids']" position="attributes">
<attribute name="context">{'line_ids': line_ids, 'journal_id': journal_id, 'default_name': default_move_line_name}</attribute>
<attribute name="context" operation="python_dict" key="default_name">default_move_line_name</attribute>
<attribute name="context" operation="python_dict" key="default_account_id">default_account_id</attribute>
<attribute name="context" operation="python_dict" key="default_credit">default_credit</attribute>
<attribute name="context" operation="python_dict" key="default_debit">default_debit</attribute>
</xpath>
<xpath expr="//field[@name='line_ids']/tree/field[@name='date_maturity']" position="after">
<field name="full_reconcile_id"/>
<xpath expr="//field[@name='line_ids']/tree/field[@name='credit']" position="after">
<field name="reconcile_string"/>
</xpath>
</field>
</record>
@@ -286,8 +320,14 @@ 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="full_reconcile_id" />
<field name="reconcile_string" />
<field name="debit" filter_domain="['|', ('debit', '=', self), ('credit', '=', self)]" string="Debit or Credit"/>
</field>
<filter name="unreconciled" position="before">
<filter name="reconciled" string="Fully Reconciled" domain="[('full_reconcile_id', '!=', False)]"/>
@@ -299,6 +339,9 @@ module -->
<field name="name" position="attributes">
<attribute name="string">Name or Reference</attribute>
</field>
<field name="partner_id" position="attributes">
<attribute name="domain">['|', ('parent_id', '=', False), ('is_company', '=', True)]</attribute>
</field>
</field>
</record>
@@ -312,7 +355,21 @@ module -->
</field>
<field name="move_id" position="after">
<field name="invoice_id"/>
<field name="account_reconcile" invisible="1"/>
</field>
<xpath expr="//field[@name='full_reconcile_id']/.." position="replace">
<field name="full_reconcile_id" nolabel="1"/> <!-- label is already in view -->
<field name="matched_debit_ids" readonly="1" widget="many2many_tags" attrs="{'invisible': ['|', ('full_reconcile_id', '!=', False), ('matched_debit_ids', '=', [])]}"/>
<field name="matched_credit_ids" readonly="1" widget="many2many_tags" attrs="{'invisible': ['|', ('full_reconcile_id', '!=', False), ('matched_credit_ids', '=', [])]}"/>
<field name="reconciled" invisible="1"/>
<button name="open_reconcile_view" class="oe_link" type="object"
string="-> View partially reconciled entries" colspan="2"
attrs="{'invisible': ['|', ('full_reconcile_id', '!=', False), '&amp;', ('matched_debit_ids', '=', []),('matched_credit_ids', '=', [])]}"/>
<span colspan="2" attrs="{'invisible': ['|', '|', ('full_reconcile_id', '!=', False), ('matched_debit_ids', '!=', []), ('matched_credit_ids', '!=', [])]}" class="o_form_field">No Partial Reconcile</span>
</xpath>
<xpath expr="//label[@for='full_reconcile_id']/.." position="attributes">
<attribute name="attrs">{'invisible': [('account_reconcile', '=', False)]}</attribute>
</xpath>
</field>
</record>
@@ -321,10 +378,12 @@ 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="full_reconcile_id"/>
<field name="balance" sum="Total Balance"/>
<field name="reconcile_string"/>
</field>
<field name="date_maturity" position="after">
<button name="show_account_move_form" type="object" icon="fa-arrows-h" string="Show Journal Entry"/>
@@ -332,6 +391,33 @@ 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>
<field name="inherit_id" ref="account.view_account_move_filter"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="domain">['|', ('parent_id', '=', False), ('is_company', '=', True)]</attribute>
</field>
</field>
</record>
<record id="view_account_search" model="ir.ui.view">
<field name="name">account.account.search</field>
<field name="model">account.account</field>
@@ -357,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>
@@ -375,6 +497,9 @@ module -->
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="arch" type="xml">
<button name="button_cancel" position="attributes">
<attribute name="invisible">1</attribute>
</button>
<xpath expr="//field[@name='line_ids']/tree/field[@name='bank_account_id']" position="after">
<!-- The cancel button is provided by the account_cancel module, but we don't want to depend on it -->
<button name="show_account_move" type="object"
@@ -384,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>
@@ -471,6 +612,16 @@ because it is useless and confusing -->
<field name="groups_id" eval="[(6, 0, [ref('base_usability.group_nobody')])]"/>
</record>
<!-- Remove menu entry "Accounting > Reports > PDF Reports" as there are broken -->
<record id="account.menu_finance_legal_statement" model="ir.ui.menu">
<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

@@ -0,0 +1,33 @@
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index b1d8012329d..b8a8e2a673d 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -210,6 +210,7 @@ class account_payment(models.Model):
payment_difference = fields.Monetary(compute='_compute_payment_difference', readonly=True)
payment_difference_handling = fields.Selection([('open', 'Keep open'), ('reconcile', 'Mark invoice as fully paid')], default='open', string="Payment Difference", copy=False)
writeoff_account_id = fields.Many2one('account.account', string="Difference Account", domain=[('deprecated', '=', False)], copy=False)
+ writeoff_analytic_account_id = fields.Many2one('account.analytic.account', string="Difference Analytic Account", copy=False)
# FIXME: ondelete='restrict' not working (eg. cancel a bank statement reconciliation with a payment)
move_line_ids = fields.One2many('account.move.line', 'payment_id', readonly=True, copy=False, ondelete='restrict')
@@ -431,6 +432,7 @@ class account_payment(models.Model):
amount_currency_wo = -abs(amount_currency_wo)
writeoff_line['name'] = _('Counterpart')
writeoff_line['account_id'] = self.writeoff_account_id.id
+ writeoff_line['analytic_account_id'] = self.writeoff_analytic_account_id.id or False
writeoff_line['debit'] = debit_wo
writeoff_line['credit'] = credit_wo
writeoff_line['amount_currency'] = amount_currency_wo
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index 2460458fbaa..4065d8f9952 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -206,6 +206,8 @@
</div>
<field name="writeoff_account_id" string="Post Difference In"
attrs="{'invisible': [('payment_difference_handling','=','open')], 'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
+ <field name="writeoff_analytic_account_id" string="Post Difference In Analytic Account"
+ attrs="{'invisible': [('payment_difference_handling','=','open')]}"/>
</group>
</group>
</sheet>

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

View File

@@ -31,6 +31,7 @@ A group by 'State' is added to module search view.
'security/group.xml',
'security/ir.model.access.csv',
'partner_view.xml',
'partner_bank_view.xml',
'users_view.xml',
'country_view.xml',
'module_view.xml',

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

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_partner_bank_tree" model="ir.ui.view">
<field name="name">base_usability.res.partner.bank.tree</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_tree"/>
<field name="arch" type="xml">
<field name="sequence" position="attributes">
<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>
</odoo>

View File

@@ -19,6 +19,10 @@
<xpath expr="//field[@name='child_ids']/form//field[@name='email']" position="attributes">
<attribute name="widget">email</attribute>
</xpath>
<!-- Show title not only on Contacts -->
<xpath expr="//field[@name='child_ids']/form//field[@name='title']" position="attributes">
<attribute name="attrs"></attribute>
</xpath>
</field>
</record>
@@ -41,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

@@ -22,6 +22,7 @@ def formatLang(
if (
'base.usability.installed' in env and
int_no_digits and
not monetary and
isinstance(value, float) and
dp):
prec = env['decimal.precision'].precision_get(dp)
@@ -33,4 +34,5 @@ def formatLang(
grouping=grouping, monetary=monetary, dp=dp, currency_obj=currency_obj)
return res
report_sxw.rml_parse.formatLang = formatLang

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,37 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
========================
User Authentication Logs
========================
This module adds user authentication logs in Odoo. It logs both authentication success and failures.
Usage
=====
The authentication logs can be seen:
* on the users's form view in the *Auth Logs* tab,
* in the menu *Settings > Technical > Security > Authentication Logs*.
Authentication failure logs are displayed in red. Authentication success logs are displayed in black.
To have read access to the logs, you need to be part of the *Access Rights* group.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/akretion/odoo-usability/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Alexis de Lattre <alexis.delattre@akretion.com>

View File

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

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2017-2018 Akretion France
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Users authentification logs',
'version': '10.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Adds users authentication logs',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'views/res_users_auth_log.xml',
'views/res_users.xml',
'data/ir_cron.xml',
],
'installable': True,
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017-2018 Akretion France
@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="purge_auth_log_cron" model="ir.cron">
<field name="name">Purge old authentication logs</field>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field> <!-- don't limit the number of calls -->
<field name="doall" eval="False"/>
<field name="model" eval="'res.users.auth.log'"/>
<field name="function" eval="'_purge_old_auth_logs'" />
<field name="args" eval="'()'"/>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright 2017-2018 Akretion France
# @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, registry, SUPERUSER_ID
import logging
logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = 'res.users'
auth_log_ids = fields.One2many(
'res.users.auth.log', 'user_id', string='Authentication Logs')
@classmethod
def _login(cls, db, login, password):
user_id = super(ResUsers, cls)._login(db, login, password)
with registry(db).cursor() as cr:
if user_id:
result = 'success'
user_log_id = user_id
else:
# To write a null value, psycopg2 wants None
user_log_id = None
result = 'failure'
cr.execute(
"SELECT id FROM res_users WHERE login=%s", (login, ))
user_select = cr.fetchall()
if user_select:
user_log_id = user_select[0][0]
cr.execute("""
INSERT INTO res_users_auth_log (
create_uid,
create_date,
date,
login,
result,
user_id
) VALUES (
%s, NOW() AT TIME ZONE 'UTC', NOW() AT TIME ZONE 'UTC',
%s, %s, %s)""", (SUPERUSER_ID, login, result, user_log_id))
logger.info('Auth log created for login %s type %s', login, result)
return user_id

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# © 2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
class ResUsersAuthLog(models.Model):
_name = 'res.users.auth.log'
_description = 'Users Authentication Logs'
_order = 'date desc'
_rec_name = 'date'
user_id = fields.Many2one(
'res.users', string='User', ondelete='cascade', readonly=True)
login = fields.Char(string='Login', readonly=True)
date = fields.Datetime(
string='Authentication Date', required=True, readonly=True)
result = fields.Selection([
('success', 'Success'),
('failure', 'Failure'),
], string='Result', required=True, readonly=True)
@api.model
def create(self, vals):
if not self._context.get('authenticate_create'):
raise UserError(_(
"You cannot manually create an authentication log."))
return super(ResUsersAuthLog, self).create(vals)
@api.multi
def write(self, vals):
raise UserError(_("You cannot modify an authentication log."))
@api.model
def _purge_old_auth_logs(self):
expiry_date = datetime.today() - timedelta(days=365)
self._cr.execute(
"DELETE FROM res_users_auth_log WHERE date <= %s", (expiry_date, ))
logger.info('Auth logs older than %s have been purged', expiry_date)

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_res_users_auth_log,Read access to Access rights group,model_res_users_auth_log,base.group_erp_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_res_users_auth_log Read access to Access rights group model_res_users_auth_log base.group_erp_manager 1 0 0 0

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017-2018 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_users_form" model="ir.ui.view">
<field name="name">auth_logs.res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Auth Logs" name="auth_logs">
<field name="auth_log_ids" nolabel="1"/>
</page>
</notebook>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017-2018 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="res_users_auth_log_form" model="ir.ui.view">
<field name="name">res.users.auth.logs.form</field>
<field name="model">res.users.auth.log</field>
<field name="arch" type="xml">
<form string="Authentication Log">
<group name="main">
<field name="date"/>
<field name="user_id"/>
<field name="login"/>
<field name="result"/>
</group>
</form>
</field>
</record>
<record id="res_users_auth_log_tree" model="ir.ui.view">
<field name="name">res.users.auth.logs.tree</field>
<field name="model">res.users.auth.log</field>
<field name="arch" type="xml">
<tree string="Authentication Logs" colors="red:result=='failure'">
<field name="date"/>
<field name="user_id" invisible="not context.get('auth_logs_main_view')"/>
<field name="login" invisible="not context.get('auth_logs_main_view')"/>
<field name="result"/>
</tree>
</field>
</record>
<record id="res_users_auth_log_search" model="ir.ui.view">
<field name="name">res.users.auth.logs.search</field>
<field name="model">res.users.auth.log</field>
<field name="arch" type="xml">
<search string="Search Authentication Logs">
<field name="user_id"/>
<filter name="success" string="Success" domain="[('result', '=', 'success')]"/>
<filter name="failure" string="Failure" domain="[('result', '=', 'failure')]"/>
<group string="Group By" name="groupby">
<filter name="day_groupby" string="Day" context="{'group_by': 'date:day'}"/>
<filter name="week_groupby" string="Week" context="{'group_by': 'date:week'}"/>
<filter name="month_groupby" string="Month" context="{'group_by': 'date:month'}"/>
<filter name="user_groupby" string="User" context="{'group_by': 'user_id'}"/>
<filter name="result_groupby" string="Result" context="{'group_by': 'result'}"/>
</group>
</search>
</field>
</record>
<record id="res_users_auth_log_graph" model="ir.ui.view">
<field name="name">res.users.auth.logs.graph</field>
<field name="model">res.users.auth.log</field>
<field name="arch" type="xml">
<graph string="Analyze Authentication Logs" type="pivot">
<field name="date" type="row" interval="week"/>
<field name="user_id" type="col"/>
</graph>
</field>
</record>
<record id="res_users_auth_log_action" model="ir.actions.act_window">
<field name="name">Authentication Logs</field>
<field name="res_model">res.users.auth.log</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{'auth_logs_main_view': True}</field>
</record>
<menuitem id="res_users_auth_log_menu" action="res_users_auth_log_action"
parent="base.menu_security" sequence="100"/>
</odoo>

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

@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
from . import stock

View File

@@ -22,8 +22,10 @@ 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',
],
'installable': True,
}

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

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class StockPicking(models.Model):
_inherit = 'stock.picking'
carrier_id = fields.Many2one(track_visibility='onchange')

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
<field name="name">delivery_usability.stock.picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form"/>
<field name="arch" type="xml">
<field name="carrier_id" position="attributes">
<!-- Sometimes we have to modify carrier_id when state is done
so remove readonly when state = done from view and add tracking on_change in
field definition -->
<attribute name="attrs">{}</attribute>
</field>
</field>
</record>
</odoo>

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

@@ -103,7 +103,7 @@ class HrExpense(models.Model):
string='Untaxed Amount', currency_field='currency_id',
readonly=True, states={'draft': [('readonly', False)]})
company_currency_id = fields.Many2one(
related='company_id.currency_id', readonly=True, store=True)
related='company_id.currency_id', readonly=True, store=True, compute_sudo=True)
total_amount_company_currency = fields.Monetary(
compute='compute_amount_company_currency', readonly=True,
store=True, string='Total in Company Currency',
@@ -309,7 +309,7 @@ class HrExpenseSheet(models.Model):
responsible_id = fields.Many2one(track_visibility='onchange')
accounting_date = fields.Date(track_visibility='onchange')
company_currency_id = fields.Many2one(
related='company_id.currency_id', readonly=True, store=True)
related='company_id.currency_id', readonly=True, store=True, compute_sudo=True)
total_amount_company_currency = fields.Monetary(
compute='compute_total_company_currency',
currency_field='company_currency_id', readonly=True, store=True,

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