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

@ -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

View file

@ -0,0 +1,725 @@
from collections import defaultdict
from datetime import datetime, time
from dateutil.relativedelta import relativedelta
from pytz import UTC
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, get_lang
from odoo.tools.float_utils import float_compare, float_round
class PurchaseOrderLine(models.Model):
_name = 'purchase.order.line'
_inherit = ['analytic.mixin']
_description = 'Purchase Order Line'
_order = 'order_id, sequence, id'
name = fields.Text(
string='Description', required=True, compute='_compute_price_unit_and_date_planned_and_name', store=True, readonly=False)
sequence = fields.Integer(string='Sequence', default=10)
product_qty = fields.Float(string='Quantity', digits='Product Unit', required=True)
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
date_planned = fields.Datetime(
string='Expected Arrival', index=True,
compute="_compute_price_unit_and_date_planned_and_name", readonly=False, store=True,
help="Delivery date expected from vendor. This date respectively defaults to vendor pricelist lead time then today's date.")
discount = fields.Float(
string="Discount (%)",
compute='_compute_price_unit_and_date_planned_and_name',
digits='Discount',
store=True, readonly=False)
tax_ids = fields.Many2many('account.tax', string='Taxes', context={'active_test': False, 'hide_original_tax_ids': True})
allowed_uom_ids = fields.Many2many('uom.uom', compute='_compute_allowed_uom_ids')
product_uom_id = fields.Many2one('uom.uom', string='Unit', domain="[('id', 'in', allowed_uom_ids)]", ondelete='restrict')
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, index='btree_not_null', ondelete='restrict')
product_type = fields.Selection(related='product_id.type', readonly=True)
price_unit = fields.Float(
string='Unit Price', required=True, min_display_digits='Product Price', aggregator='avg',
compute="_compute_price_unit_and_date_planned_and_name", readonly=False, store=True)
price_unit_product_uom = fields.Float(
string='Unit Price Product UoM', min_display_digits='Product Price', compute="_compute_price_unit_product_uom",
help="The Price of one unit of the product's Unit of Measure")
price_unit_discounted = fields.Float('Unit Price (Discounted)', compute='_compute_price_unit_discounted')
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
price_tax = fields.Float(compute='_compute_amount', string='Tax', store=True)
order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade')
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
state = fields.Selection(related='order_id.state')
invoice_lines = fields.One2many('account.move.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False)
# Replace by invoiced Qty
qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", digits='Product Unit', store=True)
qty_received_method = fields.Selection([('manual', 'Manual')], string="Received Qty Method", compute='_compute_qty_received_method', store=True,
help="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")
qty_received = fields.Float("Received Qty", compute='_compute_qty_received', inverse='_inverse_qty_received', compute_sudo=True, store=True, digits='Product Unit')
qty_received_manual = fields.Float("Manual Received Qty", digits='Product Unit', copy=False)
qty_to_invoice = fields.Float(compute='_compute_qty_invoiced', string='To Invoice Quantity', store=True, readonly=True,
digits='Product Unit')
# Same than `qty_received` and `qty_to_invoice` but non-stored and depending of the context.
qty_received_at_date = fields.Float(
string="Received",
compute='_compute_qty_received_at_date',
digits='Product Unit'
)
qty_invoiced_at_date = fields.Float(
string="Billed",
compute='_compute_qty_invoiced_at_date',
digits='Product Unit'
)
amount_to_invoice_at_date = fields.Float(string='Amount', compute='_compute_amount_to_invoice_at_date')
partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True, index='btree_not_null')
currency_id = fields.Many2one(related='order_id.currency_id', string='Currency')
date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True)
date_approve = fields.Datetime(related="order_id.date_approve", string='Confirmation Date', readonly=True)
tax_calculation_rounding_method = fields.Selection(
related='company_id.tax_calculation_rounding_method',
string='Tax calculation rounding method', readonly=True)
display_type = fields.Selection([
('line_section', "Section"),
('line_subsection', "Subsection"),
('line_note', "Note")], default=False, help="Technical field for UX purpose.")
is_downpayment = fields.Boolean()
selected_seller_id = fields.Many2one('product.supplierinfo', compute='_compute_selected_seller_id', help='Technical field to get the vendor pricelist used to generate this line')
_accountable_required_fields = models.Constraint(
'CHECK(display_type IS NOT NULL OR is_downpayment OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL AND date_planned IS NOT NULL))',
'Missing required fields on accountable purchase order line.',
)
_non_accountable_null_fields = models.Constraint(
'CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom_id IS NULL AND date_planned is NULL))',
'Forbidden values on non-accountable purchase order line',
)
product_template_attribute_value_ids = fields.Many2many(related='product_id.product_template_attribute_value_ids', readonly=True)
product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string='Product attribute values that do not create variants', ondelete='restrict')
purchase_line_warn_msg = fields.Text(compute='_compute_purchase_line_warn_msg')
parent_id = fields.Many2one(
'purchase.order.line',
string="Parent Section Line",
compute='_compute_parent_id',
)
technical_price_unit = fields.Float(help="Technical field for price computation")
@api.depends('product_qty', 'price_unit', 'tax_ids', 'discount')
def _compute_amount(self):
AccountTax = self.env['account.tax']
for line in self:
company = line.company_id or self.env.company
base_line = line._prepare_base_line_for_taxes_computation()
AccountTax._add_tax_details_in_base_line(base_line, company)
AccountTax._round_base_lines_tax_details([base_line], company)
line.price_subtotal = base_line['tax_details']['total_excluded_currency']
line.price_total = base_line['tax_details']['total_included_currency']
line.price_tax = line.price_total - line.price_subtotal
def _prepare_base_line_for_taxes_computation(self):
""" Convert the current record to a dictionary in order to use the generic taxes computation method
defined on account.tax.
:return: A python dictionary.
"""
self.ensure_one()
company = self.order_id.company_id or self.env.company
return self.env['account.tax']._prepare_base_line_for_taxes_computation(
self,
tax_ids=self.tax_ids,
quantity=self.product_qty,
partner_id=self.order_id.partner_id,
currency_id=self.order_id.currency_id or company.currency_id,
rate=self.order_id.currency_rate,
name=self.name,
)
def _compute_tax_id(self):
for line in self:
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_id.partner_id)
# filter taxes by company
taxes = line.product_id.supplier_taxes_id._filter_taxes_by_company(line.company_id)
line.tax_ids = fpos.map_tax(taxes)
@api.depends('discount', 'price_unit')
def _compute_price_unit_discounted(self):
for line in self:
line.price_unit_discounted = line.price_unit * (1 - line.discount / 100)
@api.depends('product_uom_id', 'price_unit')
def _compute_price_unit_product_uom(self):
for line in self:
line.price_unit_product_uom = not line.display_type and not line.is_downpayment and line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_id)
@api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity', 'qty_received', 'product_uom_qty', 'order_id.state')
def _compute_qty_invoiced(self):
invoiced_quantities = self._prepare_qty_invoiced()
for line in self:
line.qty_invoiced = invoiced_quantities[line]
# compute qty_to_invoice
if line.order_id.state == 'purchase':
if line.product_id.purchase_method == 'purchase':
line.qty_to_invoice = line.product_qty - line.qty_invoiced
else:
line.qty_to_invoice = line.qty_received - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.depends('qty_invoiced')
@api.depends_context('accrual_entry_date')
def _compute_qty_invoiced_at_date(self):
if not self._date_in_the_past():
for line in self:
line.qty_invoiced_at_date = line.qty_invoiced
return
invoiced_quantities = self._prepare_qty_invoiced()
for line in self:
line.qty_invoiced_at_date = invoiced_quantities[line]
def _prepare_qty_invoiced(self):
# Compute qty_invoiced
invoiced_qties = defaultdict(float)
for line in self:
for inv_line in line._get_invoice_lines():
if inv_line.move_id.state not in ['cancel'] or inv_line.move_id.payment_state == 'invoicing_legacy':
if inv_line.move_id.move_type == 'in_invoice':
invoiced_qties[line] += inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom_id)
elif inv_line.move_id.move_type == 'in_refund':
invoiced_qties[line] -= inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom_id)
return invoiced_qties
def _get_invoice_lines(self):
self.ensure_one()
if self.env.context.get('accrual_entry_date'):
accrual_date = fields.Date.from_string(self.env.context['accrual_entry_date'])
return self.invoice_lines.filtered(
lambda l: l.move_id.invoice_date and l.move_id.invoice_date <= accrual_date
)
else:
return self.invoice_lines
@api.depends('product_id.purchase_line_warn_msg')
def _compute_purchase_line_warn_msg(self):
has_warning_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_warning_group else ""
@api.depends('product_id', 'product_id.type')
def _compute_qty_received_method(self):
for line in self:
if line.product_id and line.product_id.type in ['consu', 'service']:
line.qty_received_method = 'manual'
else:
line.qty_received_method = False
@api.depends('qty_received_method', 'qty_received_manual')
def _compute_qty_received(self):
received_qties = self._prepare_qty_received()
for line in self:
if not line.qty_received or line in received_qties:
line.qty_received = received_qties[line]
@api.depends('qty_received')
@api.depends_context('accrual_entry_date')
def _compute_qty_received_at_date(self):
if not self._date_in_the_past():
for line in self:
line.qty_received_at_date = line.qty_received
return
received_quantities = self._prepare_qty_received()
for line in self:
line.qty_received_at_date = received_quantities[line]
def _prepare_qty_received(self):
received_qties = defaultdict(float)
for line in self:
if line.qty_received_method == 'manual':
received_qties[line] = line.qty_received_manual or 0.0
else:
received_qties[line] = 0.0
return received_qties
@api.onchange('qty_received')
def _inverse_qty_received(self):
""" When writing on qty_received, if the value should be modify manually (`qty_received_method` = 'manual' only),
then we put the value in `qty_received_manual`. Otherwise, `qty_received_manual` should be False since the
received qty is automatically compute by other mecanisms.
"""
for line in self:
if line.qty_received_method == 'manual':
line.qty_received_manual = line.qty_received
else:
line.qty_received_manual = 0.0
@api.depends('product_id', 'product_id.seller_ids', 'partner_id', 'product_qty', 'order_id.date_order', 'product_uom_id')
def _compute_selected_seller_id(self):
for line in self:
if line.product_id:
params = line._get_select_sellers_params()
seller = line.product_id._select_seller(
partner_id=line.partner_id,
quantity=abs(line.product_qty),
date=line.order_id.date_order and line.order_id.date_order.date() or fields.Date.context_today(line),
uom_id=line.product_uom_id,
params=params)
line.selected_seller_id = seller.id if seller else False
else:
line.selected_seller_id = False
@api.depends('price_unit', 'qty_invoiced_at_date', 'qty_received_at_date')
@api.depends_context('accrual_entry_date')
def _compute_amount_to_invoice_at_date(self):
for line in self:
line.amount_to_invoice_at_date = (line.qty_received_at_date - line.qty_invoiced_at_date) * line.price_unit
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
if values.get('display_type', self.default_get(['display_type'])['display_type']):
values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom_id=False, date_planned=False)
else:
values.update(self._prepare_add_missing_fields(values))
if values.get('price_unit') and not values.get('technical_price_unit'):
values['technical_price_unit'] = values['price_unit']
lines = super().create(vals_list)
for line in lines:
if line.product_id and line.order_id.state == 'purchase':
msg = _("Extra line with %s ", line.product_id.display_name)
line.order_id.message_post(body=msg)
return lines
def write(self, vals):
values = vals
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
raise UserError(_("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."))
if 'product_qty' in values:
precision = self.env['decimal.precision'].precision_get('Product Unit')
for line in self:
if (
line.order_id.state == "purchase"
and float_compare(line.product_qty, values["product_qty"], precision_digits=precision) != 0
):
line.order_id.message_post_with_source(
'purchase.track_po_line_template',
render_values={'line': line, 'product_qty': values['product_qty']},
subtype_xmlid='mail.mt_note',
)
if 'qty_received' in values:
for line in self:
line._track_qty_received(values['qty_received'])
return super().write(values)
@api.ondelete(at_uninstall=False)
def _unlink_except_purchase(self):
for line in self:
if line.order_id.state == 'purchase' and line.display_type not in ['line_section', 'line_subsection', 'line_note']:
state_description = {state_desc[0]: state_desc[1] for state_desc in self._fields['state']._description_selection(self.env)}
raise UserError(_('Cannot delete a purchase order line which is in state “%s”.', state_description.get(line.state)))
@api.model
def _get_date_planned(self, seller, po=False):
"""Return the datetime value to use as Schedule Date (``date_planned``) for
PO Lines that correspond to the given product.seller_ids,
when ordered at `date_order_str`.
:param Model seller: used to fetch the delivery delay (if no seller
is provided, the delay is 0)
:param Model po: purchase.order, necessary only if the PO line is
not yet attached to a PO.
:rtype: datetime
:return: desired Schedule Date for the PO line
"""
date_order = po.date_order if po else self.order_id.date_order
if date_order:
return date_order + relativedelta(days=seller.delay if seller else 0)
else:
return datetime.today() + relativedelta(days=seller.delay if seller else 0)
@api.depends('product_id', 'order_id.partner_id')
def _compute_analytic_distribution(self):
for line in self:
if not line.display_type:
distribution = self.env['account.analytic.distribution.model']._get_distribution({
"product_id": line.product_id.id,
"product_categ_id": line.product_id.categ_id.id,
"partner_id": line.order_id.partner_id.id,
"partner_category_id": line.order_id.partner_id.category_id.ids,
"company_id": line.company_id.id,
})
line.analytic_distribution = distribution or line.analytic_distribution
@api.onchange('product_id')
def onchange_product_id(self):
# TODO: Remove when onchanges are replaced with computes
if not self.product_id or (self.env.context.get('origin_po_id') and self.product_qty):
return
# Reset date, price and quantity since _onchange_quantity will provide default values
self.price_unit = self.product_qty = self.technical_price_unit = 0.0
self._product_id_change()
self._suggest_quantity()
def _product_id_change(self):
if not self.product_id:
return
self.product_uom_id = self.product_id.uom_id
product_lang = self.product_id.with_context(
lang=get_lang(self.env, self.partner_id.lang).code,
partner_id=None,
company_id=self.company_id.id,
)
self.name = self._get_product_purchase_description(product_lang)
self._compute_tax_id()
@api.depends('product_id', 'product_id.uom_id', 'product_id.uom_ids', 'product_id.seller_ids', 'product_id.seller_ids.product_uom_id')
def _compute_allowed_uom_ids(self):
for line in self:
line.allowed_uom_ids = line.product_id.uom_id | line.product_id.uom_ids | line.product_id.seller_ids.product_uom_id
@api.depends('product_qty', 'product_uom_id', 'company_id', 'order_id.partner_id')
def _compute_price_unit_and_date_planned_and_name(self):
for line in self:
if not line.product_id or line.invoice_lines or not line.company_id or self.env.context.get('skip_uom_conversion') or (line.technical_price_unit != line.price_unit):
continue
params = line._get_select_sellers_params()
if line.selected_seller_id or not line.date_planned:
line.date_planned = line._get_date_planned(line.selected_seller_id).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
# If not seller, use the standard price. It needs a proper currency conversion.
if not line.selected_seller_id:
unavailable_seller = line.product_id.seller_ids.filtered(
lambda s: s.partner_id == line.order_id.partner_id)
if not unavailable_seller and line.price_unit and line.product_uom_id == line._origin.product_uom_id:
# Avoid to modify the price unit if there is no price list for this partner and
# the line has already one to avoid to override unit price set manually.
continue
line.discount = 0
po_line_uom = line.product_uom_id or line.product_id.uom_id
price_unit = line.env['account.tax']._fix_tax_included_price_company(
line.product_id.uom_id._compute_price(line.product_id.standard_price, po_line_uom),
line.product_id.supplier_taxes_id,
line.tax_ids,
line.company_id,
)
price_unit = line.product_id.cost_currency_id._convert(
price_unit,
line.currency_id,
line.company_id,
line.date_order or fields.Date.context_today(line),
False
)
line.price_unit = line.technical_price_unit = float_round(price_unit, precision_digits=max(line.currency_id.decimal_places, self.env['decimal.precision'].precision_get('Product Price')))
elif line.selected_seller_id:
price_unit = line.env['account.tax']._fix_tax_included_price_company(line.selected_seller_id.price, line.product_id.supplier_taxes_id, line.tax_ids, line.company_id) if line.selected_seller_id else 0.0
price_unit = line.selected_seller_id.currency_id._convert(price_unit, line.currency_id, line.company_id, line.date_order or fields.Date.context_today(line), False)
price_unit = float_round(price_unit, precision_digits=max(line.currency_id.decimal_places, self.env['decimal.precision'].precision_get('Product Price')))
line.price_unit = line.technical_price_unit = line.selected_seller_id.product_uom_id._compute_price(price_unit, line.product_uom_id)
line.discount = line.selected_seller_id.discount or 0.0
# record product names to avoid resetting custom descriptions
default_names = []
vendors = line.product_id._prepare_sellers(params=params)
product_ctx = {'seller_id': None, 'partner_id': None, 'lang': get_lang(line.env, line.partner_id.lang).code}
default_names.append(line._get_product_purchase_description(line.product_id.with_context(product_ctx)))
for vendor in vendors:
product_ctx = {'seller_id': vendor.id, 'lang': get_lang(line.env, line.partner_id.lang).code}
default_names.append(line._get_product_purchase_description(line.product_id.with_context(product_ctx)))
if not line.name or line.name in default_names:
product_ctx = {'seller_id': line.selected_seller_id.id, 'lang': get_lang(line.env, line.partner_id.lang).code}
line.name = line._get_product_purchase_description(line.product_id.with_context(product_ctx))
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
def _compute_product_uom_qty(self):
for line in self:
if line.product_id and line.product_id.uom_id != line.product_uom_id:
line.product_uom_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_id)
else:
line.product_uom_qty = line.product_qty
def _get_gross_price_unit(self):
self.ensure_one()
price_unit = self.price_unit
if self.discount:
price_unit = price_unit * (1 - self.discount / 100)
if self.tax_ids:
qty = self.product_qty or 1
price_unit = self.tax_ids.compute_all(
price_unit,
currency=self.order_id.currency_id,
quantity=qty,
rounding_method='round_globally',
)['total_void']
price_unit = price_unit / qty
if self.product_uom_id.id != self.product_id.uom_id.id:
price_unit *= self.product_id.uom_id.factor / self.product_uom_id.factor
return price_unit
def _compute_parent_id(self):
purchase_order_lines = set(self)
for order, lines in self.grouped('order_id').items():
if not order:
lines.parent_id = False
continue
last_section = False
last_sub = False
for line in order.order_line.sorted('sequence'):
if line.display_type == 'line_section':
last_section = line
if line in purchase_order_lines:
line.parent_id = False
last_sub = False
elif line.display_type == 'line_subsection':
if line in purchase_order_lines:
line.parent_id = last_section
last_sub = line
elif line in purchase_order_lines:
line.parent_id = last_sub or last_section
def action_add_from_catalog(self):
order = self.env['purchase.order'].browse(self.env.context.get('order_id'))
return order.with_context(child_field='order_line').action_add_from_catalog()
def _suggest_quantity(self):
''' Suggest a minimal quantity based on the seller
'''
if not self.product_id:
return
date = self.order_id.date_order and self.order_id.date_order.date() or fields.Date.context_today(self)
seller_min_qty = self.product_id.seller_ids\
.filtered(lambda r: r.partner_id == self.order_id.partner_id and
(not r.product_id or r.product_id == self.product_id) and
(not r.date_start or r.date_start <= date) and
(not r.date_end or r.date_end >= date))\
.sorted(key=lambda r: r.min_qty)
if seller_min_qty:
self.product_qty = seller_min_qty[0].min_qty or 1.0
self.product_uom_id = seller_min_qty[0].product_uom_id
else:
self.product_qty = 1.0
def _get_product_catalog_lines_data(self, **kwargs):
""" Return information about purchase order lines in `self`.
If `self` is empty, this method returns only the default value(s) needed for the product
catalog. In this case, the quantity that equals 0.
Otherwise, it returns a quantity and a price based on the product of the POL(s) and whether
the product is read-only or not.
A product is considered read-only if the order is considered read-only (see
``PurchaseOrder._is_readonly`` for more details) or if `self` contains multiple records.
Note: This method cannot be called with multiple records that have different products linked.
:raise odoo.exceptions.ValueError: ``len(self.product_id) != 1``
:rtype: dict
:return: A dict with the following structure:
{
'quantity': float,
'price': float,
'readOnly': bool,
'uomDisplayName': String,
'packaging': dict,
'warning': String,
}
"""
if len(self) == 1:
catalog_info = self.order_id._get_product_price_and_data(self.product_id)
catalog_info.update(
quantity=self.product_qty,
price=self.price_unit * (1 - self.discount / 100),
readOnly=self.order_id._is_readonly(),
)
if self.product_id.uom_id != self.product_uom_id:
catalog_info['uomDisplayName'] = self.product_uom_id.display_name
return catalog_info
elif self:
self.product_id.ensure_one()
order_line = self[0]
catalog_info = order_line.order_id._get_product_price_and_data(order_line.product_id)
catalog_info['quantity'] = sum(self.mapped(
lambda line: line.product_uom_id._compute_quantity(
qty=line.product_qty,
to_unit=line.product_id.uom_id,
)))
catalog_info['readOnly'] = True
return catalog_info
return {'quantity': 0}
def _get_product_purchase_description(self, product_lang):
self.ensure_one()
name = product_lang.display_name
if product_lang.description_purchase:
name += '\n' + product_lang.description_purchase
return name
def _prepare_account_move_line(self, move=False):
self.ensure_one()
aml_currency = move and move.currency_id or self.currency_id
date = move and move.date or fields.Date.today()
res = {
'display_type': self.display_type or 'product',
'name': self.env['account.move.line']._get_journal_items_full_name(self.name, self.product_id.display_name),
'product_id': self.product_id.id,
'product_uom_id': self.product_uom_id.id,
'quantity': -self.qty_to_invoice if move and move.move_type == 'in_refund' else self.qty_to_invoice,
'discount': self.discount,
'price_unit': self.currency_id._convert(self.price_unit, aml_currency, self.company_id, date, round=False),
'tax_ids': [(6, 0, self.tax_ids.ids)],
'purchase_line_id': self.id,
'is_downpayment': self.is_downpayment,
}
return res
@api.model
def _prepare_add_missing_fields(self, values):
""" Deduce missing required fields from the onchange """
res = {}
onchange_fields = ['name', 'price_unit', 'product_qty', 'product_uom_id', 'tax_ids', 'date_planned']
if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields):
line = self.new(values)
line.onchange_product_id()
for field in onchange_fields:
if field not in values:
res[field] = line._fields[field].convert_to_write(line[field], line)
return res
@api.model
def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, company_id, partner_id, po):
values = self.env.context.get('procurement_values', {})
uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_id, rounding_method='HALF-UP')
# _select_seller is used if the supplier have different price depending
# the quantities ordered.
today = fields.Date.today()
seller = product_id.with_company(company_id)._select_seller(
partner_id=partner_id,
quantity=product_qty if values.get('force_uom') else uom_po_qty,
date=po.date_order and max(po.date_order.date(), today) or today,
uom_id=product_uom if values.get('force_uom') else product_id.uom_id,
params={'force_uom': values.get('force_uom')}
)
if seller and (seller.product_uom_id or seller.product_tmpl_id.uom_id) != product_uom:
uom_po_qty = product_id.uom_id._compute_quantity(uom_po_qty, seller.product_uom_id, rounding_method='HALF-UP')
tax_domain = self.env['account.tax']._check_company_domain(company_id)
product_taxes = product_id.supplier_taxes_id.filtered_domain(tax_domain)
taxes = po.fiscal_position_id.map_tax(product_taxes)
if seller:
price_unit = (seller.product_uom_id._compute_price(seller.price, product_uom) if product_uom else seller.price)
price_unit = self.env['account.tax']._fix_tax_included_price_company(
price_unit, product_taxes, taxes, company_id)
else:
price_unit = 0
if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
price_unit = seller.currency_id._convert(
price_unit, po.currency_id, po.company_id, po.date_order or fields.Date.today())
product_lang = product_id.with_prefetch().with_context(
lang=partner_id.lang,
partner_id=partner_id.id,
)
name = product_lang.with_context(seller_id=seller.id).display_name
if product_lang.description_purchase:
name += '\n' + product_lang.description_purchase
date_planned = self.order_id.date_planned or self._get_date_planned(seller, po=po)
discount = seller.discount or 0.0
return {
'name': name,
'product_qty': product_qty if product_uom else uom_po_qty,
'product_id': product_id.id,
'product_uom_id': product_uom.id or seller.product_uom_id.id,
'price_unit': price_unit,
'date_planned': date_planned,
'tax_ids': [(6, 0, taxes.ids)],
'order_id': po.id,
'discount': discount,
}
def _convert_to_middle_of_day(self, date):
"""Return a datetime which is the noon of the input date(time) according
to order user's time zone, convert to UTC time.
"""
return self.order_id.get_order_timezone().localize(datetime.combine(date, time(12))).astimezone(UTC).replace(tzinfo=None)
@api.model
def _date_in_the_past(self):
if not 'accrual_entry_date' in self.env.context:
return False
accrual_date = fields.Date.from_string(self.env.context['accrual_entry_date'])
return accrual_date < fields.Date.today()
def _update_date_planned(self, updated_date):
self.date_planned = updated_date
def _track_qty_received(self, new_qty):
self.ensure_one()
# don't track anything when coming from the accrued expense entry wizard, as it is only computing fields at a past date to get relevant amounts
# and doesn't actually change anything to the current record
if self.env.context.get('accrual_entry_date'):
return
if new_qty != self.qty_received and self.order_id.state == 'purchase':
self.order_id.message_post_with_source(
'purchase.track_po_line_qty_received_template',
render_values={'line': self, 'qty_received': new_qty},
subtype_xmlid='mail.mt_note',
)
def _validate_analytic_distribution(self):
for line in self:
if line.display_type:
continue
line._validate_distribution(
product=line.product_id.id,
business_domain='purchase_order',
company_id=line.company_id.id,
)
def action_open_order(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'purchase.order',
'res_id': self.order_id.id,
'view_mode': 'form',
}
def _merge_po_line(self, rfq_line):
self.product_qty += rfq_line.product_qty
self.price_unit = min(self.price_unit, rfq_line.price_unit)
def _get_select_sellers_params(self):
self.ensure_one()
return {
"order_id": self.order_id,
"force_uom": True,
}
def get_parent_section_line(self):
if not self.display_type and self.parent_id.display_type == 'line_subsection':
return self.parent_id.parent_id
return self.parent_id

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