Steps to reproduce the bug:
- Let's consider a company contact C with two children CH1 and CH2
- Create a purchase order for CH1 and a subscription for CH2
- Archive CH1 and CH2
Bug:
The count of purchase orders in C was displaying 0 instead of 2
Same issue for the vendor bills
opw:2360155
closesodoo/odoo#61167
X-original-commit: b3e31f2680534382b048324cfcb8a6f6c457553f
Signed-off-by: Simon Goffin (sig) <sig@openerp.com>
Some change to imporve reminder mail usability:
1. Scheduled date on POL of the PO form now is always editable.
2. Improve tooltip for the reminder email
3. don't show number of days when reminder email not checked on
both PO and partner form.
4. only show "confirm receipt date" button in debug model
5. In the mail, format date by partner.lang
6. Add button to send preview reminder mail
7. On default, receipt_reminder_email on res.partner is False
8. merge expected_date with date_planned (done by FP)
Task 2265912
PR #52693
1. automatically send a reminder mail to vendor to confirm the receipt
date. If confirmed, (confirmed by vendor) will be added next to the
receipt date. If not, vendor can update the date on the portal website.
An warning activity will be set for the purchase representative for this
update.
2. Vendor can also comfirm recieption of the PO when we 'send PO by mail'.
If confirm, (confirmed by vendor) will be added next to the confirmation
date. An filter is added in the PO search view to show all unconfirmed
PO.
Task 2230811
PR #49921
Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
Purpose
=======
To correctly choose in which account a line should be posted,
we have to know if the partner is a customer or a supplier.
Specification
=============
Keep track of the number of account moves "in" and "out"
a partner has. These counts should be based on the posted
account moves. A customer that has been created from the
'Customer' menuitem will have a rank=1. When a customer
invoice will be created for him, the generated account moves
will be taken into account to compute its rank. The most
invoices we have for a partner, the higher his rank is.
Note: To avoid any concurrent update failures on the partner,
if one transaction has already locked a partner row, the count
update will be skipped that time.
This means the values may be approximative in the database!
The exact values will eventually be correctly computed at
the next successfull try.
Known limitation of this approach: The computation ignores
the set of currently selected companies. Actually, storing
context dependent values in the database is a bad practice,
and is avoided in that case by taking all the companies into
account.
Use the stored fields `customer_rank` and `supplier_rank`
to order partners when searching by name. This allows to show
best customers or best suppliers on top.
To choose if best customer or supplier are shown on top,
the context key `res_partner_search_mode` is used.
The context key can take two values: 'customer' or 'supplier'.
This decision partially reverts/revamps 8766f38 to only use
account moves instead of PO and SO
On actions showing partners, set a default filters to menus to
only display customers (customer_rank > 0) if the string is
"Customers", and only suppliers if the string is "Vendors" or
"Suppliers".
TaskID: 2049131
closesodoo/odoo#35942
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This branch is the combination of several optimizations in the ORM:
* store field values once in the cache: the cache reflects more
faithfully the database, only fields that explicitly depend on the
context have an extra indirection in the cache;
* delay recomputations by default: use method `recompute` to explicitly
flush out pending recomputations;
* delay updates in method `write`: updates are stored in a data
structure that can be flushed efficiently to the database with method
`flush` (which also flush out recomputations);
* make method `modified` take advantage of inverse fields to inverse
dependencies;
* filter records by evaluating a domain on records in Python;
* a computed field with `readonly=False` behaves like a normal field
with an onchange method;
* computed fields are computed in superuser mode by default.
Work done by Toufik Ben Jaa, Raphael Collet, Denis Ledoux and Fabien
Pinckaers.
closesodoo/odoo#35659
Signed-off-by: Denis Ledoux <beledouxdenis@users.noreply.github.com>
This commit allows to call `name_search` with context key
`res_partner_search_mode` that can take currently two values: "customer" or
"supplier".
When ordering search results, order partners by the number of SO they have when
the value is "customer" and by the number of PO if the value is "supplier".
Top suppliers/customers are displayed above in a PO/SO partner dropdown.
We have thought about different implementations before selecting this one:
- Keep the 2 boolean flags and automatically set them if a SO or PO is created
for a customer/supplier (helps a bit but doesn't work well for the first
SO/PO). As the onboarding is also a priority, we didn't implement it.
- Instead of filtering on the bool flags, sort the name_search() result according
to the number of orders they already have (e.g. if you search for "Foo" on a
PO, a supplier "SuperFoo" with 32 purchase orders will appear before supplier
"HorribleFoo" that has no PO). Problem is to do this in an efficient manner for
large databases - possibly we could store a "relevance index" based on the
number
of recent orders. Storing those values is an issue too, as it doesn't work well
for multi company database. Indeed, storing values that depend on the context
is certainly not a good idea.
- Same as previous alternative but with no stored columns, rather a JOIN in
name_search. This is the solution that have been kept. The only concern was
about the perfomances. We tested it on our production base, on a sales order,
as we have way much more customers than suppliers. After a deep analysis on
the querry and the bitmap generated by PostgreSQL, we can conclude that the
count(*) works rather efficiently and that the request is not significantly
heavier than without the context key.
Performance Analysis
====================
The following results have been obtained on our production database.
Before this commit
------------------
The original query is the following one:
```
EXPLAIN ANALYSE
SELECT res_partner.id
FROM "res_partner"
WHERE ("res_partner"."customer" = True) AND ("res_partner"."active" = True) AND ((("res_partner"."type" != 'private') OR "res_partner"."type" IS NULL) OR "res_partner"."type" IS NULL ) AND (res_partner.email ilike '%eezee-i%'
OR res_partner.display_name ilike '%eezee-i%'
OR res_partner.ref ilike '%eezee-i%'
OR res_partner.vat ilike '%eezeei%')
-- don't panic, trust postgres bitmap
ORDER BY res_partner.display_name ilike '%eezee-i%' desc,
res_partner.display_name
limit 8;
```
Here is the EXPLAIN ANALYSE returned by PostgreSQL:
```
Limit (cost=1625.50..1625.52 rows=8 width=21) (actual time=13.397..13.404 rows=8 loops=1)
-> Sort (cost=1625.50..1626.32 rows=326 width=21) (actual time=13.395..13.396 rows=8 loops=1)
Sort Key: (((display_name)::text ~~* '%eezee-i%'::text)) DESC, display_name
Sort Method: top-N heapsort Memory: 26kB
-> Bitmap Heap Scan on res_partner (cost=1282.79..1618.98 rows=326 width=21) (actual time=13.025..13.350 rows=56 loops=1)
Recheck Cond: (((email)::text ~~* '%eezee-i%'::text) OR ((display_name)::text ~~* '%eezee-i%'::text) OR ((ref)::text ~~* '%eezee-i%'::text) OR ((vat)::text ~~* '%eezeei%'::text))
Rows Removed by Index Recheck: 1
Filter: (customer AND active AND (((type)::text <> 'private'::text) OR (type IS NULL) OR (type IS NULL)))
Rows Removed by Filter: 1
Heap Blocks: exact=58
-> BitmapOr (cost=1282.79..1282.79 rows=328 width=0) (actual time=12.983..12.983 rows=0 loops=1)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=5.022..5.022 rows=54 loops=1)
Index Cond: ((email)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=4.128..4.128 rows=56 loops=1)
Index Cond: ((display_name)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..321.00 rows=1 width=0) (actual time=2.240..2.240 rows=0 loops=1)
Index Cond: ((ref)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..317.03 rows=4 width=0) (actual time=1.585..1.585 rows=0 loops=1)
Index Cond: ((vat)::text ~~* '%eezeei%'::text)
Planning time: 0.766 ms
Execution time: 13.489 ms
```
After this commit
-----------------
Without the context key, the query looks like this:
```
SELECT res_partner.id
FROM "res_partner"
WHERE ("res_partner"."active" = True) AND ((("res_partner"."type" != 'private') OR "res_partner"."type" IS NULL) OR "res_partner"."type" IS NULL ) AND (res_partner.email ilike '%eezee-i%'
OR res_partner.display_name ilike '%eezee-i%'
OR res_partner.ref ilike '%eezee-i%'
OR res_partner.vat ilike '%eezeei%')
-- don't panic, trust postgres bitmap
GROUP BY res_partner.id
ORDER BY COUNT(*) DESC, res_partner.display_name ilike '%eezee-i%' desc,
res_partner.display_name
limit 8;
And it quite clear when looking at the EXPLAIN ANALYSE that the request
is quite the same, except the aggregation that is made with a quicksort method.
Limit (cost=1645.00..1645.02 rows=8 width=29) (actual time=13.200..13.206 rows=8 loops=1)
-> Sort (cost=1645.00..1645.82 rows=328 width=29) (actual time=13.197..13.198 rows=8 loops=1)
Sort Key: (count(*)) DESC, (((display_name)::text ~~* '%eezee-i%'::text)) DESC, display_name
Sort Method: top-N heapsort Memory: 26kB
-> GroupAggregate (cost=1631.88..1638.44 rows=328 width=29) (actual time=13.090..13.160 rows=56 loops=1)
Group Key: id
-> Sort (cost=1631.88..1632.70 rows=328 width=20) (actual time=13.080..13.084 rows=56 loops=1)
Sort Key: id
Sort Method: quicksort Memory: 29kB
-> Bitmap Heap Scan on res_partner (cost=1282.79..1618.17 rows=328 width=20) (actual time=12.793..13.055 rows=56 loops=1)
Recheck Cond: (((email)::text ~~* '%eezee-i%'::text) OR ((display_name)::text ~~* '%eezee-i%'::text) OR ((ref)::text ~~* '%eezee-i%'::text) OR ((vat)::text ~~* '%eezeei%'::text))
Rows Removed by Index Recheck: 1
Filter: (active AND (((type)::text <> 'private'::text) OR (type IS NULL) OR (type IS NULL)))
Rows Removed by Filter: 1
Heap Blocks: exact=58
-> BitmapOr (cost=1282.79..1282.79 rows=328 width=0) (actual time=12.755..12.755 rows=0 loops=1)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=4.823..4.823 rows=54 loops=1)
Index Cond: ((email)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=3.941..3.941 rows=56 loops=1)
Index Cond: ((display_name)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..321.00 rows=1 width=0) (actual time=2.186..2.186 rows=0 loops=1)
Index Cond: ((ref)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..317.03 rows=4 width=0) (actual time=1.798..1.798 rows=0 loops=1)
Index Cond: ((vat)::text ~~* '%eezeei%'::text)
Planning time: 0.761 ms
Execution time: 13.317 ms
```
With the context key, the query looks like this:
```
EXPLAIN ANALYSE
SELECT res_partner.id
FROM "res_partner"
LEFT JOIN sale_order ON res_partner.id = sale_order.partner_id
WHERE ("res_partner"."active" = True) AND ((("res_partner"."type" != 'private') OR "res_partner"."type" IS NULL) OR "res_partner"."type" IS NULL ) AND (res_partner.email ilike '%eezee-i%'
OR res_partner.display_name ilike '%eezee-i%'
OR res_partner.ref ilike '%eezee-i%'
OR res_partner.vat ilike '%eezeei%')
-- don't panic, trust postgres bitmap
GROUP BY res_partner.id
ORDER BY COUNT(*) DESC, res_partner.display_name ilike '%eezee-i%' desc,
res_partner.display_name
limit 8;
```
The only difference is that a nested loop is made for the left join, as postgreSQL has
correctly identified the dicriminating table. The correct result is obtained within
a similar duration.
```
Limit (cost=2700.68..2700.70 rows=8 width=29) (actual time=12.561..12.568 rows=8 loops=1)
-> Sort (cost=2700.68..2701.50 rows=328 width=29) (actual time=12.559..12.560 rows=8 loops=1)
Sort Key: (count(*)) DESC, (((res_partner.display_name)::text ~~* '%eezee-i%'::text)) DESC, res_partner.display_name
Sort Method: top-N heapsort Memory: 26kB
-> GroupAggregate (cost=2687.56..2694.12 rows=328 width=29) (actual time=12.449..12.527 rows=56 loops=1)
Group Key: res_partner.id
-> Sort (cost=2687.56..2688.38 rows=328 width=20) (actual time=12.408..12.425 rows=305 loops=1)
Sort Key: res_partner.id
Sort Method: quicksort Memory: 42kB
-> Nested Loop Left Join (cost=1283.21..2673.86 rows=328 width=20) (actual time=11.629..12.340 rows=305 loops=1)
-> Bitmap Heap Scan on res_partner (cost=1282.79..1618.17 rows=328 width=20) (actual time=11.596..11.862 rows=56 loops=1)
Recheck Cond: (((email)::text ~~* '%eezee-i%'::text) OR ((display_name)::text ~~* '%eezee-i%'::text) OR ((ref)::text ~~* '%eezee-i%'::text) OR ((vat)::text ~~* '%eezeei%'::text))
Rows Removed by Index Recheck: 1
Filter: (active AND (((type)::text <> 'private'::text) OR (type IS NULL) OR (type IS NULL)))
Rows Removed by Filter: 1
Heap Blocks: exact=58
-> BitmapOr (cost=1282.79..1282.79 rows=328 width=0) (actual time=11.560..11.560 rows=0 loops=1)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=4.724..4.724 rows=54 loops=1)
Index Cond: ((email)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..322.22 rows=162 width=0) (actual time=3.825..3.825 rows=56 loops=1)
Index Cond: ((display_name)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..321.00 rows=1 width=0) (actual time=1.752..1.752 rows=0 loops=1)
Index Cond: ((ref)::text ~~* '%eezee-i%'::text)
-> Bitmap Index Scan on res_partner_name_tgm_idx_gin (cost=0.00..317.03 rows=4 width=0) (actual time=1.254..1.254 rows=0 loops=1)
Index Cond: ((vat)::text ~~* '%eezeei%'::text)
-> Index Only Scan using sale_order_partner_id_index on sale_order (cost=0.42..2.74 rows=48 width=4) (actual time=0.006..0.007 rows=5 loops=56)
Index Cond: (partner_id = res_partner.id)
Heap Fetches: 12
Planning time: 1.056 ms
Execution time: 13.791 ms
```
TaskID: 2031147
Co-authored-by: Yannick Tivisse <yti@odoo.com>
Multi is the default api for methods, it is not necessary to explicitly
decorate methods with it, adds clutter and most people use it because
they see that the rest of the code uses it.
Done with `find . -type f -name '*.py' | xargs sed -i '/@api.multi/d'`
This commit merges the following models
* account.invoice and account.move
* account.invoice.line and account.move.line
* account.voucher and account.move
* account.voucher.line and account.move.line
It was the opportunity for a big cleanup of the code, so it also restructures the whole account module, its different models/fields, the tests etc. for a better world and a better code readability.
==== Rationale ====
The rationale of this huge change is that we want journal entries / invoices to be easily edited, and changes reflected in the other model. It's a HUGE feature and very strategic for the fiduciary companies. For example, changing the account of a journal entry needs to be automatically reflected on the related invoice.
The same reasoning applies to sale/purchase vouchers.
==== Changes made in features =====
When creating an invoice, you are now creating a journal entry directly.
--> The object account.invoice no longer exists.
In the same fashion when creating an invoice line, you're now adding journal items directly in the journal entry representing the invoice. If this invoice line has some tax, it may create additional journal items as well.
--> The models account.invoice.line & account.invoice.tax no longer exist
Identically, when creating a sale/purchase receipt with its lines, you are now creating a journal entry directly and there's no more usability difference between encoding a receipt or an invoice.
--> The object account.voucher no longer exists.
--> The object account.voucher.line no longer exists.
--> The whole account_voucher module no longer exists.
Positive side-effects coming from these changes are
* draft invoices/bills/sale or purchase receipts now create a draft accounting entry. Validate these objects now simply post its journal entry. That means that draft invoices/bills/sale or purchase receipt can straightforwardly be included in reporting or budgets.
* opening a journal entry in form view will now always open the correct view: if it's a sale/purchase journal entry we will have a customer invoice/vendor bill view or a sale/purchase receipt view, whatever the menu we're coming from.
* code & business logic simplification. It is also condensed in a single place instead of being partially duplicated on invoices, vouchers and journal entries.
There should be no feature loss, except the one allowing to group multiple journal items together based on the same product during the invoice validation.
==== Changes made in models =====
* account.invoice: model removed. Instead, now use account.move with following mapping
field (account.invoice) field (account.move)
----------------------- --------------------
name invoice_payment_ref
number name
reference ref
comment narration
user_id invoice_user_id
amount_ total_company_signed amount_total_signed
residual amount_residual
state state + invoice_payment_state /!\ selection changed
date_invoice invoice_date
date_due invoice_date_due
sent invoice_sent
origin invoice_origin
payment_term_id invoice_payment_term_id
partner_bank_id invoice_partner_bank_id
incoterm_id invoice_incoterm_id
vendor_bill_id invoice_vendor_bill_id
source_email invoice_source_email
vendor_display_name invoice_vendor_display_name
invoice_icon invoice_vendor_icon
cash_rounding_id invoice_cash_rounding_id
sequence_number_next invoice_sequence_number_next
sequence_number_next_prefix invoice_sequence_number_next_prefix
'invoices' subset of account.move can be accessed by using the selection field 'type' or one of the many helpers like is_invoice()
* account.move: now has a valid state 'cancel' that has to be excluded from all business logic
* account.move: field 'amount' renamed into 'amount_total'
* account.move: field 'reverse_entry_id' renamed into 'reversed_entry_id'
* account.move.line: now has a field 'display_type' that has to be excluded from all business logic, in order to support invoice layouting
* account.invoice.line: model removed. Instead, now use account.move.line with following mapping
field (account.invoice.line) field (account.move.line)
---------------------------- -------------------------
invoice_id move_id
uom_id product_uom_id
invoice_line_tax_ids tax_ids
account_analytic_id analytic_account_id
'invoice lines' subset of all account.move.line from a journal entry can be accessed by using the boolean field 'exclude_from_invoice_tab'
* account.invoice.tax: model removed. Instead, now use account.move.line with following mapping
field (account.invoice.tax) field (account.move.line)
--------------------------- -------------------------
invoice_id move_id
account_analytic_id analytic_account_id
amount price_unit
base tax_base_amount
'tax lines' subset of all account.move.line from a journal entry can be accessed by using the relational field 'tax_line_id'
* account.invoice.confirm: model removed. Instead, now use the 'post()' function of account.move
* account.invoice.refund: model removed. Instead, now use account.move.reversal to reverse the entries with the same options as we had for invoices
* account.voucher: model removed. Instead, now use account.move of type in ['out_receipt', 'in_receipt]
* account.voucher.line: model removed. Instead, now use account.move.line
==== Changes made in functions ====
* on account.move, method _run_post_draft_to_post() renamed into _autopost_draft_entries()
* on account.move, method action_account_invoice_payment() renamed into action_invoice_register_payment()
* on account.move, method action_invoice_reconcile_to_check() renamed into action_open_matching_suspense_moves()
* on account.move, method _get_domain_edition_mode_available() renamed into _get_domain_matching_supsense_moves()
* on account.move, method _get_intrastat_country_id() renamed into _get_invoice_intrastat_country_id()
* on account.move.line, method _get_domain_for_edition_mode() renamed into _get_suspense_moves_domain()
* in account.bank.statement, contextual key 'edition_mode' renamed into 'suspense_moves_mode'
Was task 1917430
When loading a partner form view, the ORM prefetches the computed fields
purchase_order_count and supplier_invoice_count for all the related
partners as well (parent, children, ..).
Instead of reading the orders and invoices once per partner, we group
them all together.
opw 1919933
closesodoo/odoo#29788
If a supplier had a refund, it was not included in the supplier invoice count
nor in the results in the statbutton
On the customer side, fields like total_invoiced works on both the out_invoice
and out_refund
No advantage to group a search on purchase.order and another one on
account.invoice.
Avoid potential access errors if a user has access to one model but not the
other one.
Closes#25761
When reinitializing modules it is impossible to create partners if
purchase is installed because purchase_warn field has a required
column but no default value if the module does not depend from
purchase. Use case is trying to run test_mail tests with purchase
installed.
As this field is not business critical it is now not required. As there
is a default value behavior should not change for users.
See also 71181a0e39, dd47abdcc3 and 06a02ae4c2.