mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 01:52:00 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -4,4 +4,4 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import populate
|
||||
from . import wizard
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<ul>
|
||||
<li><t t-esc="line.product_id.display_name"/>:</li>
|
||||
Ordered Quantity: <t t-esc="line.product_qty" /> -> <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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
File diff suppressed because it is too large
Load diff
|
|
@ -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
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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
1415
odoo-bringout-oca-ocb-purchase/purchase/models/purchase_order.py
Normal file
1415
odoo-bringout-oca-ocb-purchase/purchase/models/purchase_order.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue