19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -10,37 +10,14 @@ pip install odoo-bringout-oca-ocb-purchase
## Dependencies
This addon depends on:
- account
## Manifest Information
- **Name**: Purchase
- **Version**: 1.2
- **Category**: Inventory/Purchase
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `purchase`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/purchase
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -4,4 +4,4 @@
from . import controllers
from . import models
from . import report
from . import populate
from . import wizard

View file

@ -4,7 +4,7 @@
{
'name': 'Purchase',
'version': '1.2',
'category': 'Inventory/Purchase',
'category': 'Supply Chain/Purchase',
'sequence': 35,
'summary': 'Purchase orders, tenders and agreements',
'website': 'https://www.odoo.com/app/purchase',
@ -18,6 +18,7 @@
'data/ir_cron_data.xml',
'report/purchase_reports.xml',
'views/purchase_views.xml',
'views/purchase_bill_line_match_views.xml',
'views/res_config_settings_views.xml',
'views/product_views.xml',
'views/res_partner_views.xml',
@ -28,8 +29,9 @@
'views/portal_templates.xml',
'report/purchase_order_templates.xml',
'report/purchase_quotation_templates.xml',
'views/product_packaging_views.xml',
'views/analytic_account_views.xml',
'wizard/bill_to_po_wizard_views.xml',
'data/purchase_tour.xml',
],
'demo': [
'data/purchase_demo.xml',
@ -38,16 +40,23 @@
'application': True,
'assets': {
'web.assets_backend': [
'purchase/static/src/components/**/*',
'purchase/static/src/product_catalog/**/*',
'purchase/static/src/toaster_button/*',
'purchase/static/src/views/*.js',
'purchase/static/src/js/purchase_toaster_button.js',
'purchase/static/src/js/tours/purchase.js',
'purchase/static/src/js/tours/purchase_steps.js',
'purchase/static/src/**/*.xml',
'purchase/static/src/**/*.scss',
],
'web.assets_frontend': [
'purchase/static/src/js/purchase_datetimepicker.js',
'purchase/static/src/js/purchase_portal_sidebar.js',
'purchase/static/src/interactions/**/*',
'purchase/static/src/scss/purchase_portal.scss',
],
'web.assets_tests': [
'purchase/static/tests/tours/**/*',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
@ -8,7 +7,7 @@ from datetime import datetime
from odoo import http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request, Response
from odoo.tools import image_process
from odoo.tools.image import image_process
from odoo.tools.translate import _
from odoo.addons.portal.controllers import portal
from odoo.addons.portal.controllers.portal import pager as portal_pager
@ -22,11 +21,11 @@ class CustomerPortal(portal.CustomerPortal):
if 'rfq_count' in counters:
values['rfq_count'] = PurchaseOrder.search_count([
('state', 'in', ['sent'])
]) if PurchaseOrder.check_access_rights('read', raise_exception=False) else 0
]) if PurchaseOrder.has_access('read') else 0
if 'purchase_count' in counters:
values['purchase_count'] = PurchaseOrder.search_count([
('state', 'in', ['purchase', 'done', 'cancel'])
]) if PurchaseOrder.check_access_rights('read', raise_exception=False) else 0
('state', 'in', ['purchase', 'cancel'])
]) if PurchaseOrder.has_access('read') else 0
return values
def _get_purchase_searchbar_sortings(self):
@ -130,10 +129,9 @@ class CustomerPortal(portal.CustomerPortal):
page, date_begin, date_end, sortby, filterby,
[],
{
'all': {'label': _('All'), 'domain': [('state', 'in', ['purchase', 'done', 'cancel'])]},
'all': {'label': _('All'), 'domain': [('state', 'in', ['purchase', 'cancel'])]},
'purchase': {'label': _('Purchase Order'), 'domain': [('state', '=', 'purchase')]},
'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]},
'done': {'label': _('Locked'), 'domain': [('state', '=', 'done')]},
},
'all',
"/my/purchase",
@ -151,13 +149,11 @@ class CustomerPortal(portal.CustomerPortal):
report_type = kw.get('report_type')
if report_type in ('html', 'pdf', 'text'):
return self._show_report(model=order_sudo, report_type=report_type, report_ref='purchase.action_report_purchase_order', download=kw.get('download'))
report_ref = 'purchase.report_purchase_quotation' if order_sudo.state in ['rfq','sent'] else 'purchase.action_report_purchase_order'
return self._show_report(model=order_sudo, report_type=report_type, report_ref=report_ref, download=kw.get('download'))
confirm_type = kw.get('confirm')
if confirm_type == 'reminder':
order_sudo.confirm_reminder_mail(kw.get('confirmed_date'))
if confirm_type == 'reception':
order_sudo._confirm_reception_mail()
if kw.get('acknowledge'):
order_sudo.action_acknowledge()
values = self._purchase_order_get_page_view_values(order_sudo, access_token, **kw)
update_date = kw.get('update')
@ -167,7 +163,7 @@ class CustomerPortal(portal.CustomerPortal):
return request.render("purchase.portal_my_purchase_order_update_date", values)
return request.render("purchase.portal_my_purchase_order", values)
@http.route(['/my/purchase/<int:order_id>/update'], type='http', methods=['POST'], auth="public", website=True)
@http.route(['/my/purchase/<int:order_id>/update'], type='jsonrpc', auth="public", website=True)
def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw):
"""User update scheduled date on purchase order line.
"""
@ -196,3 +192,31 @@ class CustomerPortal(portal.CustomerPortal):
if updated_dates:
order_sudo._update_date_planned_for_lines(updated_dates)
return Response(status=204)
@http.route(['/my/purchase/<int:order_id>/download_edi'], auth="public", website=True)
def portal_my_purchase_order_download_edi(self, order_id=None, access_token=None, **kw):
"""An endpoint to download EDI file representation.
"""
try:
order_sudo = self._document_check_access('purchase.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
builders = order_sudo._get_edi_builders()
# This handles only one builder for now, more can be added in the future
# TODO: add builder choice on modal
if len(builders) == 0:
return request.redirect('/my')
builder = builders[0]
xml_content = builder._export_order(order_sudo)
download_name = builder._export_invoice_filename(order_sudo) # works even if it's a SO or PO
http_headers = [
('Content-Type', 'text/xml'),
('Content-Length', len(xml_content)),
('Content-Disposition', f'attachment; filename={download_name}')
]
return request.make_response(xml_content, headers=http_headers)

View file

@ -9,7 +9,7 @@
<div>
<p class="tip_title">Tip: How to keep late receipts under control?</p>
<p class="tip_content">When creating a purchase order, have a look at the vendor's <i>On Time Delivery</i> rate: the percentage of products shipped on time. If it is too low, activate the <i>automated reminders</i>. A few days before the due shipment, Odoo will send the vendor an email to ask confirmation of shipment dates and keep you informed in case of any delays. To get the vendor's performance statistics, click on the OTD rate.</p>
<img src="https://download.odoocdn.com/digests/purchase/static/src/img/OTDPurchase.gif" class="illustration_border" />
<img src="https://download.odoocdn.com/digests/purchase/static/src/img/milk-OTDPurchase.gif" width="540" class="illustration_border" />
</div>
</field>
</record>

View file

@ -5,8 +5,6 @@
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall">1</field>
<field name="model_id" ref="model_purchase_order"/>
<field name="state">code</field>
<field name="code">model._send_reminder_mail()</field>

View file

@ -5,7 +5,8 @@
<field name="name">Purchase: Request For Quotation</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="subject">{{ object.company_id.name }} Order (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="partner_to" eval="False"/>
<field name="use_default_to" eval="True"/>
<field name="description">Sent manually to vendor to request a quotation</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
@ -26,13 +27,11 @@
Best regards,
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
<div>--<br/><t t-out="object.user_id.signature or ''">Mitchell Admin</t></div>
</t>
</p>
</div></field>
<field name="report_template" ref="report_purchase_quotation"/>
<field name="report_name">RFQ_{{ (object.name or '').replace('/','_') }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="report_template_ids" eval="[(4, ref('purchase.report_purchase_quotation'))]"/>
<field name="auto_delete" eval="True"/>
</record>
@ -40,7 +39,8 @@
<field name="name">Purchase: Purchase Order</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="subject">{{ object.company_id.name }} Order (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="partner_to" eval="False"/>
<field name="use_default_to" eval="True"/>
<field name="description">Sent to vendor with the purchase order in attachment</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
@ -58,20 +58,23 @@
from <t t-out="object.company_id.name or ''">YourCompany</t>.
<br/><br/>
<t t-if="object.date_planned">
The receipt is expected for <span style="font-weight:bold;" t-out="format_date(object.get_localized_date_planned()) or ''">05/05/2021</span>.
The receipt is expected for <span style="font-weight:bold;" t-out="format_date(object.date_planned) or ''">05/05/2021</span>.
<br/><br/>
Could you please acknowledge the receipt of this order?
<br/><br/>
<a t-att-href="object.get_acknowledge_url()"
target="_blank"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Acknowledge</a>
</t>
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
<div>--<br/><t t-out="object.user_id.signature or ''">Mitchell Admin</t></div>
</t>
<br/><br/>
</p>
</div></field>
<field name="report_template" ref="action_report_purchase_order"/>
<field name="report_name">PO_{{ (object.name or '').replace('/','_') }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="report_template_ids" eval="[(4, ref('purchase.action_report_purchase_order'))]"/>
<field name="auto_delete" eval="True"/>
</record>
@ -80,7 +83,8 @@
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted) }}</field>
<field name="subject">{{ object.company_id.name }} Order (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="partner_to" eval="False"/>
<field name="use_default_to" eval="True"/>
<field name="description">Sent to vendors before expected arrival, based on the purchase order setting</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
@ -96,22 +100,26 @@
</t>
is expected for
<t t-if="object.date_planned">
<span style="font-weight:bold;" t-out="format_date(object.get_localized_date_planned()) or ''">05/05/2021</span>.
<span style="font-weight:bold;" t-out="format_date(object.date_planned) or ''">05/05/2021</span>.
</t>
<t t-else="">
<span style="font-weight:bold;">undefined</span>.
</t>
Could you please confirm it will be delivered on time?
<br/><br/>
<a t-att-href="object.get_acknowledge_url()"
target="_blank"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Acknowledge</a>
<br/><br/>
<t t-if="not is_html_empty(object.user_id.signature)">
<br/><br/>
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
<div>--<br/><t t-out="object.user_id.signature or ''">Mitchell Admin</t></div>
</t>
<br/><br/>
</p>
</div></field>
<field name="report_template" ref="action_report_purchase_order"/>
<field name="report_name">PO_{{ (object.name or '').replace('/','_') }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="report_template_ids" eval="[(4, ref('purchase.action_report_purchase_order'))]"/>
<field name="auto_delete" eval="True"/>
</record>

View file

@ -7,7 +7,7 @@
<ul>
<li><t t-esc="line.product_id.display_name"/>:</li>
Ordered Quantity: <t t-esc="line.product_qty" /> -&gt; <t t-esc="float(product_qty)"/><br/>
<t t-if='line.order_id.product_id.type in ("consu", "product")'>
<t t-if='line.order_id.product_id.type != "consu"'>
Received Quantity: <t t-esc="line.qty_received" /><br/>
</t>
Billed Quantity: <t t-esc="line.qty_invoiced"/>

View file

@ -12,11 +12,6 @@
<field name="default" eval="False"/>
<field name="res_model">purchase.order</field>
</record>
<record id="mt_rfq_done" model="mail.message.subtype">
<field name="name">RFQ Done</field>
<field name="default" eval="False"/>
<field name="res_model">purchase.order</field>
</record>
<record id="mt_rfq_sent" model="mail.message.subtype">
<field name="name">RFQ Sent</field>
<field name="default" eval="False"/>
@ -42,18 +37,7 @@
<field name="code">action = records.action_share()</field>
</record>
<!-- Default value for company_dependant field -->
<record forcecreate="True" id="receipt_reminder_email" model="ir.property">
<field name="name">receipt_reminder_email</field>
<field name="type" eval="'boolean'"/>
<field name="fields_id" search="[('model','=','res.partner'),('name','=','receipt_reminder_email')]"/>
<field eval="False" name="value"/>
</record>
<record forcecreate="True" id="reminder_date_before_receipt" model="ir.property">
<field name="name">reminder_date_before_receipt</field>
<field name="type" eval="'integer'"/>
<field name="fields_id" search="[('model','=','res.partner'),('name','=','reminder_date_before_receipt')]"/>
<field eval="1" name="value"/>
</record>
<!-- Fallback value for company_dependant field -->
<function model="ir.default" name="set" eval="('res.partner', 'reminder_date_before_receipt', 1)"/>
</data>
</odoo>

View file

@ -3,7 +3,11 @@
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field eval="[(4, ref('group_purchase_user'))]" name="groups_id"/>
<field name="group_ids" eval="[(3, ref('purchase.group_purchase_manager'))]"/>
</record>
<record id="base.default_user_group" model="res.groups">
<field name="implied_ids" eval="[(4, ref('purchase.group_purchase_manager'))]"/>
</record>
<record id="base.res_partner_1" model="res.partner">
@ -28,21 +32,21 @@
'name': obj().env.ref('product.product_delivery_01').partner_ref,
'price_unit': 79.80,
'product_qty': 15.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=3)}),
(0, 0, {
'product_id': ref('product.product_product_25'),
'name': obj().env.ref('product.product_product_25').partner_ref,
'price_unit': 286.70,
'product_qty': 5.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=3)}),
(0, 0, {
'product_id': ref('product.product_product_27'),
'name': obj().env.ref('product.product_product_27').partner_ref,
'price_unit': 99.00,
'product_qty': 4.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=3)})
]"/>
</record>
@ -57,14 +61,14 @@
'name': obj().env.ref('product.product_delivery_02').partner_ref,
'price_unit': 132.50,
'product_qty': 20.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=1)}),
(0, 0, {
'product_id': ref('product.product_delivery_01'),
'name': obj().env.ref('product.product_delivery_01').partner_ref,
'price_unit': 89.0,
'product_qty': 5.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=1)}),
]"/>
</record>
@ -79,7 +83,7 @@
'name': obj().env.ref('product.product_product_2').partner_ref,
'price_unit': 25.50,
'product_qty': 10.0,
'product_uom': ref('uom.product_uom_hour'),
'product_uom_id': ref('uom.product_uom_hour'),
'date_planned': DateTime.today() + relativedelta(days=1)}),
]"/>
</record>
@ -94,21 +98,21 @@
'name': obj().env.ref('product.product_delivery_02').partner_ref,
'price_unit': 85.50,
'product_qty': 6.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=5)}),
(0, 0, {
'product_id': ref('product.product_product_20'),
'name': obj().env.ref('product.product_product_20').partner_ref,
'price_unit': 1690.0,
'product_qty': 5.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=5)}),
(0, 0, {
'product_id': ref('product.product_product_6'),
'name': obj().env.ref('product.product_product_6').partner_ref,
'price_unit': 800.0,
'product_qty': 7.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today() + relativedelta(days=5)})
]"/>
</record>
@ -127,14 +131,14 @@
'name': obj().env.ref('product.product_product_22').partner_ref,
'price_unit': 2010.0,
'product_qty': 3.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
(0, 0, {
'product_id': ref('product.product_product_24'),
'name': obj().env.ref('product.product_product_24').partner_ref,
'price_unit': 876.0,
'product_qty': 3.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
]"/>
</record>
@ -149,21 +153,21 @@
'name': obj().env.ref('product.product_delivery_02').partner_ref,
'price_unit': 58.0,
'product_qty': 9.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
(0, 0, {
'product_id': ref('product.product_delivery_01'),
'name': obj().env.ref('product.product_delivery_01').partner_ref,
'price_unit': 65.0,
'product_qty': 3.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
(0, 0, {
'product_id': ref('product.consu_delivery_01'),
'name': obj().env.ref('product.consu_delivery_01').partner_ref,
'price_unit': 154.5,
'product_qty': 4.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
]"/>
</record>
@ -178,18 +182,86 @@
'name': obj().env.ref('product.product_product_12').partner_ref,
'price_unit': 130.5,
'product_qty': 5.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
(0, 0, {
'product_id': ref('product.product_delivery_02'),
'name': obj().env.ref('product.product_delivery_02').partner_ref,
'price_unit': 38.0,
'product_qty': 15.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom_id': ref('uom.product_uom_unit'),
'date_planned': DateTime.today()}),
]"/>
</record>
<record id="purchase_order_8" model="purchase.order">
<field name="partner_id" ref="base.res_partner_1"/>
<field name="user_id" ref="base.user_admin"/>
<field name="state">purchase</field>
<field name="create_date" eval="DateTime.today() - relativedelta(days=20)"/>
<field name="date_order" eval="DateTime.today() - relativedelta(days=5)"/>
<field name="date_approve" eval="DateTime.today() - relativedelta(days=9)"/>
<field name="order_line" model="purchase.order.line" eval="[(5, 0, 0),
(0, 0, {
'product_id': ref('product.product_product_16'),
'name': 'Drawer Black',
'price_unit': 280.80,
'product_qty': 15.0,
'product_uom_id': ref('uom.product_uom_dozen'),
'date_planned': time.strftime('%Y-%m-%d')}),
(0, 0, {
'product_id': ref('product.product_product_20'),
'name': 'Flipover',
'price_unit': 450.70,
'product_qty': 5.0,
'product_uom_id': ref('uom.product_uom_dozen'),
'date_planned': time.strftime('%Y-%m-%d')})
]"/>
</record>
<record id="purchase_order_9" model="purchase.order">
<field name="partner_id" ref="base.res_partner_3"/>
<field name="user_id" ref="base.user_admin"/>
<field name="state">purchase</field>
<field name="create_date" eval="DateTime.today() - relativedelta(days=20)"/>
<field name="date_order" eval="DateTime.today() - relativedelta(days=15)"/>
<field name="date_approve" eval="DateTime.today() - relativedelta(days=5)"/>
<field name="order_line" model="purchase.order.line" eval="[(5, 0, 0),
(0, 0, {
'product_id': ref('product.product_product_8'),
'name': 'Large Desk',
'price_unit': 500.00,
'product_qty': 20.0,
'product_uom_id': ref('uom.product_uom_dozen'),
'date_planned': time.strftime('%Y-%m-%d')}),
(0, 0, {
'product_id': ref('product.product_product_5'),
'name': 'Corner Desk Right Sit',
'price_unit': 500.0,
'product_qty': 5.0,
'product_uom_id': ref('uom.product_uom_dozen'),
'date_planned': time.strftime('%Y-%m-%d')}),
]"/>
</record>
<record id="purchase_order_10" model="purchase.order">
<field name="partner_id" ref="base.res_partner_12"/>
<field name="user_id" ref="base.user_admin"/>
<field name="state">purchase</field>
<field name="create_date" eval="DateTime.today() - relativedelta(days=20)"/>
<field name="date_order" eval="DateTime.today() - relativedelta(days=15)"/>
<field name="date_approve" eval="DateTime.today() - relativedelta(days=18)"/>
<field name="order_line" model="purchase.order.line" eval="[(5, 0, 0),
(0, 0, {
'product_id': ref('product.product_product_12'),
'name': 'Office Chair Black',
'price_unit': 250.50,
'product_qty': 10.0,
'product_uom_id': ref('uom.product_uom_dozen'),
'date_planned': time.strftime('%Y-%m-%d')}),
]"/>
</record>
<record id="purchase_activity_1" model="mail.activity">
<field name="res_id" ref="purchase.purchase_order_2"/>
<field name="res_model_id" ref="purchase.model_purchase_order"/>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_tour" model="web_tour.tour">
<field name="name">purchase_tour</field>
<field name="sequence">40</field>
</record>
</odoo>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,20 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase
#
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Project-Id-Version: Odoo Server saas~16.4+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-06 13:31+0000\n"
"PO-Revision-Date: 2022-09-22 05:54+0000\n"
"Language-Team: Tamil (https://app.transifex.com/odoo/teams/41243/ta/)\n"
"POT-Creation-Date: 2023-08-15 09:57+0000\n"
"PO-Revision-Date: 2023-08-15 09:57+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en_AU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ta\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Plural-Forms: \n"
#. module: purchase
#: model:ir.actions.report,print_report_name:purchase.action_report_purchase_order
@ -37,7 +38,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "%(amount)s due %(date)s"
msgstr ""
@ -45,21 +45,18 @@ msgstr ""
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "%(product)s from %(original_receipt_date)s to %(new_receipt_date)s"
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "%s confirmed the receipt will take place on %s."
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "%s modified receipt dates for the following products:"
msgstr ""
@ -106,7 +103,7 @@ msgid ""
" </t>\n"
" is expected for \n"
" <t t-if=\"object.date_planned\">\n"
" <span style=\"font-weight:bold;\" t-out=\"format_date(object.get_localized_date_planned()) or ''\">05/05/2021</span>.\n"
" <span style=\"font-weight:bold;\" t-out=\"format_date(object.date_planned) or ''\">05/05/2021</span>.\n"
" </t>\n"
" <t t-else=\"\">\n"
" <span style=\"font-weight:bold;\">undefined</span>.\n"
@ -139,7 +136,7 @@ msgid ""
" from <t t-out=\"object.company_id.name or ''\">YourCompany</t>. \n"
" <br><br>\n"
" <t t-if=\"object.date_planned\">\n"
" The receipt is expected for <span style=\"font-weight:bold;\" t-out=\"format_date(object.get_localized_date_planned()) or ''\">05/05/2021</span>.\n"
" The receipt is expected for <span style=\"font-weight:bold;\" t-out=\"format_date(object.date_planned) or ''\">05/05/2021</span>.\n"
" <br><br>\n"
" Could you please acknowledge the receipt of this order?\n"
" </t>\n"
@ -201,11 +198,6 @@ msgstr ""
msgid "<i class=\"fa fa-fw fa-check\"/> <b>Paid</b>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<i class=\"fa fa-fw fa-check\"/> <b>Reversed</b>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<i class=\"fa fa-fw fa-clock-o\"/> <b>Waiting Payment</b>"
@ -259,14 +251,6 @@ msgid ""
" <span class=\"d-block d-md-none\">Ref.</span>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid ""
"<span class=\"fa fa-lg fa-building-o\" title=\"Values set here are company-"
"specific.\" aria-label=\"Values set here are company-specific.\" "
"groups=\"base.group_multi_company\" role=\"img\"/>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid ""
@ -294,13 +278,6 @@ msgid ""
"[('mail_reminder_confirmed', '=', False)]}\">(confirmed by vendor)</span>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid ""
"<span groups=\"account.group_show_line_subtotals_tax_excluded\">Amount</span>\n"
" <span groups=\"account.group_show_line_subtotals_tax_included\">Total Price</span>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.view_partner_property_form
msgid "<span> day(s) before</span>"
@ -311,10 +288,20 @@ msgstr ""
msgid "<span>Ask confirmation</span>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<span>Tax excl.</span>"
msgstr "<span>GST excl.</span>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<span>Tax incl.</span>"
msgstr "<span>GST incl.</span>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<span>Taxes</span>"
msgstr ""
msgstr "<span>GST</span>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
@ -337,11 +324,6 @@ msgstr ""
msgid "<strong class=\"text-muted\">Purchase Representative</strong>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.report_purchaseorder_document
msgid "<strong>Amount</strong>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
msgid "<strong>Confirmation Date:</strong>"
@ -401,10 +383,20 @@ msgstr ""
msgid "<strong>Shipping address:</strong>"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.report_purchaseorder_document
msgid "<strong>Tax excl.</strong>"
msgstr "<strong>GST excl.</strong>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.report_purchaseorder_document
msgid "<strong>Tax incl.</strong>"
msgstr "<strong>GST incl.</strong>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.report_purchaseorder_document
msgid "<strong>Taxes</strong>"
msgstr ""
msgstr "<strong>GST</strong>"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.track_po_line_template
@ -439,7 +431,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "A sample email has been sent to %s."
msgstr ""
@ -466,7 +457,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Accept"
msgstr ""
@ -478,7 +468,7 @@ msgstr ""
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order_line__qty_received_method
msgid ""
"According to product configuration, the received quantity can be automatically computed by mechanism :\n"
"According to product configuration, the received quantity can be automatically computed by mechanism:\n"
" - Manual: the quantity is set manually on the line\n"
" - Stock Moves: the quantity comes from confirmed pickings\n"
msgstr ""
@ -536,7 +526,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Add some products or services to your quotation."
msgstr ""
@ -548,35 +537,30 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/controllers/portal.py:0
#, python-format
msgid "All"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "All Draft RFQs"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "All Late RFQs"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "All RFQs"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "All Waiting RFQs"
msgstr ""
@ -602,28 +586,19 @@ msgid ""
"order line."
msgstr ""
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_report__avg_days_to_purchase
msgid ""
"Amount of time between purchase approval and document creation date. Due to "
"a hack needed to calculate this, every record will show the "
"same average value, therefore only use this as an aggregated value with "
"group_operator=avg"
msgstr ""
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_report__delay
msgid "Amount of time between purchase approval and order by date."
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__analytic_distribution
msgid "Analytic"
#: model:ir.model,name:purchase.model_account_analytic_account
msgid "Analytic Account"
msgstr ""
#. module: purchase
#: model:ir.model,name:purchase.model_account_analytic_account
msgid "Analytic Account"
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__analytic_distribution
msgid "Analytic Distribution"
msgstr ""
#. module: purchase
@ -693,6 +668,7 @@ msgid "Automatically remind the receipt date to your vendors"
msgstr ""
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order__receipt_reminder_email
#: model:ir.model.fields,help:purchase.field_res_partner__receipt_reminder_email
#: model:ir.model.fields,help:purchase.field_res_users__receipt_reminder_email
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
@ -706,15 +682,9 @@ msgstr ""
msgid "Average Cost"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_report__avg_days_to_purchase
msgid "Average Days to Purchase"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Avg Order Value"
msgstr ""
@ -776,6 +746,8 @@ msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__user_id
#: model:ir.model.fields,field_description:purchase.field_res_partner__buyer_id
#: model:ir.model.fields,field_description:purchase.field_res_users__buyer_id
msgid "Buyer"
msgstr ""
@ -801,7 +773,6 @@ msgstr ""
#: code:addons/purchase/controllers/portal.py:0
#: model:ir.model.fields.selection,name:purchase.selection__purchase_order__state__cancel
#: model:ir.model.fields.selection,name:purchase.selection__purchase_report__state__cancel
#, python-format
msgid "Cancelled"
msgstr ""
@ -813,7 +784,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Cannot delete a purchase order line which is in state '%s'."
msgstr ""
@ -850,7 +820,6 @@ msgstr ""
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Compose Email"
msgstr ""
@ -867,7 +836,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Confirm"
msgstr ""
@ -889,7 +857,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Confirm your purchase."
msgstr ""
@ -1000,7 +967,6 @@ msgstr ""
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Date Updated"
msgstr ""
@ -1012,7 +978,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Days"
msgstr ""
@ -1078,11 +1043,6 @@ msgstr ""
msgid "Display Type"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid "Documentation"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_account_analytic_applicability__business_domain
msgid "Domain"
@ -1137,7 +1097,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Extra line with %s "
msgstr ""
@ -1170,8 +1129,13 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/account_invoice.py:0
#, python-format
msgid "From %s document"
msgid "From %s"
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/account_invoice.py:0
msgid "From Electronic Document"
msgstr ""
#. module: purchase
@ -1274,14 +1238,12 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/product.py:0
#, python-format
msgid "Import Template for Products"
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "In order to delete a purchase order, you must cancel it first."
msgstr ""
@ -1293,7 +1255,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Indicate the product quantity you want to order."
msgstr ""
@ -1329,14 +1290,6 @@ msgstr ""
msgid "Journal Item"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_bill_union____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_order____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_report____last_update
msgid "Last Modified on"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__write_uid
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__write_uid
@ -1352,7 +1305,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Late"
msgstr ""
@ -1370,14 +1322,12 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Lead Time to Purchase"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Let's create your first request for quotation."
msgstr ""
@ -1385,7 +1335,6 @@ msgstr ""
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid ""
"Let's try the Purchase app to manage the flow from purchase to reception and"
" invoice control."
@ -1415,15 +1364,9 @@ msgstr ""
#. odoo-python
#: code:addons/purchase/controllers/portal.py:0
#: model:ir.model.fields.selection,name:purchase.selection__purchase_order__state__done
#, python-format
msgid "Locked"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_main_attachment_id
msgid "Main Attachment"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid ""
@ -1513,14 +1456,12 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "My Draft RFQs"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "My Late RFQs"
msgstr ""
@ -1537,21 +1478,18 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "My RFQs"
msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "My Waiting RFQs"
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/controllers/portal.py:0
#, python-format
msgid "Name"
msgstr ""
@ -1563,15 +1501,9 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/controllers/portal.py:0
#, python-format
msgid "Newest"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__activity_calendar_event_id
msgid "Next Activity Calendar Event"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__activity_date_deadline
msgid "Next Activity Deadline"
@ -1650,6 +1582,7 @@ msgid "Number of Actions"
msgstr ""
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order__reminder_date_before_receipt
#: model:ir.model.fields,help:purchase.field_res_partner__reminder_date_before_receipt
#: model:ir.model.fields,help:purchase.field_res_users__reminder_date_before_receipt
msgid "Number of days to send reminder email before the promised receipt date"
@ -1691,7 +1624,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid ""
"Once you get the price from the vendor, you can complete the purchase order "
"with the right price."
@ -1759,12 +1691,6 @@ msgstr ""
msgid "Other Information"
msgstr ""
#. module: purchase
#: model:mail.template,report_name:purchase.email_template_edi_purchase_done
#: model:mail.template,report_name:purchase.email_template_edi_purchase_reminder
msgid "PO_{{ (object.name or '').replace('/','_') }}"
msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__product_packaging_id
msgid "Packaging"
@ -1936,7 +1862,6 @@ msgstr ""
#: code:addons/purchase/models/product.py:0
#: code:addons/purchase/models/product.py:0
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Purchase History for %s"
msgstr ""
@ -1971,7 +1896,6 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_tree
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_activity
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_tree
#, python-format
msgid "Purchase Order"
msgstr ""
@ -2046,7 +1970,6 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_bill_union_filter
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_order_filter
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_order_search
#, python-format
msgid "Purchase Orders"
msgstr ""
@ -2085,7 +2008,6 @@ msgstr ""
#. module: purchase
#: model:ir.actions.server,name:purchase.purchase_send_reminder_mail_ir_actions_server
#: model:ir.cron,cron_name:purchase.purchase_send_reminder_mail
msgid "Purchase reminder"
msgstr ""
@ -2118,7 +2040,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Purchased Last 7 Days"
msgstr ""
@ -2209,11 +2130,6 @@ msgstr ""
msgid "RFQ Sent"
msgstr ""
#. module: purchase
#: model:mail.template,report_name:purchase.email_template_edi_purchase
msgid "RFQ_{{ (object.name or '').replace('/','_') }}"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_order_filter
msgid "RFQs"
@ -2222,7 +2138,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "RFQs Sent Last 7 Days"
msgstr ""
@ -2344,7 +2259,6 @@ msgstr ""
#: model:ir.actions.report,name:purchase.report_purchase_quotation
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
#: model_terms:ir.ui.view,arch_db:purchase.report_purchasequotation_document
#, python-format
msgid "Request for Quotation"
msgstr ""
@ -2410,7 +2324,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Search a vendor name, or create one on the fly."
msgstr ""
@ -2437,7 +2350,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Select a product, or create a new one on the fly."
msgstr ""
@ -2457,6 +2369,11 @@ msgid ""
"block the flow. The Message has to be written in the next field."
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid "Sell and purchase products in different units of measure"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Send PO by Email"
@ -2481,7 +2398,6 @@ msgstr ""
#. odoo-javascript
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#: code:addons/purchase/static/src/js/tours/purchase.js:0
#, python-format
msgid "Send the request for quotation to your vendor."
msgstr ""
@ -2606,23 +2522,49 @@ msgstr ""
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__price_tax
msgid "Tax"
msgstr ""
msgstr "GST"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__tax_country_id
msgid "Tax Country"
msgstr ""
msgstr "GST Country"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__tax_totals
msgid "Tax Totals"
msgstr ""
msgstr "GST Totals"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__tax_calculation_rounding_method
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__tax_calculation_rounding_method
msgid "Tax calculation rounding method"
msgstr "GST calculation rounding method"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Tax excl."
msgstr "GST excl."
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Tax excl.:"
msgstr "GST excl.:"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Tax incl."
msgstr "GST incl."
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Tax incl.:"
msgstr "GST incl.:"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__amount_tax
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__taxes_id
msgid "Taxes"
msgstr ""
msgstr "GST"
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order_line__display_type
@ -2635,6 +2577,8 @@ msgid ""
"Technical field to filter the available taxes depending on the fiscal "
"country and fiscal position."
msgstr ""
"Technical field to filter the available GST depending on the fiscal "
"country and fiscal position."
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
@ -2656,7 +2600,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "The order receipt has been acknowledged by %s."
msgstr ""
@ -2681,7 +2624,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid ""
"There is no invoiceable line. If a product has a control policy based on "
"received quantity, please make sure that a quantity has been received."
@ -2718,7 +2660,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid ""
"This product is packaged by %(pack_size).2f %(pack_name)s. You should "
"purchase %(quantity).2f %(unit)s."
@ -2727,15 +2668,13 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/account_invoice.py:0
#, python-format
msgid "This vendor bill has been created from: %s"
msgid "This vendor bill has been created from: "
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/account_invoice.py:0
#, python-format
msgid "This vendor bill has been modified from: %s"
msgid "This vendor bill has been modified from: "
msgstr ""
#. module: purchase
@ -2770,7 +2709,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "To Send"
msgstr ""
@ -2788,7 +2726,6 @@ msgstr ""
#: model:ir.model.fields,field_description:purchase.field_purchase_report__price_total
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_purchase_orders
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_purchase_rfqs
#, python-format
msgid "Total"
msgstr ""
@ -2835,7 +2772,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid ""
"Unable to cancel this purchase order. You must first cancel the related "
"vendor bills."
@ -2857,6 +2793,11 @@ msgstr ""
msgid "Unit of Measure"
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid "Units Of Measure"
msgstr ""
#. module: purchase
#: model:ir.ui.menu,name:purchase.menu_purchase_uom_form_action
msgid "Units of Measure"
@ -2902,7 +2843,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Update Dates"
msgstr ""
@ -2916,15 +2856,6 @@ msgstr ""
msgid "User"
msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/report/purchase_report.py:0
#, python-format
msgid ""
"Value: 'avg_days_to_purchase' should only be used to show an average. If you"
" are seeing this message then it is being accessed incorrectly."
msgstr ""
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.res_config_settings_view_form_purchase
msgid "Variant Grid Entry"
@ -2987,7 +2918,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "View"
msgstr ""
@ -2999,7 +2929,6 @@ msgstr ""
#. module: purchase
#. odoo-javascript
#: code:addons/purchase/static/src/views/purchase_dashboard.xml:0
#, python-format
msgid "Waiting"
msgstr ""
@ -3019,7 +2948,6 @@ msgstr ""
#: code:addons/purchase/models/purchase.py:0
#: model:ir.model.fields.selection,name:purchase.selection__product_template__purchase_line_warn__warning
#: model:ir.model.fields.selection,name:purchase.selection__res_partner__purchase_warn__warning
#, python-format
msgid "Warning"
msgstr ""
@ -3027,7 +2955,6 @@ msgstr ""
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid "Warning for %s"
msgstr ""
@ -3087,7 +3014,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid ""
"You cannot change the type of a purchase order line. Instead you should "
"delete the current line and create a new line of the proper type."
@ -3110,7 +3036,6 @@ msgstr ""
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
msgid ""
"Your quotation contains products from company %(product_company)s whereas your quotation belongs to company %(quote_company)s. \n"
" Please change the company of your quotation or remove the products from other companies (%(bad_products)s)."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,9 +5,9 @@
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0\n"
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-23 08:22+0000\n"
"POT-Creation-Date: 2024-01-17 10:09+0000\n"
"PO-Revision-Date: 2016-07-22 02:00+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/"
@ -23,11 +23,6 @@ msgstr ""
msgid "# of Lines"
msgstr "# de líneas"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_needaction
msgid "Action Needed"
msgstr "Acción necesaria"
#. module: purchase
#: model:ir.model,name:purchase.model_account_analytic_account
msgid "Analytic Account"
@ -48,7 +43,6 @@ msgstr "Cancelar"
#: code:addons/purchase/controllers/portal.py:0
#: model:ir.model.fields.selection,name:purchase.selection__purchase_order__state__cancel
#: model:ir.model.fields.selection,name:purchase.selection__purchase_report__state__cancel
#, python-format
msgid "Cancelled"
msgstr "Cancelado"
@ -73,8 +67,7 @@ msgstr "Compañía"
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#, python-format
#: code:addons/purchase/models/purchase_order.py:0
msgid "Compose Email"
msgstr "Componer correo electrónico"
@ -121,27 +114,12 @@ msgstr "Nombre mostrado"
msgid "Done"
msgstr "Realizado"
#. module: purchase
#: model:ir.model,name:purchase.model_mail_compose_message
msgid "Email composition wizard"
msgstr "Asistente de redacción de correo electrónico."
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__fiscal_position_id
#: model:ir.model.fields,field_description:purchase.field_purchase_report__fiscal_position_id
msgid "Fiscal Position"
msgstr "Posición fiscal"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_follower_ids
msgid "Followers"
msgstr "Seguidores"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_partner_ids
msgid "Followers (Partners)"
msgstr "Seguidores (Empresas)"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_search
@ -158,29 +136,11 @@ msgstr "Agrupar por"
msgid "ID"
msgstr "ID (identificación)"
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Si está marcado, hay nuevos mensajes que requieren su atención."
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__incoterm_id
msgid "Incoterm"
msgstr "Incoterm"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_is_follower
msgid "Is Follower"
msgstr "Es un seguidor"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_bill_union____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_order____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line____last_update
#: model:ir.model.fields,field_description:purchase.field_purchase_report____last_update
msgid "Last Modified on"
msgstr "Última modificación en"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__write_uid
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__write_uid
@ -198,21 +158,11 @@ msgstr "Última actualización en"
msgid "Manual Invoices"
msgstr "Facturas manuales"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_ids
msgid "Messages"
msgstr "Mensajes"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_form
msgid "Notes"
msgstr "Notas"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__message_needaction_counter
msgid "Number of Actions"
msgstr "Número de acciones"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__date_order
#: model:ir.model.fields,field_description:purchase.field_purchase_report__date_order
@ -308,7 +258,7 @@ msgstr "Plazo de tiempo de compra"
#. module: purchase
#. odoo-python
#: code:addons/purchase/controllers/portal.py:0
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase_order.py:0
#: model:ir.actions.report,name:purchase.action_report_purchase_order
#: model:ir.model,name:purchase.model_purchase_order
#: model:ir.model.fields,field_description:purchase.field_account_bank_statement_line__purchase_id
@ -329,7 +279,6 @@ msgstr "Plazo de tiempo de compra"
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_tree
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_activity
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_tree
#, python-format
msgid "Purchase Order"
msgstr "Órden de Compra"
@ -355,12 +304,10 @@ msgstr "Líneas orden de compra"
#: model:ir.ui.menu,name:purchase.menu_purchase_form_action
#: model_terms:ir.ui.view,arch_db:purchase.account_analytic_account_view_form_purchase
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_home_menu_purchase
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_home_purchase
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_purchase_orders
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_bill_union_filter
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_order_filter
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_order_search
#, python-format
msgid "Purchase Orders"
msgstr "Pedidos de compra"
@ -392,11 +339,10 @@ msgstr "Referencia"
#. module: purchase
#. odoo-python
#: code:addons/purchase/models/purchase.py:0
#: code:addons/purchase/models/purchase_order.py:0
#: model:ir.actions.report,name:purchase.report_purchase_quotation
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_portal_content
#: model_terms:ir.ui.view,arch_db:purchase.report_purchasequotation_document
#, python-format
msgid "Request for Quotation"
msgstr "Solicitud de presupuesto"
@ -458,6 +404,7 @@ msgid "Supplier Pricelist"
msgstr "Tarifa de proveedor"
#. module: purchase
#: model:ir.model,name:purchase.model_account_tax
#: model:ir.model.fields,field_description:purchase.field_purchase_order_line__price_tax
msgid "Tax"
msgstr "Impuestos"
@ -476,7 +423,6 @@ msgstr "Impuestos"
#: model:ir.model.fields,field_description:purchase.field_purchase_report__price_total
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_purchase_orders
#: model_terms:ir.ui.view,arch_db:purchase.portal_my_purchase_rfqs
#, python-format
msgid "Total"
msgstr "Total"
@ -485,7 +431,7 @@ msgstr "Total"
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_tree
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_tree
msgid "Total Untaxed amount"
msgstr "Total neto"
msgstr "Monto neto"
#. module: purchase
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_kpis_tree
@ -515,7 +461,7 @@ msgstr "Base"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__amount_untaxed
msgid "Untaxed Amount"
msgstr "Total neto"
msgstr "Monto neto"
#. module: purchase
#: model:res.groups,name:purchase.group_purchase_user
@ -526,6 +472,8 @@ msgstr "Usuario"
#: model:ir.model.fields,field_description:purchase.field_purchase_bill_union__partner_id
#: model:ir.model.fields,field_description:purchase.field_purchase_order__partner_id
#: model:ir.model.fields,field_description:purchase.field_purchase_report__partner_id
#: model_terms:ir.ui.view,arch_db:purchase.product_template_search_view_purchase
#: model_terms:ir.ui.view,arch_db:purchase.product_view_search_catalog
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_line_search
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_line_tree
#: model_terms:ir.ui.view,arch_db:purchase.purchase_order_view_search
@ -541,13 +489,3 @@ msgstr "Vendedor"
#: model_terms:ir.ui.view,arch_db:purchase.view_purchase_bill_union_filter
msgid "Vendor Bills"
msgstr "Facturas del Proveedor"
#. module: purchase
#: model:ir.model.fields,field_description:purchase.field_purchase_order__website_message_ids
msgid "Website Messages"
msgstr "Mensajes del sitio web"
#. module: purchase
#: model:ir.model.fields,help:purchase.field_purchase_order__website_message_ids
msgid "Website communication history"
msgstr "Historial de comunicaciones del sitio web"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ def convert_field(cr, model, field, target_model):
FROM information_schema.columns
WHERE table_name = %s
AND column_name = %s
AND table_schema = current_schema
""", (table, field))
if not cr.fetchone():
return

View file

@ -2,11 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_invoice
from . import account_tax
from . import analytic_account
from . import analytic_applicability
from . import purchase
from . import ir_actions_report
from . import purchase_bill_line_match
from . import purchase_order
from . import purchase_order_line
from . import product
from . import res_company
from . import res_config_settings
from . import res_partner
from . import mail_compose_message

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import difflib
import logging
import time
from markupsafe import Markup
from odoo import api, fields, models, Command, _
from odoo.tools import OrderedSet
_logger = logging.getLogger(__name__)
@ -13,22 +16,19 @@ TOLERANCE = 0.02 # tolerance applied to the total when searching for a matching
class AccountMove(models.Model):
_inherit = 'account.move'
purchase_vendor_bill_id = fields.Many2one('purchase.bill.union', store=False, readonly=True,
states={'draft': [('readonly', False)]},
purchase_vendor_bill_id = fields.Many2one('purchase.bill.union', store=False, readonly=False,
string='Auto-complete',
help="Auto-complete from a past bill / purchase order.")
purchase_id = fields.Many2one('purchase.order', store=False, readonly=True,
states={'draft': [('readonly', False)]},
help="Auto-complete from a previous bill, refund, or purchase order.")
purchase_id = fields.Many2one('purchase.order', store=False, readonly=False,
string='Purchase Order',
help="Auto-complete from a past purchase order.")
purchase_order_count = fields.Integer(compute="_compute_origin_po_count", string='Purchase Order Count')
def _get_invoice_reference(self):
self.ensure_one()
vendor_refs = [ref for ref in set(self.invoice_line_ids.mapped('purchase_line_id.order_id.partner_ref')) if ref]
if self.ref:
return [ref for ref in self.ref.split(', ') if ref and ref not in vendor_refs] + vendor_refs
return vendor_refs
purchase_order_name = fields.Char(compute='_compute_purchase_order_name')
is_purchase_matched = fields.Boolean(compute='_compute_is_purchase_matched') # 0: PO not required or partially linked. 1: All lines linked
purchase_warning_text = fields.Text(
"Purchase Warning",
help="Internal warning for the partner or the products as set by the user.",
compute='_compute_purchase_warning_text')
@api.onchange('purchase_vendor_bill_id', 'purchase_id')
def _onchange_purchase_auto_complete(self):
@ -52,9 +52,8 @@ class AccountMove(models.Model):
# Copy data from PO
invoice_vals = self.purchase_id.with_company(self.purchase_id.company_id)._prepare_invoice()
has_invoice_lines = bool(self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_note', 'line_section')))
has_invoice_lines = bool(self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_section', 'line_subsection', 'line_note')))
new_currency_id = self.currency_id if has_invoice_lines else invoice_vals.get('currency_id')
del invoice_vals['ref'], invoice_vals['payment_reference']
del invoice_vals['company_id'] # avoid recomputing the currency
if self.move_type == invoice_vals['move_type']:
del invoice_vals['move_type'] # no need to be updated if it's same value, to avoid recomputes
@ -63,25 +62,15 @@ class AccountMove(models.Model):
# Copy purchase lines.
po_lines = self.purchase_id.order_line - self.invoice_line_ids.mapped('purchase_line_id')
for line in po_lines.filtered(lambda l: not l.display_type):
self.invoice_line_ids += self.env['account.move.line'].new(
line._prepare_account_move_line(self)
)
self._add_purchase_order_lines(po_lines)
# Compute invoice_origin.
origins = set(self.invoice_line_ids.mapped('purchase_line_id.order_id.name'))
self.invoice_origin = ','.join(list(origins))
# Compute ref.
refs = self._get_invoice_reference()
self.ref = ', '.join(refs)
# Compute payment_reference.
if not self.payment_reference:
if len(refs) == 1:
self.payment_reference = refs[0]
elif len(refs) > 1:
self.payment_reference = refs[-1]
# Copy company_id (only changes if the id is of a child company (branch))
if self.company_id != self.purchase_id.company_id:
self.company_id = self.purchase_id.company_id
self.purchase_id = False
@ -98,8 +87,8 @@ class AccountMove(models.Model):
if self.partner_id and self.move_type in ['in_invoice', 'in_refund'] and self.currency_id != currency_id:
if not self.env.context.get('default_journal_id'):
journal_domain = [
*self.env['account.journal']._check_company_domain(self.company_id),
('type', '=', 'purchase'),
('company_id', '=', self.company_id.id),
('currency_id', '=', currency_id.id),
]
default_journal_id = self.env['account.journal'].search(journal_domain, limit=1)
@ -110,11 +99,62 @@ class AccountMove(models.Model):
return res
@api.depends('line_ids.purchase_line_id')
def _compute_is_purchase_matched(self):
for move in self:
if any(il.display_type == 'product' and not bool(il.purchase_line_id) for il in move.invoice_line_ids):
move.is_purchase_matched = False
continue
move.is_purchase_matched = True
@api.depends('line_ids.purchase_line_id')
def _compute_origin_po_count(self):
for move in self:
move.purchase_order_count = len(move.line_ids.purchase_line_id.order_id)
@api.depends('purchase_order_count')
def _compute_purchase_order_name(self):
for move in self:
if move.purchase_order_count == 1:
move.purchase_order_name = move.invoice_line_ids.purchase_order_id.display_name
else:
move.purchase_order_name = False
@api.depends('partner_id.name', 'partner_id.purchase_warn_msg', 'invoice_line_ids.product_id.purchase_line_warn_msg', 'invoice_line_ids.product_id.display_name')
def _compute_purchase_warning_text(self):
if not self.env.user.has_group('purchase.group_warning_purchase'):
self.purchase_warning_text = ''
return
for move in self:
if move.move_type != 'in_invoice':
move.purchase_warning_text = ''
continue
warnings = OrderedSet()
if partner_msg := move.partner_id.purchase_warn_msg:
warnings.add((move.partner_id.name or move.partner_id.display_name) + ' - ' + partner_msg)
if partner_parent_msg := move.partner_id.parent_id.purchase_warn_msg:
parent = move.partner_id.parent_id
warnings.add((parent.name or parent.display_name) + ' - ' + partner_parent_msg)
for product in move.invoice_line_ids.product_id:
if product_msg := product.purchase_line_warn_msg:
warnings.add(product.display_name + ' - ' + product_msg)
move.purchase_warning_text = '\n'.join(warnings)
def action_purchase_matching(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _("Purchase Matching"),
'res_model': 'purchase.bill.line.match',
'domain': [
('partner_id', 'in', (self.partner_id | self.partner_id.commercial_partner_id).ids),
('company_id', 'in', self.env.companies.ids),
('company_id', 'child_of', self.company_id.ids),
('account_move_id', 'in', [self.id, False]),
],
'views': [(self.env.ref('purchase.purchase_bill_line_match_tree').id, 'list')],
}
def action_view_source_purchase_orders(self):
self.ensure_one()
source_orders = self.line_ids.purchase_line_id.order_id
@ -139,7 +179,7 @@ class AccountMove(models.Model):
if not purchases:
continue
refs = [purchase._get_html_link() for purchase in purchases]
message = _("This vendor bill has been created from: %s") % ','.join(refs)
message = _("This vendor bill has been created from: ") + Markup(',').join(refs)
move.message_post(body=message)
return moves
@ -154,133 +194,365 @@ class AccountMove(models.Model):
diff_purchases = new_purchases - old_purchases[i]
if diff_purchases:
refs = [purchase._get_html_link() for purchase in diff_purchases]
message = _("This vendor bill has been modified from: %s") % ','.join(refs)
message = _("This vendor bill has been modified from: ") + Markup(',').join(refs)
move.message_post(body=message)
return res
def find_matching_subset_invoice_lines(self, invoice_lines, goal_total, timeout):
""" The problem of finding the subset of `invoice_lines` which sums up to `goal_total` reduces to the 0-1 Knapsack problem.
The dynamic programming approach to solve this problem is most of the time slower than this because identical sub-problems don't arise often enough.
It returns the list of invoice lines which sums up to `goal_total` or an empty list if multiple or no solutions were found."""
def _find_matching_subset_invoice_lines(lines, goal):
def _add_purchase_order_lines(self, purchase_order_lines):
""" Creates new invoice lines from purchase order lines """
self.ensure_one()
new_line_ids = self.env['account.move.line']
for po_line in purchase_order_lines:
new_line_values = po_line._prepare_account_move_line(self)
new_line_ids += self.env['account.move.line'].new(new_line_values)
self.invoice_line_ids += new_line_ids
def _find_matching_subset_po_lines(self, po_lines_with_amount, goal_total, timeout):
"""Finds the purchase order lines adding up to the goal amount.
The problem of finding the subset of `po_lines_with_amount` which sums up to `goal_total` reduces to
the 0-1 Knapsack problem. The dynamic programming approach to solve this problem is most of the time slower
than this because identical sub-problems don't arise often enough. It returns the list of purchase order lines
which sum up to `goal_total` or an empty list if multiple or no solutions were found.
:param po_lines_with_amount: a dict (str: float|recordset) containing:
* line: an `purchase.order.line`
* amount_to_invoice: the remaining amount to be invoiced of the line
:param goal_total: the total amount to match with a subset of purchase order lines
:param timeout: the max time the line matching algorithm can take before timing out
:return: list of `purchase.order.line` whose remaining sum matches `goal_total`
"""
def find_matching_subset_po_lines(lines, goal):
if time.time() - start_time > timeout:
raise TimeoutError
solutions = []
for i, line in enumerate(lines):
if line['amount_to_invoice'] < goal - TOLERANCE:
sub_solutions = _find_matching_subset_invoice_lines(lines[i + 1:], goal - line['amount_to_invoice'])
solutions.extend((line, *solution) for solution in sub_solutions)
# The amount to invoice of the current purchase order line is less than the amount we still need on
# the vendor bill.
# We try finding purchase order lines that match the remaining vendor bill amount minus the amount
# to invoice of the current purchase order line. We only look in the purchase order lines that we
# haven't passed yet.
sub_solutions = find_matching_subset_po_lines(lines[i + 1:], goal - line['amount_to_invoice'])
# We add all possible sub-solutions' purchase order lines in a tuple together with our current
# purchase order line.
solutions.extend((line['line'], *solution) for solution in sub_solutions)
elif goal - TOLERANCE <= line['amount_to_invoice'] <= goal + TOLERANCE:
solutions.append([line])
# The amount to invoice of the current purchase order line matches the remaining vendor bill amount.
# We add this purchase order line to our list of solutions.
solutions.append([line['line']])
if len(solutions) > 1:
# More than 1 solution found, we can't know for sure which is the correct one, so we don't return any solution
# More than one solution was found. We can't know for sure which is the correct one, so we don't
# return any solution.
return []
return solutions
start_time = time.time()
try:
subsets = _find_matching_subset_invoice_lines(sorted(invoice_lines, key=lambda line: line['amount_to_invoice'], reverse=True), goal_total)
subsets = find_matching_subset_po_lines(
sorted(po_lines_with_amount, key=lambda line: line['amount_to_invoice'], reverse=True),
goal_total
)
return subsets[0] if subsets else []
except TimeoutError:
_logger.warning("Timed out during search of a matching subset of invoice lines")
_logger.warning("Timed out during search of a matching subset of purchase order lines")
return []
def _set_purchase_orders(self, purchase_orders, force_write=True):
with self.env.cr.savepoint():
with self._get_edi_creation() as move_form:
if force_write and move_form.line_ids:
move_form.invoice_line_ids = [Command.clear()]
for purchase_order in purchase_orders:
move_form.invoice_line_ids = [Command.create({
'display_type': 'line_section',
'name': _('From %s document', purchase_order.name)
})]
move_form.purchase_id = purchase_order
move_form._onchange_purchase_auto_complete()
def _find_matching_po_and_inv_lines(self, po_lines, inv_lines, timeout):
"""Finds purchase order lines that match some of the invoice lines.
def _match_purchase_orders(self, po_references, partner_id, amount_total, timeout):
""" Tries to match a purchase order given some bill arguments/hints.
We try to find a purchase order line for every invoice line matching on the unit price and having at least
the same quantity to invoice.
:param po_references: A list of potencial purchase order references/name.
:param partner_id: The vendor id.
:param amount_total: The vendor bill total.
:param timeout: The timeout for subline search
:return: A tuple containing:
* a str which is the match method:
'total_match': the invoice amount AND the partner or bill' reference match
'subset_total_match': the reference AND a subset of line that match the bill amount total
'po_match': only the reference match
'no_match': no result found
* recordset of matched 'purchase.order.line' (could come from more than one purchase.order)
:param po_lines: list of purchase order lines that can be matched
:param inv_lines: list of invoice lines to be matched
:param timeout: how long this function can run before we consider it too long
:return: a tuple (list, list) containing:
* matched 'purchase.order.line'
* tuple of purchase order line ids and their matched 'account.move.line'
"""
common_domain = [('company_id', '=', self.company_id.id), ('state', 'in', ('purchase', 'done')), ('invoice_status', 'in', ('to invoice', 'no'))]
# Sort the invoice lines by unit price and quantity to speed up matching
invoice_lines = sorted(inv_lines, key=lambda line: (line.price_unit, line.quantity), reverse=True)
# Sort the purchase order lines by unit price and remaining quantity to speed up matching
purchase_lines = sorted(
po_lines,
key=lambda line: (line.price_unit, line.product_qty - line.qty_invoiced),
reverse=True
)
matched_po_lines = []
matched_inv_lines = []
try:
start_time = time.time()
for invoice_line in invoice_lines:
# There are no purchase order lines left. We are done matching.
if not purchase_lines:
break
# A dict of purchase lines mapping to a diff score for the name
purchase_line_candidates = {}
for purchase_line in purchase_lines:
if time.time() - start_time > timeout:
raise TimeoutError
matching_pos = self.env['purchase.order']
# The lists are sorted by unit price descendingly.
# When the unit price of the purchase line is lower than the unit price of the invoice line,
# we cannot get a match anymore.
if purchase_line.price_unit < invoice_line.price_unit:
break
if (invoice_line.price_unit == purchase_line.price_unit
and invoice_line.quantity <= purchase_line.product_qty - purchase_line.qty_invoiced):
# The current purchase line is a possible match for the current invoice line.
# We calculate the name match ratio and continue with other possible matches.
#
# We could match on more fields coming from an EDI invoice, but that requires extending the
# account.move.line model with the extra matching fields and extending the EDI extraction
# logic to fill these new fields.
purchase_line_candidates[purchase_line] = difflib.SequenceMatcher(
None, invoice_line.name, purchase_line.name).ratio()
if len(purchase_line_candidates) > 0:
# We take the best match based on the name.
purchase_line_match = max(purchase_line_candidates, key=purchase_line_candidates.get)
if purchase_line_match:
# We found a match. We remove the purchase order line so it does not get matched twice.
purchase_lines.remove(purchase_line_match)
matched_po_lines.append(purchase_line_match)
matched_inv_lines.append((purchase_line_match.id, invoice_line))
return (matched_po_lines, matched_inv_lines)
except TimeoutError:
_logger.warning('Timed out during search of matching purchase order lines')
return ([], [])
def _set_purchase_orders(self, purchase_orders, force_write=True):
"""Link the given purchase orders to this vendor bill and add their lines as invoice lines.
:param purchase_orders: a list of purchase orders to be linked to this vendor bill
:param force_write: whether to delete all existing invoice lines before adding the vendor bill lines
"""
with self.env.cr.savepoint():
with self._get_edi_creation() as invoice:
if force_write and invoice.line_ids:
invoice.invoice_line_ids = [Command.clear()]
for purchase_order in purchase_orders:
invoice.invoice_line_ids = [Command.create({
'display_type': 'line_section',
'name': _('From %s', purchase_order.name)
})]
invoice.purchase_id = purchase_order
invoice._onchange_purchase_auto_complete()
def _match_purchase_orders(self, po_references, partner_id, amount_total, from_ocr, timeout):
"""Tries to match open purchase order lines with this invoice given the information we have.
:param po_references: a list of potential purchase order references/names
:param partner_id: the vendor id inferred from the vendor bill
:param amount_total: the total amount of the vendor bill
:param from_ocr: indicates whether this vendor bill was created from an OCR scan (less reliable)
:param timeout: the max time the line matching algorithm can take before timing out
:return: tuple (str, recordset, dict) containing:
* the match method:
* `total_match`: purchase order reference(s) and total amounts match perfectly
* `subset_total_match`: a subset of the referenced purchase orders' lines matches the total amount of
this invoice (OCR only)
* `po_match`: only the purchase order reference matches (OCR only)
* `subset_match`: a subset of the referenced purchase orders' lines matches a subset of the invoice
lines based on unit prices (EDI only)
* `no_match`: no result found
* recordset of `purchase.order.line` containing purchase order lines matched with an invoice line
* list of tuple containing every `purchase.order.line` id and its related `account.move.line`
"""
common_domain = [
('company_id', '=', self.company_id.id),
('state', '=', 'purchase'),
('invoice_status', 'in', ('to invoice', 'no'))
]
matching_purchase_orders = self.env['purchase.order']
# We have purchase order references in our vendor bill and a total amount.
if po_references and amount_total:
matching_pos |= self.env['purchase.order'].search(common_domain + [('name', 'in', po_references)])
# We first try looking for purchase orders whose names match one of the purchase order references in the
# vendor bill.
matching_purchase_orders |= self.env['purchase.order'].search(
common_domain + [('name', 'in', po_references)])
if not matching_pos:
matching_pos |= self.env['purchase.order'].search(common_domain + [('partner_ref', 'in', po_references)])
if not matching_purchase_orders:
# If not found, we try looking for purchase orders whose `partner_ref` field matches one of the
# purchase order references in the vendor bill.
matching_purchase_orders |= self.env['purchase.order'].search(
common_domain + [('partner_ref', 'in', po_references)])
if matching_pos:
matching_pos_invoice_lines = [{
if matching_purchase_orders:
# We found matching purchase orders and are extracting all purchase order lines together with their
# amounts still to be invoiced.
po_lines = [line for line in matching_purchase_orders.order_line if line.product_qty]
po_lines_with_amount = [{
'line': line,
'amount_to_invoice': (1 - line.qty_invoiced / line.product_qty) * line.price_total,
} for line in matching_pos.order_line if line.product_qty]
} for line in po_lines]
if amount_total - TOLERANCE < sum(line['amount_to_invoice'] for line in matching_pos_invoice_lines) < amount_total + TOLERANCE:
return 'total_match', matching_pos.order_line
# If the sum of all remaining amounts to be invoiced for these purchase orders' lines is within a
# tolerance from the vendor bill total, we have a total match. We return all purchase order lines
# summing up to this vendor bill's total (could be from multiple purchase orders).
if (amount_total - TOLERANCE
< sum(line['amount_to_invoice'] for line in po_lines_with_amount)
< amount_total + TOLERANCE):
return 'total_match', matching_purchase_orders.order_line, None
elif from_ocr:
# The invoice comes from an OCR scan.
# We try to match the invoice total with purchase order lines.
matching_po_lines = self._find_matching_subset_po_lines(
po_lines_with_amount, amount_total, timeout)
if matching_po_lines:
return 'subset_total_match', self.env['purchase.order.line'].union(*matching_po_lines), None
else:
# We did not find a match for the invoice total.
# We return all purchase order lines based only on the purchase order reference(s) in the
# vendor bill.
return 'po_match', matching_purchase_orders.order_line, None
else:
il_subset = self.find_matching_subset_invoice_lines(matching_pos_invoice_lines, amount_total, timeout)
if il_subset:
return 'subset_total_match', self.env['purchase.order.line'].union(*[line['line'] for line in il_subset])
else:
return 'po_match', matching_pos.order_line
# We have an invoice from an EDI document, so we try to match individual invoice lines with
# individual purchase order lines from referenced purchase orders.
matching_po_lines, matching_inv_lines = self._find_matching_po_and_inv_lines(
po_lines, self.invoice_line_ids, timeout)
if matching_po_lines:
# We found a subset of purchase order lines that match a subset of the vendor bill lines.
# We return the matching purchase order lines and vendor bill lines.
return ('subset_match',
self.env['purchase.order.line'].union(*matching_po_lines),
matching_inv_lines)
# As a last resort we try matching a purchase order by vendor and total amount.
if partner_id and amount_total:
purchase_id_domain = common_domain + [('partner_id', 'child_of', [partner_id]), ('amount_total', '>=', amount_total - TOLERANCE), ('amount_total', '<=', amount_total + TOLERANCE)]
matching_pos |= self.env['purchase.order'].search(purchase_id_domain)
if len(matching_pos) == 1:
return 'total_match', matching_pos.order_line
purchase_id_domain = common_domain + [
('partner_id', 'child_of', [partner_id]),
('amount_total', '>=', amount_total - TOLERANCE),
('amount_total', '<=', amount_total + TOLERANCE)
]
matching_purchase_orders = self.env['purchase.order'].search(purchase_id_domain)
if len(matching_purchase_orders) == 1:
# We found exactly one match on vendor and total amount (within tolerance).
# We return all purchase order lines of the purchase order whose total amount matched our vendor bill.
return 'total_match', matching_purchase_orders.order_line, None
return 'no_match', matching_pos.order_line
# We couldn't find anything, so we return no lines.
return ('no_match', matching_purchase_orders.order_line, None)
def _find_and_set_purchase_orders(self, po_references, partner_id, amount_total, prefer_purchase_line=False, timeout=10):
def _find_and_set_purchase_orders(self, po_references, partner_id, amount_total, from_ocr=False, timeout=10):
"""Finds related purchase orders that (partially) match the vendor bill and links the matching lines on this
vendor bill.
:param po_references: a list of potential purchase order references/names
:param partner_id: the vendor id matched on the vendor bill
:param amount_total: the total amount of the vendor bill
:param from_ocr: indicates whether this vendor bill was created from an OCR scan (less reliable)
:param timeout: the max time the line matching algorithm can take before timing out
"""
self.ensure_one()
method, matched_po_lines = self._match_purchase_orders(po_references, partner_id, amount_total, timeout)
method, matched_po_lines, matched_inv_lines = self._match_purchase_orders(
po_references, partner_id, amount_total, from_ocr, timeout
)
if method == 'total_match': # erase all lines and autocomplete
if method in ('total_match', 'po_match'):
# The purchase order reference(s) and total amounts match perfectly or there is only one purchase order
# reference that matches with an OCR invoice. We replace the invoice lines with the purchase order lines.
self._set_purchase_orders(matched_po_lines.order_id, force_write=True)
elif method == 'subset_total_match': # don't erase and add autocomplete
elif method == 'subset_total_match':
# A subset of the referenced purchase order lines matches the total amount of this invoice.
# We keep the invoice lines, but add all the lines from the partially matched purchase orders:
# * "naively" matched purchase order lines keep their quantity
# * unmatched purchase order lines are added with their quantity set to 0
self._set_purchase_orders(matched_po_lines.order_id, force_write=False)
with self._get_edi_creation() as move_form: # logic for unmatched lines
unmatched_lines = move_form.invoice_line_ids.filtered(
with self._get_edi_creation() as invoice:
unmatched_lines = invoice.invoice_line_ids.filtered(
lambda l: l.purchase_line_id and l.purchase_line_id not in matched_po_lines)
for line in unmatched_lines:
if prefer_purchase_line:
line.quantity = 0
else:
line.unlink()
invoice.invoice_line_ids = [Command.update(line.id, {'quantity': 0}) for line in unmatched_lines]
if not prefer_purchase_line:
move_form.invoice_line_ids.filtered('purchase_line_id').quantity = 0
elif method == 'subset_match':
# A subset of the referenced purchase order lines matches a subset of the invoice lines.
# We add the purchase order lines, but adjust the quantity to the quantities in the invoice.
# The original invoice lines that correspond with a purchase order line are removed.
self._set_purchase_orders(matched_po_lines.order_id, force_write=False)
elif method == 'po_match': # erase all lines and autocomplete
if prefer_purchase_line:
self._set_purchase_orders(matched_po_lines.order_id, force_write=True)
with self._get_edi_creation() as invoice:
unmatched_lines = invoice.invoice_line_ids.filtered(
lambda l: l.purchase_line_id and l.purchase_line_id not in matched_po_lines)
invoice.invoice_line_ids = [Command.delete(line.id) for line in unmatched_lines]
# We remove the original matched invoice lines and apply their quantities and taxes to the matched
# purchase order lines.
inv_and_po_lines = list(map(lambda line: (
invoice.invoice_line_ids.filtered(
lambda l: l.purchase_line_id and l.purchase_line_id.id == line[0]),
invoice.invoice_line_ids.filtered(
lambda l: l in line[1])
),
matched_inv_lines
))
invoice.invoice_line_ids = [
Command.update(po_line.id, {'quantity': inv_line.quantity, 'tax_ids': inv_line.tax_ids})
for po_line, inv_line in inv_and_po_lines
]
invoice.invoice_line_ids = [Command.delete(inv_line.id) for dummy, inv_line in inv_and_po_lines]
# If there are lines left not linked to a purchase order, we add a header
unmatched_lines = invoice.invoice_line_ids.filtered(lambda l: not l.purchase_line_id)
if len(unmatched_lines) > 0:
invoice.invoice_line_ids = [Command.create({
'display_type': 'line_section',
'name': _('From Electronic Document'),
'sequence': -1,
})]
if not any(line.purchase_order_id for line in self.line_ids):
self.invoice_origin = False
class AccountMoveLine(models.Model):
""" Override AccountInvoice_line to add the link to the purchase order line it is related to"""
_inherit = 'account.move.line'
purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index='btree_not_null')
is_downpayment = fields.Boolean()
purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index='btree_not_null', copy=False)
purchase_order_id = fields.Many2one('purchase.order', 'Purchase Order', related='purchase_line_id.order_id', readonly=True)
purchase_line_warn_msg = fields.Text(compute='_compute_purchase_line_warn_msg')
def _copy_data_extend_business_fields(self, values):
# OVERRIDE to copy the 'purchase_line_id' field as well.
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
values['purchase_line_id'] = self.purchase_line_id.id
def _prepare_line_values_for_purchase(self):
return [
{
'product_id': line.product_id.id,
'product_qty': line.quantity,
'product_uom_id': line.product_uom_id.id,
'price_unit': line.price_unit,
'discount': line.discount,
}
for line in self
]
def _related_analytic_distribution(self):
# EXTENDS 'account'
vals = super()._related_analytic_distribution()
if self.purchase_line_id and not self.analytic_distribution:
vals |= self.purchase_line_id.analytic_distribution or {}
return vals
@api.depends('product_id.purchase_line_warn_msg')
def _compute_purchase_line_warn_msg(self):
has_group = self.env.user.has_group('purchase.group_warning_purchase')
for line in self:
line.purchase_line_warn_msg = line.product_id.purchase_line_warn_msg if has_group else ""

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from odoo import models
class AccountTax(models.Model):
_inherit = "account.tax"
def _hook_compute_is_used(self, taxes_to_compute):
# OVERRIDE in order to fetch taxes used in purchase
used_taxes = super()._hook_compute_is_used(taxes_to_compute)
taxes_to_compute -= used_taxes
if taxes_to_compute:
self.env['purchase.order.line'].flush_model(['tax_ids'])
self.env.cr.execute("""
SELECT id
FROM account_tax
WHERE EXISTS(
SELECT 1
FROM account_tax_purchase_order_line_rel AS pur
WHERE account_tax_id IN %s
AND account_tax.id = pur.account_tax_id
)
""", [tuple(taxes_to_compute)])
used_taxes.update([tax[0] for tax in self.env.cr.fetchall()])
return used_taxes

View file

@ -13,7 +13,7 @@ class AccountAnalyticAccount(models.Model):
def _compute_purchase_order_count(self):
for account in self:
account.purchase_order_count = self.env['purchase.order'].search_count([
('order_line.invoice_lines.analytic_line_ids.account_id', '=', account.id)
('order_line.invoice_lines.analytic_line_ids.account_id', 'in', account.ids)
])
def action_view_purchase_orders(self):
@ -26,7 +26,7 @@ class AccountAnalyticAccount(models.Model):
"res_model": "purchase.order",
"domain": [['id', 'in', purchase_orders.ids]],
"name": _("Purchase Orders"),
'view_mode': 'tree,form',
'view_mode': 'list,form',
}
if len(purchase_orders) == 1:
result['view_mode'] = 'form'

View file

@ -0,0 +1,56 @@
import io
from odoo import models
from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
# EXTENDS base
collected_streams = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids)
if (
collected_streams
and res_ids
and len(res_ids) == 1
and self._is_purchase_order_report(report_ref)
):
purchase_order = self.env['purchase.order'].browse(res_ids)
builders = purchase_order._get_edi_builders()
if len(builders) == 0:
return collected_streams
# Read pdf content.
pdf_stream = collected_streams[purchase_order.id]['stream']
pdf_content = pdf_stream.getvalue()
reader_buffer = io.BytesIO(pdf_content)
reader = OdooPdfFileReader(reader_buffer, strict=False)
writer = OdooPdfFileWriter()
writer.cloneReaderDocumentRoot(reader)
# Generate and attach EDI documents from each builder
for builder in builders:
xml_content = builder._export_order(purchase_order)
writer.addAttachment(
builder._export_invoice_filename(purchase_order), # works even if it's a SO or PO
xml_content,
subtype='text/xml'
)
# Replace the current content.
pdf_stream.close()
new_pdf_stream = io.BytesIO()
writer.write(new_pdf_stream)
collected_streams[purchase_order.id]['stream'] = new_pdf_stream
return collected_streams
def _is_purchase_order_report(self, report_ref):
return self._get_report(report_ref).report_name in (
'purchase.report_purchasequotation',
'purchase.report_purchaseorder'
)

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# purches Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
def _action_send_mail(self, auto_commit=False):
if self.model == 'purchase.order':
self = self.with_context(mailing_document_based=True)
if self.env.context.get('mark_rfq_as_sent'):
self = self.with_context(mail_notify_author=self.env.user.partner_id in self.partner_ids)
return super(MailComposeMessage, self)._action_send_mail(auto_commit=auto_commit)

View file

@ -2,39 +2,38 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.tools.float_utils import float_round
from odoo.exceptions import UserError
from dateutil.relativedelta import relativedelta
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = 'product.template'
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased', digits='Product Unit of Measure')
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased', digits='Product Unit')
purchase_method = fields.Selection([
('purchase', 'On ordered quantities'),
('receive', 'On received quantities'),
], string="Control Policy", compute='_compute_purchase_method', default='receive', store=True, readonly=False,
], string="Control Policy", compute='_compute_purchase_method', precompute=True, store=True, readonly=False,
help="On ordered quantities: Control bills based on ordered quantities.\n"
"On received quantities: Control bills based on received quantities.")
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line Warning', help=WARNING_HELP, required=True, default="no-message")
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')
@api.depends('detailed_type')
@api.depends('type')
def _compute_purchase_method(self):
default_purchase_method = self.env['product.template'].default_get(['purchase_method']).get('purchase_method')
default_purchase_method = self.env['product.template'].default_get(['purchase_method']).get('purchase_method', 'receive')
for product in self:
if product.detailed_type == 'service':
if product.type == 'service':
product.purchase_method = 'purchase'
else:
product.purchase_method = default_purchase_method
def _compute_purchased_product_qty(self):
for template in self.with_context(active_test=False):
template.purchased_product_qty = float_round(sum(p.purchased_product_qty for
p in template.product_variant_ids), precision_rounding=template.uom_id.rounding
)
template.purchased_product_qty = template.uom_id.round(sum(p.purchased_product_qty for p in template.product_variant_ids))
def _get_backend_root_menu_ids(self):
return super()._get_backend_root_menu_ids() + [self.env.ref('purchase.menu_purchase_root').id]
@api.model
def get_import_templates(self):
@ -48,42 +47,99 @@ class ProductTemplate(models.Model):
def action_view_po(self):
action = self.env["ir.actions.actions"]._for_xml_id("purchase.action_purchase_history")
action['domain'] = [
('state', 'in', ['purchase', 'done']),
('product_id', 'in', self.with_context(active_test=False).product_variant_ids.ids),
action['domain'] = ['&',
('state', '=', 'purchase'),
('product_id', 'in', self.with_context(active_test=False).product_variant_ids.ids)
]
action['display_name'] = _("Purchase History for %s", self.display_name)
return action
class ProductProduct(models.Model):
_name = 'product.product'
_inherit = 'product.product'
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased',
digits='Product Unit of Measure')
digits='Product Unit')
is_in_purchase_order = fields.Boolean(
compute='_compute_is_in_purchase_order',
search='_search_is_in_purchase_order',
)
def _compute_purchased_product_qty(self):
date_from = fields.Datetime.to_string(fields.Date.context_today(self) - relativedelta(years=1))
domain = [
('order_id.state', 'in', ['purchase', 'done']),
('order_id.state', '=', 'purchase'),
('product_id', 'in', self.ids),
('order_id.date_approve', '>=', date_from)
]
order_lines = self.env['purchase.order.line']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id'])
purchased_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in order_lines])
order_lines = self.env['purchase.order.line']._read_group(domain, ['product_id'], ['product_uom_qty:sum'])
purchased_data = {product.id: qty for product, qty in order_lines}
for product in self:
if not product.id:
product.purchased_product_qty = 0.0
continue
product.purchased_product_qty = float_round(purchased_data.get(product.id, 0), precision_rounding=product.uom_id.rounding)
product.purchased_product_qty = product.uom_id.round(purchased_data.get(product.id, 0))
@api.depends_context('order_id')
def _compute_is_in_purchase_order(self):
order_id = self.env.context.get('order_id')
if not order_id:
self.is_in_purchase_order = False
return
read_group_data = self.env['purchase.order.line']._read_group(
domain=[('order_id', '=', order_id)],
groupby=['product_id'],
aggregates=['__count'],
)
data = {product.id: count for product, count in read_group_data}
for product in self:
product.is_in_purchase_order = bool(data.get(product.id, 0))
def _search_is_in_purchase_order(self, operator, value):
if operator != 'in':
return NotImplemented
product_ids = self.env['purchase.order.line'].search([
('order_id', 'in', [self.env.context.get('order_id', '')]),
]).product_id.ids
return [('id', 'in', product_ids)]
def action_view_po(self):
action = self.env["ir.actions.actions"]._for_xml_id("purchase.action_purchase_history")
action['domain'] = ['&', ('state', 'in', ['purchase', 'done']), ('product_id', 'in', self.ids)]
action['domain'] = ['&', ('state', '=', 'purchase'), ('product_id', 'in', self.ids)]
action['display_name'] = _("Purchase History for %s", self.display_name)
return action
def _get_backend_root_menu_ids(self):
return super()._get_backend_root_menu_ids() + [self.env.ref('purchase.menu_purchase_root').id]
def _update_uom(self, to_uom_id):
for uom, product, po_lines in self.env['purchase.order.line']._read_group(
[('product_id', 'in', self.ids)],
['product_uom_id', 'product_id'],
['id:recordset'],
):
if uom != product.product_tmpl_id.uom_id:
raise UserError(_(
'As other units of measure (ex : %(problem_uom)s) '
'than %(uom)s have already been used for this product, the change of unit of measure can not be done.'
'If you want to change it, please archive the product and create a new one.',
problem_uom=uom.display_name, uom=product.product_tmpl_id.uom_id.display_name))
po_lines.product_uom_id = to_uom_id
po_lines.flush_recordset()
return super()._update_uom(to_uom_id)
def _trigger_uom_warning(self):
res = super()._trigger_uom_warning()
if res:
return res
po_lines = self.env['purchase.order.line'].sudo().search_count(
[('product_id', 'in', self.ids)], limit=1
)
return bool(po_lines)
class ProductSupplierinfo(models.Model):
_inherit = "product.supplierinfo"
@ -92,8 +148,7 @@ class ProductSupplierinfo(models.Model):
def _onchange_partner_id(self):
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.company.currency_id.id
class ProductPackaging(models.Model):
_inherit = 'product.packaging'
purchase = fields.Boolean("Purchase", default=True, help="If true, the packaging can be used for purchase orders")
def _get_filtered_supplier(self, company_id, product_id, params=False):
if params and 'order_id' in params and params['order_id'].company_id:
company_id = params['order_id'].company_id
return super()._get_filtered_supplier(company_id, product_id, params)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,203 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import SQL
from odoo.exceptions import UserError
class PurchaseBillLineMatch(models.Model):
_name = 'purchase.bill.line.match'
_description = "Purchase Line and Vendor Bill line matching view"
_auto = False
_order = 'product_id, aml_id, pol_id'
pol_id = fields.Many2one(comodel_name='purchase.order.line', readonly=True)
aml_id = fields.Many2one(comodel_name='account.move.line', readonly=True)
company_id = fields.Many2one(comodel_name='res.company', readonly=True)
partner_id = fields.Many2one(comodel_name='res.partner', readonly=True)
product_id = fields.Many2one(comodel_name='product.product', readonly=True)
line_qty = fields.Float(readonly=True)
line_uom_id = fields.Many2one(comodel_name='uom.uom', readonly=True)
qty_invoiced = fields.Float(readonly=True)
qty_to_invoice = fields.Float('Qty to invoice', readonly=True)
purchase_order_id = fields.Many2one(comodel_name='purchase.order', readonly=True)
account_move_id = fields.Many2one(comodel_name='account.move', readonly=True)
line_amount_untaxed = fields.Monetary(readonly=True)
currency_id = fields.Many2one(comodel_name='res.currency', readonly=True)
state = fields.Char(readonly=True)
product_uom_id = fields.Many2one(comodel_name='uom.uom', related='product_id.uom_id')
product_uom_qty = fields.Float(compute='_compute_product_uom_qty', inverse='_inverse_product_uom_qty', readonly=False)
product_uom_price = fields.Float(compute='_compute_product_uom_price', inverse='_inverse_product_uom_price', readonly=False)
billed_amount_untaxed = fields.Monetary(compute='_compute_amount_untaxed_fields', currency_field='currency_id')
purchase_amount_untaxed = fields.Monetary(compute='_compute_amount_untaxed_fields', currency_field='currency_id')
reference = fields.Char(compute='_compute_reference')
@api.onchange('product_uom_price')
def _inverse_product_uom_price(self):
for line in self:
if line.aml_id:
line.aml_id.price_unit = line.product_uom_price
else:
line.pol_id.price_unit = line.product_uom_price
@api.onchange('product_uom_qty')
def _inverse_product_uom_qty(self):
for line in self:
if line.aml_id:
line.aml_id.quantity = line.product_uom_qty
else:
# on POL, setting product_qty will recompute price_unit to have the old value
# this prevents the price to revert by saving the previous price and re-setting them again
previous_price_unit = line.pol_id.price_unit
line.pol_id.product_qty = line.product_uom_qty
line.pol_id.price_unit = previous_price_unit
def _compute_amount_untaxed_fields(self):
for line in self:
line.billed_amount_untaxed = line.line_amount_untaxed if line.account_move_id else False
line.purchase_amount_untaxed = line.line_amount_untaxed if line.purchase_order_id else False
def _compute_reference(self):
for line in self:
line.reference = line.purchase_order_id.display_name or line.account_move_id.display_name
def _compute_display_name(self):
for line in self:
line.display_name = line.product_id.display_name or line.aml_id.name or line.pol_id.name
def _compute_product_uom_qty(self):
for line in self:
line.product_uom_qty = line.line_uom_id._compute_quantity(line.line_qty, line.product_uom_id)
@api.depends('aml_id.price_unit', 'pol_id.price_unit')
def _compute_product_uom_price(self):
for line in self:
line.product_uom_price = line.aml_id.price_unit if line.aml_id else line.pol_id.price_unit
@api.model
def _select_po_line(self):
return SQL("""
SELECT pol.id,
pol.id as pol_id,
NULL as aml_id,
pol.company_id as company_id,
pol.partner_id as partner_id,
pol.product_id as product_id,
pol.product_qty as line_qty,
pol.product_uom_id as line_uom_id,
pol.qty_invoiced as qty_invoiced,
pol.qty_to_invoice as qty_to_invoice,
po.id as purchase_order_id,
NULL as account_move_id,
pol.price_subtotal as line_amount_untaxed,
po.currency_id as currency_id,
po.state as state
FROM purchase_order_line pol
LEFT JOIN purchase_order po ON pol.order_id = po.id
WHERE po.state = 'purchase'
AND (pol.product_qty > pol.qty_invoiced OR pol.qty_to_invoice != 0)
OR ((pol.display_type = '' OR pol.display_type IS NULL) AND pol.is_downpayment AND pol.qty_invoiced > 0)
""")
@api.model
def _select_am_line(self):
return SQL("""
SELECT -aml.id,
NULL as pol_id,
aml.id as aml_id,
aml.company_id as company_id,
am.partner_id as partner_id,
aml.product_id as product_id,
aml.quantity as line_qty,
aml.product_uom_id as line_uom_id,
NULL as qty_invoiced,
NULL as qty_to_invoice,
NULL as purchase_order_id,
am.id as account_move_id,
aml.amount_currency as line_amount_untaxed,
aml.currency_id as currency_id,
aml.parent_state as state
FROM account_move_line aml
LEFT JOIN account_move am on aml.move_id = am.id
WHERE aml.display_type = 'product'
AND am.move_type in ('in_invoice', 'in_refund')
AND aml.parent_state in ('draft', 'posted')
AND aml.purchase_line_id IS NULL
""")
@property
def _table_query(self):
return SQL("%s UNION ALL %s", self._select_po_line(), self._select_am_line())
def action_open_line(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'account.move' if self.account_move_id else 'purchase.order',
'view_mode': 'form',
'res_id': self.account_move_id.id if self.account_move_id else self.purchase_order_id.id,
}
@api.model
def _action_create_bill_from_po_lines(self, partner, po_lines):
""" Create a new vendor bill with the selected PO lines and returns an action to open it """
bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': partner.id,
})
bill._add_purchase_order_lines(po_lines)
return bill._get_records_action()
def action_match_lines(self):
if not self.pol_id: # we need POL(s) to either match or create bill
raise UserError(_("You must select at least one Purchase Order line to match or create bill."))
if not self.aml_id: # select POL(s) without AML -> create a draft bill with the POL(s)
return self._action_create_bill_from_po_lines(self.partner_id, self.pol_id)
pol_by_product = self.pol_id.grouped('product_id')
aml_by_product = self.aml_id.grouped('product_id')
residual_purchase_order_lines = self.pol_id
residual_account_move_lines = self.aml_id
# Match all matchable POL-AML lines and remove them from the residual group
for product, po_line in pol_by_product.items():
po_line = po_line[0] # in case of multiple POL with same product, only match the first one
matching_bill_lines = aml_by_product.get(product)
if matching_bill_lines:
matching_bill_lines.purchase_line_id = po_line.id
residual_purchase_order_lines -= po_line
residual_account_move_lines -= matching_bill_lines
if len(residual_bill := self.aml_id.move_id) == 1:
# Delete all unmatched selected AML
if residual_account_move_lines:
residual_account_move_lines.unlink()
# Add all remaining POL to the residual bill
residual_bill._add_purchase_order_lines(residual_purchase_order_lines)
def action_add_to_po(self):
if not self or not self.aml_id:
raise UserError(_("Select Vendor Bill lines to add to a Purchase Order"))
partner = self.mapped("partner_id.commercial_partner_id")
if len(partner) > 1:
raise UserError(_("Please select bill lines with the same vendor."))
context = {
'default_partner_id': partner.id,
'dialog_size': 'medium',
'has_products': bool(self.aml_id.product_id),
}
if len(self.purchase_order_id) > 1:
raise UserError(_("Vendor Bill lines can only be added to one Purchase Order."))
elif self.purchase_order_id:
context['default_purchase_order_id'] = self.purchase_order_id.id
return {
'type': 'ir.actions.act_window',
'name': _("Add to Purchase Order"),
'res_model': 'bill.to.po.wizard',
'target': 'new',
'views': [(self.env.ref('purchase.bill_to_po_wizard_form').id, 'form')],
'context': context,
}

File diff suppressed because it is too large Load diff

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