From c0e320646067e4ae288693b0eeda6671181e045a Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sun, 28 Aug 2016 00:24:41 +0200 Subject: [PATCH 01/20] Initial check-in of modules purchase_order_import, purchase_order_import_ubl and purchase_order_ubl --- purchase_order_import/README.rst | 72 +++++ purchase_order_import/__init__.py | 3 + purchase_order_import/__openerp__.py | 19 ++ purchase_order_import/views/purchase.xml | 24 ++ purchase_order_import/wizard/__init__.py | 3 + .../wizard/purchase_order_import.py | 271 ++++++++++++++++++ .../wizard/purchase_order_import_view.xml | 53 ++++ 7 files changed, 445 insertions(+) create mode 100644 purchase_order_import/README.rst create mode 100644 purchase_order_import/__init__.py create mode 100644 purchase_order_import/__openerp__.py create mode 100644 purchase_order_import/views/purchase.xml create mode 100644 purchase_order_import/wizard/__init__.py create mode 100644 purchase_order_import/wizard/purchase_order_import.py create mode 100644 purchase_order_import/wizard/purchase_order_import_view.xml diff --git a/purchase_order_import/README.rst b/purchase_order_import/README.rst new file mode 100644 index 0000000000..e764d13541 --- /dev/null +++ b/purchase_order_import/README.rst @@ -0,0 +1,72 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +===================== +Purchase Order Import +===================== + +This module adds support for the import of electronic quotations. This module provides the base methods to import electronic quotations ; it requires additionnal modules to support specific order formats: + +* module *purchase_order_import_ubl*: adds support for `Universal Business Language (UBL) `_ quotations as: + + - XML file, + - PDF file with an embedded XML file. + +Configuration +============= + +No configuration is needed. + +Usage +===== + +This module adds a button *Import Quotation File* on Requests for Quotation. This button starts a wizard that will propose you to select the quotation file. The wizard will also propose you an update option: + +* only update the prices of the draft purchase order from the quotation file (default option), +* update prices and quantities of the draft purchase order from the quotation file. + +When you click on the button *Update RFQ*: + +* if Odoo has a line in the quotation file that is not in the draft purchase order, it will create a new purchase order line, +* if Odoo has a line in the draft purchase order that is not in the quotation file, it will write a warning in the chatter of the purchase order (it will not delete the purchase order line), +* for all the lines that are both in the draft purchase order and in the quotation file, the purchase order line will be updated if needed. +* if the incoterm of the quotation file is not the same as the incoterm of the draft purchase order, Odoo will update the incoterm of the purchase order. +* the imported quotation file is attached to the purchase order. + +Once the quotation file is imported, you should read the messages in the chatter of the purchase order because it may contain important information about the import. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/142/8.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/purchase_order_import/__init__.py b/purchase_order_import/__init__.py new file mode 100644 index 0000000000..3b4c3edf09 --- /dev/null +++ b/purchase_order_import/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import wizard diff --git a/purchase_order_import/__openerp__.py b/purchase_order_import/__openerp__.py new file mode 100644 index 0000000000..4dfd483fba --- /dev/null +++ b/purchase_order_import/__openerp__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Purchase Order Import', + 'version': '8.0.1.0.0', + 'category': 'Purchase Management', + 'license': 'AGPL-3', + 'summary': 'Update RFQ via the import of quotations from suppliers', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'website': 'http://www.akretion.com', + 'depends': ['purchase', 'base_business_document_import_stock'], + 'data': [ + 'wizard/purchase_order_import_view.xml', + 'views/purchase.xml', + ], + 'installable': True, +} diff --git a/purchase_order_import/views/purchase.xml b/purchase_order_import/views/purchase.xml new file mode 100644 index 0000000000..7d765dfc4a --- /dev/null +++ b/purchase_order_import/views/purchase.xml @@ -0,0 +1,24 @@ + + + + + + + + purchase_order_import.purchase.order.form + purchase.order + + + + + + + + diff --git a/purchase_order_import/wizard/__init__.py b/purchase_order_import/wizard/__init__.py new file mode 100644 index 0000000000..627d4faa91 --- /dev/null +++ b/purchase_order_import/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import purchase_order_import diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py new file mode 100644 index 0000000000..612b764751 --- /dev/null +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields, api, _ +from openerp.tools import float_is_zero +from openerp.exceptions import Warning as UserError +import logging +import mimetypes +from lxml import etree + +logger = logging.getLogger(__name__) + + +class PurchaseOrderImport(models.TransientModel): + _name = 'purchase.order.import' + _inherit = ['business.document.import'] + _description = 'Purchase Order Import from Files' + + @api.model + def _get_purchase_id(self): + assert self._context['active_model'] == 'purchase.order',\ + 'bad active_model' + return self.env['purchase.order'].browse(self._context['active_id']) + + quote_file = fields.Binary( + string='XML or PDF Quotation', required=True, + help="Upload a quotation file that you received from " + "your supplier. Supported formats: XML and PDF " + "(PDF with an embeded XML file).") + quote_filename = fields.Char(string='Filename') + update_option = fields.Selection([ + ('price', 'Price'), + ('all', 'Price and Quantity'), + ], default='price', string='Update Option', required=True) + purchase_id = fields.Many2one( + 'purchase.order', string='RFQ to Update', default=_get_purchase_id, + readonly=True) + + @api.model + def parse_xml_quote(self, xml_root): + raise UserError(_( + "This type of XML quotation is not supported. Did you install " + "the module to support this XML format?")) + + @api.model + def parse_pdf_quote(self, quote_file): + """ + Get PDF attachments, filter on XML files and call import_order_xml + """ + xml_files_dict = self.get_xml_files_from_pdf(quote_file) + if not xml_files_dict: + raise UserError(_( + 'There are no embedded XML file in this PDF file.')) + for xml_filename, xml_root in xml_files_dict.iteritems(): + logger.info('Trying to parse XML file %s', xml_filename) + try: + parsed_quote = self.parse_xml_quote(xml_root) + return parsed_quote + except: + continue + raise UserError(_( + "This type of XML quotation is not supported. Did you install " + "the module to support this XML format?")) + + # Format of parsed_quote + # { + # 'partner': { + # 'vat': 'FR25499247138', + # 'name': 'Camptocamp', + # 'email': 'luc@camptocamp.com', + # }, + # 'currency': {'iso': 'EUR', 'symbol': u'€'}, + # 'incoterm': 'EXW', + # 'note': 'some notes', + # 'chatter_msg': ['msg1', 'msg2'] + # 'lines': [{ + # 'product': { + # 'code': 'EA7821', + # 'ean13': '2100002000003', + # }, + # 'qty': 2.5, + # 'uom': {'unece_code': 'C62'}, + # 'price_unit': 12.42, # without taxes + # }] + + @api.model + def parse_quote(self, quote_file, quote_filename): + assert quote_file, 'Missing quote file' + assert quote_filename, 'Missing quote filename' + filetype = mimetypes.guess_type(quote_filename)[0] + logger.debug('Quote file mimetype: %s', filetype) + if filetype == 'application/xml': + try: + xml_root = etree.fromstring(quote_file) + except: + raise UserError(_("This XML file is not XML-compliant")) + pretty_xml_string = etree.tostring( + xml_root, pretty_print=True, encoding='UTF-8', + xml_declaration=True) + logger.debug('Starting to import the following XML file:') + logger.debug(pretty_xml_string) + parsed_quote = self.parse_xml_quote(xml_root) + elif filetype == 'application/pdf': + parsed_quote = self.parse_pdf_quote(quote_file) + else: + raise UserError(_( + "This file '%s' is not recognised as XML nor PDF file. " + "Please check the file and it's extension.") % quote_filename) + logger.debug('Result of quotation parsing: %s', parsed_quote) + if 'attachments' not in parsed_quote: + parsed_quote['attachments'] = {} + parsed_quote['attachments'][quote_filename] =\ + quote_file.encode('base64') + if 'chatter_msg' not in parsed_quote: + parsed_quote['chatter_msg'] = [] + return parsed_quote + + @api.model + def _prepare_update_order_vals(self, parsed_quote, order): + vals = {} + incoterm = self._match_incoterm( + parsed_quote.get('incoterm'), parsed_quote['chatter_msg']) + if incoterm and incoterm != order.incoterm_id: + parsed_quote['chatter_msg'].append(_( + "The incoterm has been updated from %s to %s upon import " + "of the quotation file '%s'") % ( + order.incoterm_id.code, incoterm.code, + self.quote_filename)) + vals['incoterm_id'] = incoterm.id + return vals + + @api.model + def _prepare_create_order_line( + self, product, qty, uom, price_unit, so_vals): + vals = { + 'product_id': product.id, + 'product_qty': qty, + 'product_uom': uom.id, + 'price_unit': price_unit, # TODO fix + } + return vals + + @api.multi + def update_order_lines(self, parsed_quote, order): + polo = self.env['purchase.order.line'] + chatter = parsed_quote['chatter_msg'] + dpo = self.env['decimal.precision'] + qty_prec = dpo.precision_get('Product Unit of Measure') + existing_lines = [] + for oline in order.order_line: + price_unit = 0.0 + if not float_is_zero( + oline.product_qty, precision_digits=qty_prec): + price_unit = oline.price_subtotal / float(oline.product_qty) + existing_lines.append({ + 'product': oline.product_id, + 'name': oline.name, + 'qty': oline.product_qty, + 'uom': oline.product_uom, + 'price_unit': price_unit, + 'line': oline, + }) + + compare_res = self.compare_lines( + existing_lines, parsed_quote['lines'], chatter, + seller=order.partner_id.commercial_partner_id) + + update_option = self.update_option + for oline, cdict in compare_res['to_update'].iteritems(): + write_vals = {} + if cdict.get('price_unit'): + chatter.append(_( + "The unit price has been updated on the RFQ line with " + "product '%s' from %s to %s %s.") % ( + oline.product_id.name_get()[0][1], + cdict['price_unit'][0], cdict['price_unit'][1], + order.currency_id.name)) + write_vals['price_unit'] = cdict['price_unit'][1] # TODO + if update_option == 'all' and cdict.get('qty'): + chatter.append(_( + "The quantity has been updated on the RFQ line with " + "product '%s' from %s to %s %s.") % ( + oline.product_id.name_get()[0][1], + cdict['qty'][0], cdict['qty'][1], + oline.product_uom.name)) + if write_vals: + oline.write(write_vals) + if compare_res['to_remove']: # we don't delete the lines, only warn + warn_label = [ + '%s %s x %s' % ( + l.product_qty, l.product_uom.name, l.product_id.name) + for l in compare_res['to_remove']] + chatter.append(_( + "%d order line(s) are not in the imported quotation: %s") % ( + len(compare_res['to_remove']), + ', '.join(warn_label))) + if compare_res['to_add']: + to_create_label = [] + for add in compare_res['to_add']: + line_vals = self._prepare_create_order_line( + add['product'], add['uom'], add['import_line'], + order) + line_vals['order_id'] = order.id + new_line = polo.create(line_vals) + to_create_label.append('%s %s x %s' % ( + new_line.product_qty, + new_line.product_uom.name, + new_line.name)) + chatter.append(_("%d new order line(s) created: %s") % ( + len(compare_res['to_add']), ', '.join(to_create_label))) + return True + + @api.model + def _prepare_create_order_line(self, product, uom, import_line, order): + polo = self.env['purchase.order.line'] + product_change_res = polo.onchange_product_id( + order.pricelist_id.id, product.id, + import_line['qty'], uom.id, + order.partner_id.id, + fiscal_position_id=order.fiscal_position.id or False, + price_unit=import_line['price_unit'])['value'] + if product_change_res.get('taxes_id'): + product_change_res['taxes_id'] = [ + (6, 0, product_change_res['taxes_id'])] + vals = product_change_res + vals['product_id'] = product.id + return vals + + @api.multi + def update_rfq_button(self): + self.ensure_one() + order = self.purchase_id + assert order, 'No link to PO' + if not order: + raise UserError(_('You must select a quotation to update.')) + parsed_quote = self.parse_quote( + self.quote_file.decode('base64'), self.quote_filename) + currency = self._match_currency( + parsed_quote.get('currency'), parsed_quote['chatter_msg']) + partner = self._match_partner( + parsed_quote['partner'], parsed_quote['chatter_msg'], + partner_type='supplier') + if ( + partner.commercial_partner_id != + order.partner_id.commercial_partner_id): + raise UserError(_( + "The supplier of the imported quotation (%s) is different " + "from the supplier of the RFQ (%s)." % ( + partner.commercial_partner_id.name, + order.partner_id.commercial_partner_id.name))) + if currency != order.currency_id: + raise UserError(_( + "The currency of the imported quotation (%s) is different " + "from the currency of the RFQ (%s)") % ( + currency.name, order.currency_id.name)) + vals = self._prepare_update_order_vals(parsed_quote, order) + if vals: + order.write(vals) + if not parsed_quote.get('lines'): + raise UserError(_( + "This quotation doesn't have any line !")) + self.update_order_lines(parsed_quote, order) + self.post_create_or_update(parsed_quote, order) + logger.info( + 'purchase.order ID %d updated via import of file %s', order.id, + self.quote_filename) + order.message_post(_( + "This RFQ has been updated automatically via the import of " + "quotation file %s") % self.quote_filename) + return True diff --git a/purchase_order_import/wizard/purchase_order_import_view.xml b/purchase_order_import/wizard/purchase_order_import_view.xml new file mode 100644 index 0000000000..2fd72927e3 --- /dev/null +++ b/purchase_order_import/wizard/purchase_order_import_view.xml @@ -0,0 +1,53 @@ + + + + + + + + purchase.order.import.form + purchase.order.import + +
+ +
+

Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:

+
    +
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module purchase_order_import_ubl.
  2. +
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. +
+

Then, Odoo will compare the imported quotation and the current RFQ:

+
    +
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • +
  • for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ,
  • +
  • for the lines that are present only in the RFQ, Odoo will put a warning message in the chatter (it won't delete them automatically).
  • +
+
+
+ + + + + + +
+
+
+
+
+ + + Import Quotation + purchase.order.import + form + new + + +
+
From a2686c29ee168caa599e2d49a2653146d63f8de6 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sun, 2 Oct 2016 22:32:31 +0200 Subject: [PATCH 02/20] FIX Don't use _inherit = ['business.document.import'] because we want to have access to the code of the modules that inherit business.document.import --- .../wizard/purchase_order_import.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 612b764751..e5002e5d40 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -14,7 +14,6 @@ class PurchaseOrderImport(models.TransientModel): _name = 'purchase.order.import' - _inherit = ['business.document.import'] _description = 'Purchase Order Import from Files' @api.model @@ -119,7 +118,7 @@ def parse_quote(self, quote_file, quote_filename): @api.model def _prepare_update_order_vals(self, parsed_quote, order): vals = {} - incoterm = self._match_incoterm( + incoterm = self.env['business.document.import']._match_incoterm( parsed_quote.get('incoterm'), parsed_quote['chatter_msg']) if incoterm and incoterm != order.incoterm_id: parsed_quote['chatter_msg'].append(_( @@ -146,6 +145,7 @@ def update_order_lines(self, parsed_quote, order): polo = self.env['purchase.order.line'] chatter = parsed_quote['chatter_msg'] dpo = self.env['decimal.precision'] + bdio = self.env['business.document.import'] qty_prec = dpo.precision_get('Product Unit of Measure') existing_lines = [] for oline in order.order_line: @@ -162,7 +162,7 @@ def update_order_lines(self, parsed_quote, order): 'line': oline, }) - compare_res = self.compare_lines( + compare_res = bdio.compare_lines( existing_lines, parsed_quote['lines'], chatter, seller=order.partner_id.commercial_partner_id) @@ -230,15 +230,16 @@ def _prepare_create_order_line(self, product, uom, import_line, order): @api.multi def update_rfq_button(self): self.ensure_one() + bdio = self.env['business.document.import'] order = self.purchase_id assert order, 'No link to PO' if not order: raise UserError(_('You must select a quotation to update.')) parsed_quote = self.parse_quote( self.quote_file.decode('base64'), self.quote_filename) - currency = self._match_currency( + currency = bdio._match_currency( parsed_quote.get('currency'), parsed_quote['chatter_msg']) - partner = self._match_partner( + partner = bdio._match_partner( parsed_quote['partner'], parsed_quote['chatter_msg'], partner_type='supplier') if ( @@ -261,7 +262,7 @@ def update_rfq_button(self): raise UserError(_( "This quotation doesn't have any line !")) self.update_order_lines(parsed_quote, order) - self.post_create_or_update(parsed_quote, order) + bdio.post_create_or_update(parsed_quote, order) logger.info( 'purchase.order ID %d updated via import of file %s', order.id, self.quote_filename) From 22605221658eddba9e486fd72731037325a1683d Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Tue, 4 Oct 2016 16:33:44 +0200 Subject: [PATCH 03/20] [IMP] handle the case where the xml file is generated with mime type 'text/xml' --- purchase_order_import/wizard/purchase_order_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index e5002e5d40..a90addfa34 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -89,7 +89,7 @@ def parse_quote(self, quote_file, quote_filename): assert quote_filename, 'Missing quote filename' filetype = mimetypes.guess_type(quote_filename)[0] logger.debug('Quote file mimetype: %s', filetype) - if filetype == 'application/xml': + if filetype in ['application/xml','text/xml']: try: xml_root = etree.fromstring(quote_file) except: From ef618ebb0540b7b91af269ac77d9311dbc582e1e Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Oct 2016 19:48:09 +0200 Subject: [PATCH 04/20] Add support for extraction + matching on website PEP8 fix --- purchase_order_import/wizard/purchase_order_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index a90addfa34..de70b9e294 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -89,7 +89,7 @@ def parse_quote(self, quote_file, quote_filename): assert quote_filename, 'Missing quote filename' filetype = mimetypes.guess_type(quote_filename)[0] logger.debug('Quote file mimetype: %s', filetype) - if filetype in ['application/xml','text/xml']: + if filetype in ['application/xml', 'text/xml']: try: xml_root = etree.fromstring(quote_file) except: From e4849abefdf9c8b21393a1f819388747358cdfd0 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 18 Oct 2016 23:02:55 +0200 Subject: [PATCH 05/20] 8.0 Add support for partner bank matching on invoice update (#6) Add support for partner bank matching on invoice update (before, it was only supported on invoice creation) --- purchase_order_import/README.rst | 4 +- purchase_order_import/i18n/fr.po | 284 +++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 purchase_order_import/i18n/fr.po diff --git a/purchase_order_import/README.rst b/purchase_order_import/README.rst index e764d13541..5d34043ae0 100644 --- a/purchase_order_import/README.rst +++ b/purchase_order_import/README.rst @@ -38,13 +38,13 @@ Once the quotation file is imported, you should read the messages in the chatter .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/142/8.0 + :target: https://runbot.odoo-community.org/runbot/226/8.0 Bug Tracker =========== Bugs are tracked on `GitHub Issues -`_. In case of trouble, please +`_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po new file mode 100644 index 0000000000..3eac1f1a21 --- /dev/null +++ b/purchase_order_import/i18n/fr.po @@ -0,0 +1,284 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_import +# +# Translators: +# OCA Transbot , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-11-12 13:36+0000\n" +"PO-Revision-Date: 2016-11-12 13:36+0000\n" +"Last-Translator: OCA Transbot , 2016\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:210 +#, python-format +msgid "%d new order line(s) created: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:194 +#, python-format +msgid "%d order line(s) are not in the imported quotation: %s" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "Cancel" +msgstr "Annuler" + +#. module: purchase_order_import +#: field:purchase.order.import,create_uid:0 +msgid "Created by" +msgstr "Créé par" + +#. module: purchase_order_import +#: field:purchase.order.import,create_date:0 +msgid "Created on" +msgstr "Créé le" + +#. module: purchase_order_import +#: field:purchase.order.import,display_name:0 +msgid "Display Name" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,quote_filename:0 +msgid "Filename" +msgstr "Nom du fichier" + +#. module: purchase_order_import +#: field:purchase.order.import,id:0 +msgid "ID" +msgstr "ID" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"If it is a PDF file, Odoo will try to find an XML file in the attachments of" +" the PDF file and then use this XML file." +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"If it is an XML file, Odoo will parse it if the module that adds support for" +" this XML format is installed. For the" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action +msgid "Import Quotation" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order:purchase_order_import.purchase_order_form +msgid "Import Quotation File" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "Import Quotations Files from Suppliers" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,__last_update:0 +msgid "Last Modified on" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,write_uid:0 +msgid "Last Updated by" +msgstr "Dernière mise-à-jour par" + +#. module: purchase_order_import +#: field:purchase.order.import,write_date:0 +msgid "Last Updated on" +msgstr "Dernière mise-à-jour le" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price" +msgstr "" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price and Quantity" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order_import +msgid "Purchase Order Import from Files" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,purchase_id:0 +msgid "RFQ to Update" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:254 +#, python-format +msgid "" +"The currency of the imported quotation (%s) is different from the currency " +"of the RFQ (%s)" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#, python-format +msgid "" +"The incoterm has been updated from %s to %s upon import of the quotation " +"file '%s'" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#, python-format +msgid "" +"The quantity has been updated on the RFQ line with product '%s' from %s to " +"%s %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 +#, python-format +msgid "" +"The supplier of the imported quotation (%s) is different from the supplier " +"of the RFQ (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#, python-format +msgid "" +"The unit price has been updated on the RFQ line with product '%s' from %s to" +" %s %s." +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "Then, Odoo will compare the imported quotation and the current RFQ:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#, python-format +msgid "There are no embedded XML file in this PDF file." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:269 +#, python-format +msgid "" +"This RFQ has been updated automatically via the import of quotation file %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#, python-format +msgid "This XML file is not XML-compliant" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#, python-format +msgid "" +"This file '%s' is not recognised as XML nor PDF file. Please check the file " +"and it's extension." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#, python-format +msgid "This quotation doesn't have any line !" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#, python-format +msgid "" +"This type of XML quotation is not supported. Did you install the module to " +"support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "Universal Business Language" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,update_option:0 +msgid "Update Option" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "Update RFQ" +msgstr "" + +#. module: purchase_order_import +#: help:purchase.order.import,quote_file:0 +msgid "" +"Upload a quotation file that you received from your supplier. Supported " +"formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"Upload below the quotation that you received from your supplier for this RFQ" +" as XML or PDF file. When you click on the Update RFQ button:" +msgstr "" + +#. module: purchase_order_import +#: field:purchase.order.import,quote_file:0 +msgid "XML or PDF Quotation" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:237 +#, python-format +msgid "You must select a quotation to update." +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"for lines that are present both in the imported quotation and in the RFQ, " +"Odoo will update the unit prices (and also the quantities, depending on the " +"chosen option)," +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"for the lines that are present only in the RFQ, Odoo will put a warning " +"message in the chatter (it won't delete them automatically)." +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "" +"for the lines that are present only in the quotation (shipping costs for " +"example), Odoo will add them to the RFQ," +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "format (UBL), you should install the module" +msgstr "" + +#. module: purchase_order_import +#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +msgid "purchase_order_import_ubl" +msgstr "" From 3d877175f58dad9c580d3acb3bdb0fe65ac28a43 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 15 Feb 2017 15:11:22 +0100 Subject: [PATCH 06/20] Prepare v10 branch Rename __openerp__.py to __manifest__.py and set installable to False --- purchase_order_import/{__openerp__.py => __manifest__.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename purchase_order_import/{__openerp__.py => __manifest__.py} (96%) diff --git a/purchase_order_import/__openerp__.py b/purchase_order_import/__manifest__.py similarity index 96% rename from purchase_order_import/__openerp__.py rename to purchase_order_import/__manifest__.py index 4dfd483fba..ce27a47c16 100644 --- a/purchase_order_import/__openerp__.py +++ b/purchase_order_import/__manifest__.py @@ -15,5 +15,5 @@ 'wizard/purchase_order_import_view.xml', 'views/purchase.xml', ], - 'installable': True, + 'installable': False, } From e90d482df70db55c387be10c453959a6a9f2887d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 27 Feb 2017 23:23:59 +0100 Subject: [PATCH 07/20] Continue port of modules for v10.0, in particular sale_order_import_* module Fix spelling mistake and other remarks on README by Tarteo --- purchase_order_import/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purchase_order_import/README.rst b/purchase_order_import/README.rst index 5d34043ae0..2d9765dbbe 100644 --- a/purchase_order_import/README.rst +++ b/purchase_order_import/README.rst @@ -6,7 +6,7 @@ Purchase Order Import ===================== -This module adds support for the import of electronic quotations. This module provides the base methods to import electronic quotations ; it requires additionnal modules to support specific order formats: +This module adds support for the import of electronic quotations. This module provides the base methods to import electronic quotations ; it requires additional modules to support specific order formats: * module *purchase_order_import_ubl*: adds support for `Universal Business Language (UBL) `_ quotations as: From 9cb9a29188828b445928c759ede51d9f5a12ca99 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 28 Feb 2017 21:51:43 +0100 Subject: [PATCH 08/20] Port purchase_order_import* to v10.0 Add ubl invoice generation option in accounting config page --- purchase_order_import/README.rst | 2 +- purchase_order_import/__manifest__.py | 12 +- purchase_order_import/i18n/es.po | 281 ++++++++++++++++++ purchase_order_import/i18n/fr.po | 93 +++--- .../i18n/purchase_order_import.pot | 245 +++++++++++++++ purchase_order_import/views/purchase.xml | 8 +- .../wizard/purchase_order_import.py | 23 +- .../wizard/purchase_order_import_view.xml | 8 +- 8 files changed, 594 insertions(+), 78 deletions(-) create mode 100644 purchase_order_import/i18n/es.po create mode 100644 purchase_order_import/i18n/purchase_order_import.pot diff --git a/purchase_order_import/README.rst b/purchase_order_import/README.rst index 2d9765dbbe..3ade7d3aab 100644 --- a/purchase_order_import/README.rst +++ b/purchase_order_import/README.rst @@ -38,7 +38,7 @@ Once the quotation file is imported, you should read the messages in the chatter .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/226/8.0 + :target: https://runbot.odoo-community.org/runbot/226/10.0 Bug Tracker =========== diff --git a/purchase_order_import/__manifest__.py b/purchase_order_import/__manifest__.py index ce27a47c16..f2dc00589d 100644 --- a/purchase_order_import/__manifest__.py +++ b/purchase_order_import/__manifest__.py @@ -1,19 +1,23 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Purchase Order Import', - 'version': '8.0.1.0.0', + 'version': '10.0.1.0.0', 'category': 'Purchase Management', 'license': 'AGPL-3', 'summary': 'Update RFQ via the import of quotations from suppliers', 'author': 'Akretion,Odoo Community Association (OCA)', 'website': 'http://www.akretion.com', - 'depends': ['purchase', 'base_business_document_import_stock'], + 'depends': [ + 'purchase', + 'base_business_document_import_stock', + 'onchange_helper' + ], 'data': [ 'wizard/purchase_order_import_view.xml', 'views/purchase.xml', ], - 'installable': False, + 'installable': True, } diff --git a/purchase_order_import/i18n/es.po b/purchase_order_import/i18n/es.po new file mode 100644 index 0000000000..cac2f20dfe --- /dev/null +++ b/purchase_order_import/i18n/es.po @@ -0,0 +1,281 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_import +# +# Translators: +# enjolras , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-12 01:43+0000\n" +"PO-Revision-Date: 2018-03-12 01:43+0000\n" +"Last-Translator: enjolras , 2018\n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 +#, python-format +msgid "%d new order line(s) created: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 +#, python-format +msgid "%d order line(s) are not in the imported quotation: %s" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date +msgid "Created on" +msgstr "Creado el" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename +msgid "Filename" +msgstr "Archivo" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id +msgid "ID" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"If it is a PDF file, Odoo will try to find an XML file in the attachments of " +"the PDF file and then use this XML file." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"If it is an XML file, Odoo will parse it if the module that adds support for " +"this XML format is installed. For the" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action +msgid "Import Quotation" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_form +msgid "Import Quotation File" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Import Quotations Files from Suppliers" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid +msgid "Last Updated by" +msgstr "Última modificación por" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price" +msgstr "" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price and Quantity" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order_import +msgid "Purchase Order Import from Files" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id +msgid "RFQ to Update" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 +#, python-format +msgid "" +"The currency of the imported quotation (%s) is different from the currency " +"of the RFQ (%s)" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#, python-format +msgid "" +"The incoterm has been updated from %s to %s upon import of the quotation " +"file '%s'" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#, python-format +msgid "" +"The quantity has been updated on the RFQ line with product '%s' from %s to " +"%s %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 +#, python-format +msgid "" +"The supplier of the imported quotation (%s) is different from the supplier " +"of the RFQ (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#, python-format +msgid "" +"The unit price has been updated on the RFQ line with product '%s' from %s to " +"%s %s." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Then, Odoo will compare the imported quotation and the current RFQ:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#, python-format +msgid "There are no embedded XML file in this PDF file." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#, python-format +msgid "" +"This RFQ has been updated automatically via the import of quotation file %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#, python-format +msgid "This XML file is not XML-compliant" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#, python-format +msgid "" +"This file '%s' is not recognised as XML nor PDF file. Please check the file " +"and it's extension." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 +#, python-format +msgid "This quotation doesn't have any line !" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#, python-format +msgid "" +"This type of XML quotation is not supported. Did you install the module to " +"support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Universal Business Language" +msgstr "Universal Business Language" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option +msgid "Update Option" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Update RFQ" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_import_quote_file +msgid "" +"Upload a quotation file that you received from your supplier. Supported " +"formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"Upload below the quotation that you received from your supplier for this RFQ " +"as XML or PDF file. When you click on the Update RFQ button:" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file +msgid "XML or PDF Quotation" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 +#, python-format +msgid "You must select a quotation to update." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"for lines that are present both in the imported quotation and in the RFQ, " +"Odoo will update the unit prices (and also the quantities, depending on the " +"chosen option)," +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"for the lines that are present only in the RFQ, Odoo will put a warning " +"message in the chatter (it won't delete them automatically)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"for the lines that are present only in the quotation (shipping costs for " +"example), Odoo will add them to the RFQ," +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"format (UBL), you should install the module purchase_order_import_ubl." +msgstr "" diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po index 3eac1f1a21..272726eac0 100644 --- a/purchase_order_import/i18n/fr.po +++ b/purchase_order_import/i18n/fr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * purchase_order_import -# +# # Translators: # OCA Transbot , 2016 msgid "" @@ -12,66 +12,66 @@ msgstr "" "PO-Revision-Date: 2016-11-12 13:36+0000\n" "Last-Translator: OCA Transbot , 2016\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:210 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:194 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Cancel" msgstr "Annuler" #. module: purchase_order_import -#: field:purchase.order.import,create_uid:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid msgid "Created by" msgstr "Créé par" #. module: purchase_order_import -#: field:purchase.order.import,create_date:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date msgid "Created on" msgstr "Créé le" #. module: purchase_order_import -#: field:purchase.order.import,display_name:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name msgid "Display Name" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,quote_filename:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename msgid "Filename" msgstr "Nom du fichier" #. module: purchase_order_import -#: field:purchase.order.import,id:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id msgid "ID" msgstr "ID" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" -"If it is a PDF file, Odoo will try to find an XML file in the attachments of" -" the PDF file and then use this XML file." +"If it is a PDF file, Odoo will try to find an XML file in the attachments of " +"the PDF file and then use this XML file." msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" -"If it is an XML file, Odoo will parse it if the module that adds support for" -" this XML format is installed. For the" +"If it is an XML file, Odoo will parse it if the module that adds support for " +"this XML format is installed. For the" msgstr "" #. module: purchase_order_import @@ -80,27 +80,27 @@ msgid "Import Quotation" msgstr "" #. module: purchase_order_import -#: view:purchase.order:purchase_order_import.purchase_order_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_form msgid "Import Quotation File" msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,__last_update:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update msgid "Last Modified on" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,write_uid:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid msgid "Last Updated by" msgstr "Dernière mise-à-jour par" #. module: purchase_order_import -#: field:purchase.order.import,write_date:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date msgid "Last Updated on" msgstr "Dernière mise-à-jour le" @@ -120,12 +120,12 @@ msgid "Purchase Order Import from Files" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,purchase_id:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:254 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 #, python-format msgid "" "The currency of the imported quotation (%s) is different from the currency " @@ -149,7 +149,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 #, python-format msgid "" "The supplier of the imported quotation (%s) is different from the supplier " @@ -160,12 +160,12 @@ msgstr "" #: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 #, python-format msgid "" -"The unit price has been updated on the RFQ line with product '%s' from %s to" -" %s %s." +"The unit price has been updated on the RFQ line with product '%s' from %s to " +"%s %s." msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" @@ -176,7 +176,7 @@ msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:269 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 #, python-format msgid "" "This RFQ has been updated automatically via the import of quotation file %s" @@ -197,7 +197,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 #, python-format msgid "This quotation doesn't have any line !" msgstr "" @@ -212,47 +212,47 @@ msgid "" msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,update_option:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option msgid "Update Option" msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Update RFQ" msgstr "" #. module: purchase_order_import -#: help:purchase.order.import,quote_file:0 +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_import_quote_file msgid "" "Upload a quotation file that you received from your supplier. Supported " "formats: XML and PDF (PDF with an embeded XML file)." msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" -"Upload below the quotation that you received from your supplier for this RFQ" -" as XML or PDF file. When you click on the Update RFQ button:" +"Upload below the quotation that you received from your supplier for this RFQ " +"as XML or PDF file. When you click on the Update RFQ button:" msgstr "" #. module: purchase_order_import -#: field:purchase.order.import,quote_file:0 +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:237 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 #, python-format msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for lines that are present both in the imported quotation and in the RFQ, " "Odoo will update the unit prices (and also the quantities, depending on the " @@ -260,25 +260,22 @@ msgid "" msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the RFQ, Odoo will put a warning " "message in the chatter (it won't delete them automatically)." msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the quotation (shipping costs for " "example), Odoo will add them to the RFQ," msgstr "" #. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form -msgid "format (UBL), you should install the module" -msgstr "" - -#. module: purchase_order_import -#: view:purchase.order.import:purchase_order_import.purchase_order_import_form -msgid "purchase_order_import_ubl" +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "" +"format (UBL), you should install the module purchase_order_import_ubl." msgstr "" diff --git a/purchase_order_import/i18n/purchase_order_import.pot b/purchase_order_import/i18n/purchase_order_import.pot new file mode 100644 index 0000000000..8d3b4f4753 --- /dev/null +++ b/purchase_order_import/i18n/purchase_order_import.pot @@ -0,0 +1,245 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_import +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 +#, python-format +msgid "%d new order line(s) created: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 +#, python-format +msgid "%d order line(s) are not in the imported quotation: %s" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Cancel" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date +msgid "Created on" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename +msgid "Filename" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id +msgid "ID" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action +msgid "Import Quotation" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_form +msgid "Import Quotation File" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Import Quotations Files from Suppliers" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price" +msgstr "" + +#. module: purchase_order_import +#: selection:purchase.order.import,update_option:0 +msgid "Price and Quantity" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order_import +msgid "Purchase Order Import from Files" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id +msgid "RFQ to Update" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 +#, python-format +msgid "The currency of the imported quotation (%s) is different from the currency of the RFQ (%s)" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#, python-format +msgid "The incoterm has been updated from %s to %s upon import of the quotation file '%s'" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#, python-format +msgid "The quantity has been updated on the RFQ line with product '%s' from %s to %s %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 +#, python-format +msgid "The supplier of the imported quotation (%s) is different from the supplier of the RFQ (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#, python-format +msgid "The unit price has been updated on the RFQ line with product '%s' from %s to %s %s." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Then, Odoo will compare the imported quotation and the current RFQ:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#, python-format +msgid "There are no embedded XML file in this PDF file." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#, python-format +msgid "This RFQ has been updated automatically via the import of quotation file %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#, python-format +msgid "This XML file is not XML-compliant" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#, python-format +msgid "This file '%s' is not recognised as XML nor PDF file. Please check the file and it's extension." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 +#, python-format +msgid "This quotation doesn't have any line !" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#, python-format +msgid "This type of XML quotation is not supported. Did you install the module to support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Universal Business Language" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option +msgid "Update Option" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Update RFQ" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_import_quote_file +msgid "Upload a quotation file that you received from your supplier. Supported formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file +msgid "XML or PDF Quotation" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 +#, python-format +msgid "You must select a quotation to update." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option)," +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "for the lines that are present only in the RFQ, Odoo will put a warning message in the chatter (it won't delete them automatically)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ," +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form +msgid "format (UBL), you should install the module purchase_order_import_ubl." +msgstr "" + diff --git a/purchase_order_import/views/purchase.xml b/purchase_order_import/views/purchase.xml index 7d765dfc4a..1f0a65e9a5 100644 --- a/purchase_order_import/views/purchase.xml +++ b/purchase_order_import/views/purchase.xml @@ -1,11 +1,10 @@ - - + purchase_order_import.purchase.order.form @@ -20,5 +19,4 @@ - - + diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index de70b9e294..431e976a0c 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api, _ -from openerp.tools import float_is_zero -from openerp.exceptions import Warning as UserError +from odoo import models, fields, api, _ +from odoo.tools import float_is_zero +from odoo.exceptions import UserError import logging import mimetypes from lxml import etree @@ -184,6 +184,7 @@ def update_order_lines(self, parsed_quote, order): oline.product_id.name_get()[0][1], cdict['qty'][0], cdict['qty'][1], oline.product_uom.name)) + write_vals['product_qty'] = cdict['qty'][1] if write_vals: oline.write(write_vals) if compare_res['to_remove']: # we don't delete the lines, only warn @@ -214,17 +215,9 @@ def update_order_lines(self, parsed_quote, order): @api.model def _prepare_create_order_line(self, product, uom, import_line, order): polo = self.env['purchase.order.line'] - product_change_res = polo.onchange_product_id( - order.pricelist_id.id, product.id, - import_line['qty'], uom.id, - order.partner_id.id, - fiscal_position_id=order.fiscal_position.id or False, - price_unit=import_line['price_unit'])['value'] - if product_change_res.get('taxes_id'): - product_change_res['taxes_id'] = [ - (6, 0, product_change_res['taxes_id'])] - vals = product_change_res - vals['product_id'] = product.id + vals = {'product_id': product.id, 'order_id': order} + vals = polo.play_onchanges(vals, ['product_id']) + vals.pop('order_id') return vals @api.multi diff --git a/purchase_order_import/wizard/purchase_order_import_view.xml b/purchase_order_import/wizard/purchase_order_import_view.xml index 2fd72927e3..3a0c3d68c2 100644 --- a/purchase_order_import/wizard/purchase_order_import_view.xml +++ b/purchase_order_import/wizard/purchase_order_import_view.xml @@ -1,11 +1,10 @@ - - + purchase.order.import.form @@ -49,5 +48,4 @@ new - - + From 7f28716f546fbe0d2ad919f167a200b313c7b9c5 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 14 Aug 2018 11:04:43 +0200 Subject: [PATCH 09/20] Fix import of sale order with price_source='pricelist' when pricelist has visible discounts Code improvements in sale_order_import Add unit tests in sale_order_import Use display_name instead of name_get()[0][1] --- purchase_order_import/wizard/purchase_order_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 431e976a0c..6d323562ef 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -173,7 +173,7 @@ def update_order_lines(self, parsed_quote, order): chatter.append(_( "The unit price has been updated on the RFQ line with " "product '%s' from %s to %s %s.") % ( - oline.product_id.name_get()[0][1], + oline.product_id.display_name, cdict['price_unit'][0], cdict['price_unit'][1], order.currency_id.name)) write_vals['price_unit'] = cdict['price_unit'][1] # TODO @@ -181,7 +181,7 @@ def update_order_lines(self, parsed_quote, order): chatter.append(_( "The quantity has been updated on the RFQ line with " "product '%s' from %s to %s %s.") % ( - oline.product_id.name_get()[0][1], + oline.product_id.display_name, cdict['qty'][0], cdict['qty'][1], oline.product_uom.name)) write_vals['product_qty'] = cdict['qty'][1] From 1a325650a3f83c527b6a2fed9f64c2c1bddeafc0 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sun, 26 Aug 2018 00:05:16 +0200 Subject: [PATCH 10/20] Check the VAT number of the destination partner, to make sure the business document is imported in the right company --- .../i18n/purchase_order_import.pot | 30 +++++++++---------- .../wizard/purchase_order_import.py | 16 ++++++++-- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/purchase_order_import/i18n/purchase_order_import.pot b/purchase_order_import/i18n/purchase_order_import.pot index 8d3b4f4753..81d72d8d86 100644 --- a/purchase_order_import/i18n/purchase_order_import.pot +++ b/purchase_order_import/i18n/purchase_order_import.pot @@ -14,13 +14,13 @@ msgstr "" "Plural-Forms: \n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -116,31 +116,31 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 #, python-format msgid "The currency of the imported quotation (%s) is different from the currency of the RFQ (%s)" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:134 #, python-format msgid "The incoterm has been updated from %s to %s upon import of the quotation file '%s'" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 #, python-format msgid "The quantity has been updated on the RFQ line with product '%s' from %s to %s %s." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 #, python-format msgid "The supplier of the imported quotation (%s) is different from the supplier of the RFQ (%s)." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 #, python-format msgid "The unit price has been updated on the RFQ line with product '%s' from %s to %s %s." msgstr "" @@ -151,38 +151,38 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 #, python-format msgid "This RFQ has been updated automatically via the import of quotation file %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "This file '%s' is not recognised as XML nor PDF file. Please check the file and it's extension." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "This quotation doesn't have any line !" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 #, python-format msgid "This type of XML quotation is not supported. Did you install the module to support this XML format?" msgstr "" @@ -218,7 +218,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 6d323562ef..79bce66ca8 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -# © 2016-2017 Akretion (Alexis de Lattre ) +# Copyright 2016-2018 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields, api, _ -from odoo.tools import float_is_zero +from odoo import api, fields, models, _ +from odoo.tools import float_is_zero, config from odoo.exceptions import UserError import logging import mimetypes @@ -69,6 +70,9 @@ def parse_pdf_quote(self, quote_file): # 'name': 'Camptocamp', # 'email': 'luc@camptocamp.com', # }, + # 'company': {'vat': 'FR12123456789'}, # Only used to check we are not + # # importing the quote in the + # # wrong company by mistake # 'currency': {'iso': 'EUR', 'symbol': u'€'}, # 'incoterm': 'EXW', # 'note': 'some notes', @@ -113,6 +117,12 @@ def parse_quote(self, quote_file, quote_filename): quote_file.encode('base64') if 'chatter_msg' not in parsed_quote: parsed_quote['chatter_msg'] = [] + if ( + parsed_quote.get('company') and + not config['test_enable'] and + not self._context.get('edi_skip_company_check')): + self.env['business.document.import']._check_company( + parsed_quote['company'], parsed_quote['chatter_msg']) return parsed_quote @api.model From a041c0b54991554b045945082a197059b2ffc4fe Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sat, 24 Nov 2018 20:33:21 +0000 Subject: [PATCH 11/20] Update translation files Updated by Actualizar ficheiros PO com o novo POT (msgmerge) hook in Weblate. --- purchase_order_import/i18n/es.po | 30 +++++++++--------- purchase_order_import/i18n/fr.po | 30 +++++++++--------- .../static/description/icon.png | Bin 0 -> 9455 bytes 3 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 purchase_order_import/static/description/icon.png diff --git a/purchase_order_import/i18n/es.po b/purchase_order_import/i18n/es.po index cac2f20dfe..e3989eb4d3 100644 --- a/purchase_order_import/i18n/es.po +++ b/purchase_order_import/i18n/es.po @@ -19,13 +19,13 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -125,7 +125,7 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 #, python-format msgid "" "The currency of the imported quotation (%s) is different from the currency " @@ -133,7 +133,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:134 #, python-format msgid "" "The incoterm has been updated from %s to %s upon import of the quotation " @@ -141,7 +141,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 #, python-format msgid "" "The quantity has been updated on the RFQ line with product '%s' from %s to " @@ -149,7 +149,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 #, python-format msgid "" "The supplier of the imported quotation (%s) is different from the supplier " @@ -157,7 +157,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 #, python-format msgid "" "The unit price has been updated on the RFQ line with product '%s' from %s to " @@ -170,26 +170,26 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 #, python-format msgid "" "This RFQ has been updated automatically via the import of quotation file %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "" "This file '%s' is not recognised as XML nor PDF file. Please check the file " @@ -197,14 +197,14 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "This quotation doesn't have any line !" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 #, python-format msgid "" "This type of XML quotation is not supported. Did you install the module to " @@ -246,7 +246,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po index 272726eac0..5b3317e75f 100644 --- a/purchase_order_import/i18n/fr.po +++ b/purchase_order_import/i18n/fr.po @@ -19,13 +19,13 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:211 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:195 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -125,7 +125,7 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:247 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 #, python-format msgid "" "The currency of the imported quotation (%s) is different from the currency " @@ -133,7 +133,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:124 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:134 #, python-format msgid "" "The incoterm has been updated from %s to %s upon import of the quotation " @@ -141,7 +141,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:181 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 #, python-format msgid "" "The quantity has been updated on the RFQ line with product '%s' from %s to " @@ -149,7 +149,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:243 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 #, python-format msgid "" "The supplier of the imported quotation (%s) is different from the supplier " @@ -157,7 +157,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:173 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 #, python-format msgid "" "The unit price has been updated on the RFQ line with product '%s' from %s to " @@ -170,26 +170,26 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:52 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:262 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 #, python-format msgid "" "This RFQ has been updated automatically via the import of quotation file %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:96 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:106 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "" "This file '%s' is not recognised as XML nor PDF file. Please check the file " @@ -197,14 +197,14 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:255 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "This quotation doesn't have any line !" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:41 -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:61 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 #, python-format msgid "" "This type of XML quotation is not supported. Did you install the module to " @@ -246,7 +246,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:230 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/static/description/icon.png b/purchase_order_import/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 91fe0404572fdac077c7d5574e25bfffae1bed99 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Thu, 12 Dec 2019 16:44:53 +0100 Subject: [PATCH 12/20] [FIX] 10.0 bad use of play_onchange the method play_onchange in server-tools/onchange_helper only returns the changed keys in the dictionary it receives as first arguement. The code in account_invoice_import, purchase_order_import and sale_order_import was written as if all the keys were returned. Maybe the behavior of play_onchange was updated in a recent version, but this nevertheless needs fixing in this module. This should fix the unit tests which are red on this branch. --- purchase_order_import/wizard/purchase_order_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 79bce66ca8..177b8b276b 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -226,7 +226,7 @@ def update_order_lines(self, parsed_quote, order): def _prepare_create_order_line(self, product, uom, import_line, order): polo = self.env['purchase.order.line'] vals = {'product_id': product.id, 'order_id': order} - vals = polo.play_onchanges(vals, ['product_id']) + vals.update(polo.play_onchanges(vals, ['product_id'])) vals.pop('order_id') return vals From 3d0d777ea1d8f650719ea46ba6a5865567d21dec Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Thu, 12 Dec 2019 16:45:18 +0100 Subject: [PATCH 13/20] [FIX] remove method defined twice --- purchase_order_import/wizard/purchase_order_import.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 177b8b276b..676cee1187 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -139,17 +139,6 @@ def _prepare_update_order_vals(self, parsed_quote, order): vals['incoterm_id'] = incoterm.id return vals - @api.model - def _prepare_create_order_line( - self, product, qty, uom, price_unit, so_vals): - vals = { - 'product_id': product.id, - 'product_qty': qty, - 'product_uom': uom.id, - 'price_unit': price_unit, # TODO fix - } - return vals - @api.multi def update_order_lines(self, parsed_quote, order): polo = self.env['purchase.order.line'] From dc49325e525a39f5ec39867e424d71350ecf6531 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Thu, 12 Dec 2019 17:13:24 +0100 Subject: [PATCH 14/20] [FIX] purchase_order_import: missing price_unit on line --- purchase_order_import/i18n/es.po | 18 +++++++++--------- purchase_order_import/i18n/fr.po | 18 +++++++++--------- .../i18n/purchase_order_import.pot | 18 +++++++++--------- .../wizard/purchase_order_import.py | 6 +++++- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/purchase_order_import/i18n/es.po b/purchase_order_import/i18n/es.po index e3989eb4d3..3a4389ebb2 100644 --- a/purchase_order_import/i18n/es.po +++ b/purchase_order_import/i18n/es.po @@ -19,13 +19,13 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:210 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:194 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -125,7 +125,7 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format msgid "" "The currency of the imported quotation (%s) is different from the currency " @@ -141,7 +141,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format msgid "" "The quantity has been updated on the RFQ line with product '%s' from %s to " @@ -149,7 +149,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format msgid "" "The supplier of the imported quotation (%s) is different from the supplier " @@ -157,7 +157,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:172 #, python-format msgid "" "The unit price has been updated on the RFQ line with product '%s' from %s to " @@ -176,7 +176,7 @@ msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "" "This RFQ has been updated automatically via the import of quotation file %s" @@ -197,7 +197,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" @@ -246,7 +246,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:233 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po index 5b3317e75f..5e79a5c45a 100644 --- a/purchase_order_import/i18n/fr.po +++ b/purchase_order_import/i18n/fr.po @@ -19,13 +19,13 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:210 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:194 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -125,7 +125,7 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format msgid "" "The currency of the imported quotation (%s) is different from the currency " @@ -141,7 +141,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format msgid "" "The quantity has been updated on the RFQ line with product '%s' from %s to " @@ -149,7 +149,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format msgid "" "The supplier of the imported quotation (%s) is different from the supplier " @@ -157,7 +157,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:172 #, python-format msgid "" "The unit price has been updated on the RFQ line with product '%s' from %s to " @@ -176,7 +176,7 @@ msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "" "This RFQ has been updated automatically via the import of quotation file %s" @@ -197,7 +197,7 @@ msgid "" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" @@ -246,7 +246,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:233 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/i18n/purchase_order_import.pot b/purchase_order_import/i18n/purchase_order_import.pot index 81d72d8d86..5fb2b0b630 100644 --- a/purchase_order_import/i18n/purchase_order_import.pot +++ b/purchase_order_import/i18n/purchase_order_import.pot @@ -14,13 +14,13 @@ msgstr "" "Plural-Forms: \n" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:221 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:210 #, python-format msgid "%d new order line(s) created: %s" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:205 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:194 #, python-format msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" @@ -116,7 +116,7 @@ msgid "RFQ to Update" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:257 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format msgid "The currency of the imported quotation (%s) is different from the currency of the RFQ (%s)" msgstr "" @@ -128,19 +128,19 @@ msgid "The incoterm has been updated from %s to %s upon import of the quotation msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:191 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format msgid "The quantity has been updated on the RFQ line with product '%s' from %s to %s %s." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:253 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format msgid "The supplier of the imported quotation (%s) is different from the supplier of the RFQ (%s)." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:183 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:172 #, python-format msgid "The unit price has been updated on the RFQ line with product '%s' from %s to %s %s." msgstr "" @@ -157,7 +157,7 @@ msgid "There are no embedded XML file in this PDF file." msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:272 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 #, python-format msgid "This RFQ has been updated automatically via the import of quotation file %s" msgstr "" @@ -175,7 +175,7 @@ msgid "This file '%s' is not recognised as XML nor PDF file. Please check the fi msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:265 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" @@ -218,7 +218,7 @@ msgid "XML or PDF Quotation" msgstr "" #. module: purchase_order_import -#: code:addons/purchase_order_import/wizard/purchase_order_import.py:240 +#: code:addons/purchase_order_import/wizard/purchase_order_import.py:233 #, python-format msgid "You must select a quotation to update." msgstr "" diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 676cee1187..6d3887980c 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -214,7 +214,11 @@ def update_order_lines(self, parsed_quote, order): @api.model def _prepare_create_order_line(self, product, uom, import_line, order): polo = self.env['purchase.order.line'] - vals = {'product_id': product.id, 'order_id': order} + vals = { + 'product_id': product.id, + 'order_id': order, + 'price_unit': import_line['price_unit'], + } vals.update(polo.play_onchanges(vals, ['product_id'])) vals.pop('order_id') return vals From 9018c09816bba4b44abd40b22317522797911cc6 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 12 Feb 2020 11:30:12 +0100 Subject: [PATCH 15/20] [ADD] Add support for UBL OrderResponse The ORderResponce document allows a supplier to communicate the ack and the confirmation of an Order documnet. Depending of its content, the PO will be cancelled or confirmed. If the OrderResponse document contains some amended line, the import process will update the picking to reflect the changes --- purchase_order_import/__init__.py | 1 + purchase_order_import/__manifest__.py | 1 + purchase_order_import/models/__init__.py | 1 + .../models/purchase_order.py | 18 + purchase_order_import/tests/__init__.py | 1 + .../tests/test_order_response_import.py | 594 ++++++++++++++++++ purchase_order_import/views/purchase.xml | 3 + purchase_order_import/wizard/__init__.py | 1 + .../wizard/order_response_import.py | 413 ++++++++++++ .../wizard/order_response_import_view.xml | 62 ++ 10 files changed, 1095 insertions(+) create mode 100644 purchase_order_import/models/__init__.py create mode 100644 purchase_order_import/models/purchase_order.py create mode 100644 purchase_order_import/tests/__init__.py create mode 100644 purchase_order_import/tests/test_order_response_import.py create mode 100644 purchase_order_import/wizard/order_response_import.py create mode 100644 purchase_order_import/wizard/order_response_import_view.xml diff --git a/purchase_order_import/__init__.py b/purchase_order_import/__init__.py index 3b4c3edf09..3c4e748f01 100644 --- a/purchase_order_import/__init__.py +++ b/purchase_order_import/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import wizard +from . import models diff --git a/purchase_order_import/__manifest__.py b/purchase_order_import/__manifest__.py index f2dc00589d..1d66441b06 100644 --- a/purchase_order_import/__manifest__.py +++ b/purchase_order_import/__manifest__.py @@ -16,6 +16,7 @@ 'onchange_helper' ], 'data': [ + 'wizard/order_response_import_view.xml', 'wizard/purchase_order_import_view.xml', 'views/purchase.xml', ], diff --git a/purchase_order_import/models/__init__.py b/purchase_order_import/models/__init__.py new file mode 100644 index 0000000000..9f03530643 --- /dev/null +++ b/purchase_order_import/models/__init__.py @@ -0,0 +1 @@ +from . import purchase_order diff --git a/purchase_order_import/models/purchase_order.py b/purchase_order_import/models/purchase_order.py new file mode 100644 index 0000000000..7bf5a2acd6 --- /dev/null +++ b/purchase_order_import/models/purchase_order.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PurchaseOrder(models.Model): + + _inherit = "purchase.order" + + supplier_ack_dt = fields.Datetime( + "Supplier Acknowledgement Date", + help="Date and time of the acknowledgement by the supplier. " + "This field is filled by Odoo when processing a " + "OrderResponse document.", + index=True, + ) diff --git a/purchase_order_import/tests/__init__.py b/purchase_order_import/tests/__init__.py new file mode 100644 index 0000000000..315c6f8bdb --- /dev/null +++ b/purchase_order_import/tests/__init__.py @@ -0,0 +1 @@ +from . import test_order_response_import diff --git a/purchase_order_import/tests/test_order_response_import.py b/purchase_order_import/tests/test_order_response_import.py new file mode 100644 index 0000000000..536be02032 --- /dev/null +++ b/purchase_order_import/tests/test_order_response_import.py @@ -0,0 +1,594 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, _ +from odoo.exceptions import UserError +from odoo.tests import SavepointCase + +from ..wizard.order_response_import import ( + ORDER_RESPONSE_STATUS_ACK, + ORDER_RESPONSE_STATUS_ACCEPTED, + ORDER_RESPONSE_STATUS_REJECTED, + ORDER_RESPONSE_STATUS_CONDITIONAL, + LINE_STATUS_ACCEPTED, + LINE_STATUS_REJECTED, + LINE_STATUS_AMEND, +) + + +class TestOrderResponseImportCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestOrderResponseImportCommon, cls).setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.supplier = cls.env.ref("base.res_partner_12") + cls.supplier.vat = "BE0477472701" + cls.env.user.company_id.partner_id.vat = "BE0421801233" + cls.currency_euro = cls.env.ref("base.EUR") + cls.currency_usd = cls.env.ref("base.USD") + cls.product_1 = cls.env["product.product"].create( + { + "name": "Product 1", + "seller_ids": [ + (0, 0, {"name": cls.supplier.id, "product_code": "P1"}) + ], + } + ) + cls.product_2 = cls.env["product.product"].create( + { + "name": "Product 2", + "seller_ids": [ + (0, 0, {"name": cls.supplier.id, "product_code": "P2"}) + ], + } + ) + cls.purchase_order = cls.env["purchase.order"].create( + { + "partner_id": cls.supplier.id, + "date_order": fields.Datetime.now(), + "date_planned": fields.Datetime.now(), + "currency_id": cls.currency_euro.id, + } + ) + cls.line1 = cls.purchase_order.order_line.create( + { + "order_id": cls.purchase_order.id, + "product_id": cls.product_1.id, + "name": cls.product_2.name, + "date_planned": fields.Datetime.now(), + "product_qty": 10, + "product_uom": cls.env.ref("product.product_uom_unit").id, + "price_unit": 15, + } + ) + cls.line2 = cls.purchase_order.order_line.create( + { + "order_id": cls.purchase_order.id, + "product_id": cls.product_2.id, + "name": cls.product_2.name, + "date_planned": fields.Datetime.now(), + "product_qty": 5, + "product_uom": cls.env.ref("product.product_uom_unit").id, + "price_unit": 25, + } + ) + cls.OrderResponseImport = cls.env["order.response.import"] + + def order_line_to_data( + self, + order_line, + qty=None, + status=LINE_STATUS_ACCEPTED, + backorder_qty=None, + note=None, + ): + return { + "status": status, + "backorder_qty": backorder_qty, + "qty": qty if qty is not None else order_line.product_qty, + "note": note, + "line_id": str(order_line.id), + "uom": {"unece_code": order_line.product_uom.unece_code}, + } + + +class TestOrderResponseImport(TestOrderResponseImportCommon): + def _get_base_data(self): + return { + "status": ORDER_RESPONSE_STATUS_ACK, + "company": {"vat": "BE0421801233"}, + "currency": {"iso": "EUR"}, + "date": "2020-02-04", + "chatter_msg": [], + "lines": [], + "note": "Note1\nNote2", + "time": "22:10:30", + "supplier": {"vat": "BE0477472701"}, + "ref": str(self.purchase_order.name), + } + + def test_01(self): + """ + Data: + Data with unknown PO reference + Test Case: + Process data + Expected result: + UserError is raised + """ + data = self._get_base_data() + data["ref"] = "123456" + with self.assertRaises(UserError) as ue: + self.OrderResponseImport.process_data(data) + self.assertEqual( + ue.exception.name, _("No purchase order found for name 123456.") + ) + + def test_02(self): + """ + Data: + Data with unknown PO status + Test Case: + Process data + Expected result: + UserError is raised + """ + data = self._get_base_data() + data["status"] = "unknown" + with self.assertRaises(UserError) as ue: + self.OrderResponseImport.process_data(data) + self.assertEqual(ue.exception.name, _("Unknown status 'unknown'.")) + + def test_03(self): + """ + Data: + Data with an other currency + Test Case: + Process data + Expected result: + UserError is raised + """ + data = self._get_base_data() + data["currency"] = {"iso": self.currency_usd.name} + with self.assertRaises(UserError) as ue: + self.OrderResponseImport.process_data(data) + self.assertEqual( + ue.exception.name, + _( + "The currency of the imported OrderResponse (USD) is " + "different from the currency of the purchase order (EUR)." + ), + ) + + def test_04(self): + """ + Data: + Data with status ack. + Test Case: + Process data + Expected result: + The ack info is filled + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_ACK + self.assertFalse(self.purchase_order.supplier_ack_dt) + self.OrderResponseImport.process_data(data) + self.assertTrue(self.purchase_order.supplier_ack_dt) + + def test_05(self): + """ + Data: + Data with status accepted + PO not yet confirmed + Test Case: + Process data + Expected result: + PO is confirmed + A picking is created + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_ACCEPTED + self.assertFalse(self.purchase_order.picking_ids) + self.assertEqual(self.purchase_order.state, "draft") + self.OrderResponseImport.process_data(data) + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.purchase_order.state, "purchase") + + def test_06(self): + """ + Data: + Data with status rejected + PO not yet confirmed + Test Case: + Process data + Expected result: + PO is cancelled + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_REJECTED + self.assertEqual(self.purchase_order.state, "draft") + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "cancel") + + def test_07(self): + """ + Data: + Data with status 'conditionally_accepted' and without lines + Test Case: + Process data + Expected result: + UserError is raised since a all line details must be provided with + this status + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + data["lines"] = [] + with self.assertRaises(UserError) as ue: + self.OrderResponseImport.process_data(data) + expected = ( + _( + "Unable to conditionally confirm the purchase order. \n" + "Line IDS into the parsed document differs from the " + "expected list of order line ids: \n " + "received: []\n" + "expected: %s\n" + ) + % self.purchase_order.order_line.ids + ) + self.assertEqual(ue.exception.name, expected) + + def test_08(self): + """ + Data: + Data with status 'conditionally_accepted' and with a wrong line id + Test Case: + Process data + Expected result: + UserError is raised since a all line details must be provided with + this status + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + data["lines"] = [self.order_line_to_data(self.line1)] + line2 = self.order_line_to_data(self.line2) + line2["line_id"] = "WRONG" + data["lines"].append(line2) + with self.assertRaises(UserError) as ue: + self.OrderResponseImport.process_data(data) + expected = _( + "Unable to conditionally confirm the purchase order. \n" + "Line IDS into the parsed document differs from the " + "expected list of order line ids: \n " + "received: [%s]\n" + "expected: %s\n" + ) % ( + [str(self.line1.id), "WRONG"], + self.purchase_order.order_line.ids, + ) + self.assertEqual(ue.exception.name, expected) + + def test_09(self): + """ + Data: + Data with status 'conditionally_accepted' and all line accepted + Test Case: + Process data + Expected result: + PO is confirmed + A picking is created with one move by po line in state assigned + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + data["lines"] = [ + self.order_line_to_data(self.line1), + self.order_line_to_data(self.line2), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.line1.move_ids.state, "assigned") + self.assertEqual(self.line2.move_ids.state, "assigned") + + def test_10(self): + """ + Data: + Data with status 'conditionally_accepted' and one line accepted + and another one rejected + Test Case: + Process data + Expected result: + PO is confirmed + A picking is created with one move by po line + The move linked to the accepted line is in state assigned + The move linked to the rejected line is in state cancel + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + data["lines"] = [ + self.order_line_to_data(self.line1), + self.order_line_to_data( + self.line2, + status=LINE_STATUS_REJECTED, + note="cancel by import", + ), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.line1.move_ids.state, "assigned") + self.assertEqual(self.line2.move_ids.state, "cancel") + self.assertEqual(self.line2.move_ids.note, "cancel by import") + + def test_11(self): + """ + Data: + Data with status 'conditionally_accepted' and one line accepted + and another one rejected + Test Case: + Process data + Expected result: + PO is confirmed + A picking is created with one move by po line + The move linked to the accepted line is in state assigned + The move linked to the rejected line is in state cancel + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + data["lines"] = [ + self.order_line_to_data(self.line1), + self.order_line_to_data( + self.line2, + status=LINE_STATUS_REJECTED, + note="cancel by import", + ), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.line1.move_ids.state, "assigned") + self.assertEqual(self.line2.move_ids.state, "cancel") + self.assertEqual(self.line2.move_ids.note, "cancel by import") + + def test_12(self): + """ + Data: + Data with status 'conditionally_accepted' + * line1 amended with less qty than ordered and without + backorder qty + * line2 accepted + Test Case: + Process data + Expected result: + PO is confirmed + A picking is created with two moves for the amended line + * line1 move 1 assigned with qty = confirmed qty + * line1 move 2 cancel with qty = expected qty -confirmed qty + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + confirmed_qty = self.line1.product_qty - 3 + data["lines"] = [ + self.order_line_to_data( + self.line1, status=LINE_STATUS_AMEND, qty=confirmed_qty + ), + self.order_line_to_data(self.line2), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 2) + self.assertEqual( + sum(move_ids.mapped("product_qty")), self.line1.product_qty + ) + assigned = move_ids.filtered(lambda s: s.state == "assigned") + self.assertEqual(assigned.product_qty, confirmed_qty) + cancel = move_ids.filtered(lambda s: s.state == "cancel") + self.assertEqual(cancel.product_qty, 3) + self.assertEqual(cancel.note, "No backorder planned by the supplier.") + + def test_13(self): + """ + Data: + Data with status 'conditionally_accepted' + * line1 amended with less qty than ordered and with + backorder qty equal to remaining qty + * line2 accepted + Test Case: + Process data + Expected result: + PO is confirmed + One picking is created with two moves + * line1 assigned with qty = confirmed qty + * line2 assigned with qty = confirmed qty + One backorder picking is created with one move + * line1 assigned with qty = expected qty - confirmed qty + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + confirmed_qty = self.line1.product_qty - 3 + data["lines"] = [ + self.order_line_to_data( + self.line1, + status=LINE_STATUS_AMEND, + qty=confirmed_qty, + backorder_qty=3, + note="my note", + ), + self.order_line_to_data(self.line2), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 2) + self.assertEqual( + sum(move_ids.mapped("product_qty")), self.line1.product_qty + ) + move_confirmed = move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertEqual( + _("my note\n%s items should be delivered into a next delivery.") + % "3", + move_confirmed.note, + ) + move_backorder = move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == 3 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + ) + + def test_14(self): + """ + Data: + Data with status 'conditionally_accepted' + * line1 amended with less qty than ordered and with + backorder qty equal to remaining qty + * line2 amended with less qty than ordered and with + backorder qty equal to remaining qty + Test Case: + Process data + Expected result: + PO is confirmed + One picking is created with two moves + * line1 assigned with qty = confirmed qty + * line2 assigned with qty = confirmed qty + One backorder picking is created with two moves + * line1 assigned with qty = expected qty - confirmed qty + * line2 assigned with qty = expected qty - confirmed qty + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + line1_confirmed_qty = self.line1.product_qty - 3 + line2_confirmed_qty = self.line2.product_qty - 3 + data["lines"] = [ + self.order_line_to_data( + self.line1, + status=LINE_STATUS_AMEND, + qty=line1_confirmed_qty, + backorder_qty=3, + note="my note", + ), + self.order_line_to_data( + self.line2, + status=LINE_STATUS_AMEND, + qty=line2_confirmed_qty, + backorder_qty=3, + note="my note", + ), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + # line1 + line1_move_ids = self.line1.move_ids + self.assertEqual(len(line1_move_ids), 2) + self.assertEqual( + sum(line1_move_ids.mapped("product_qty")), self.line1.product_qty + ) + move_confirmed = line1_move_ids.filtered( + lambda s: s.state == "assigned" + and s.product_qty == line1_confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertEqual( + _("my note\n%s items should be delivered into a next delivery.") + % "3", + move_confirmed.note, + ) + move_backorder = line1_move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == 3 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + ) + # lin1 + line2_move_ids = self.line2.move_ids + self.assertEqual(len(line2_move_ids), 2) + self.assertEqual( + sum(line2_move_ids.mapped("product_qty")), self.line2.product_qty + ) + move_confirmed = line2_move_ids.filtered( + lambda s: s.state == "assigned" + and s.product_qty == line2_confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertEqual( + _("my note\n%s items should be delivered into a next delivery.") + % "3", + move_confirmed.note, + ) + move_backorder = line2_move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == 3 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + ) + + def test_15(self): + """ + Data: + Data with status 'conditionally_accepted' + * line1 amended with less qty than ordered and with + backorder qty less than the remaining qty + * line2 accepted + Test Case: + Process data + Expected result: + PO is confirmed + One picking is created with three moves + * line1 assigned with qty = confirmed qty + * line1 cancel with qty = qty that will not be delivered + * line2 assigned with qty = confirmed qty + One backorder picking is created with one move + * line1 assigned with qty = planned backorder qty + """ + data = self._get_base_data() + data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL + confirmed_qty = self.line1.product_qty - 3 + data["lines"] = [ + self.order_line_to_data( + self.line1, + status=LINE_STATUS_AMEND, + qty=confirmed_qty, + backorder_qty=2, + ), + self.order_line_to_data(self.line2), + ] + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 3) + self.assertEqual( + sum(move_ids.mapped("product_qty")), self.line1.product_qty + ) + move_confirmed = move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertEqual( + _("%s items should be delivered into a next delivery.") % "2", + move_confirmed.note, + ) + move_cancel = move_ids.filtered( + lambda s: s.state == "cancel" and s.product_qty == 1 + ) + self.assertTrue(move_cancel) + self.assertEqual( + _("No backorder planned by the supplier."), move_cancel.note, + ) + move_backorder = move_ids.filtered( + lambda s: s.state == "assigned" and s.product_qty == 2 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + ) diff --git a/purchase_order_import/views/purchase.xml b/purchase_order_import/views/purchase.xml index 1f0a65e9a5..07159ecbed 100644 --- a/purchase_order_import/views/purchase.xml +++ b/purchase_order_import/views/purchase.xml @@ -16,6 +16,9 @@ name="%(purchase_order_import.purchase_order_import_action)d" states="draft,sent,bid" groups="purchase.group_purchase_user"/> + + + diff --git a/purchase_order_import/wizard/__init__.py b/purchase_order_import/wizard/__init__.py index 627d4faa91..5134c02956 100644 --- a/purchase_order_import/wizard/__init__.py +++ b/purchase_order_import/wizard/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import purchase_order_import +from . import order_response_import diff --git a/purchase_order_import/wizard/order_response_import.py b/purchase_order_import/wizard/order_response_import.py new file mode 100644 index 0000000000..1da878020d --- /dev/null +++ b/purchase_order_import/wizard/order_response_import.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ +from odoo.tools import config, float_compare +from odoo.exceptions import UserError, ValidationError +import logging +import mimetypes +from lxml import etree + +logger = logging.getLogger(__name__) + +ORDER_RESPONSE_STATUS_ACK = "acknowledgement" +ORDER_RESPONSE_STATUS_ACCEPTED = "accepted" +ORDER_RESPONSE_STATUS_REJECTED = "rejected" +ORDER_RESPONSE_STATUS_CONDITIONAL = "conditionally_accepted" + +LINE_STATUS_ACCEPTED = "accepted" +LINE_STATUS_REJECTED = "rejected" +LINE_STATUS_AMEND = "amend" + + +def is_int(val): + try: + int(val) + return True + except ValueError: + return False + + +class OrderResponseImport(models.TransientModel): + _name = "order.response.import" + _description = "Purchase Order Response Import from Files" + + @api.model + def _get_purchase_id(self): + assert ( + self._context["active_model"] == "purchase.order" + ), "bad active_model" + return self.env["purchase.order"].browse(self._context["active_id"]) + + document = fields.Binary( + string="XML or PDF Order response", + required=True, + help="Upload an Order response file that you received from " + "your supplier. Supported formats: XML and PDF " + "(PDF with an embeded XML file).", + ) + filename = fields.Char(string="Filename") + + @api.model + def parse_xml_order_document(self, xml_root): + raise UserError( + _( + "This type of XML Order Response is not supported. Did you " + "install the module to support this XML format?" + ) + ) + + @api.model + def parse_pdf_order_document(self, document): + """ + Get PDF attachments, filter on XML files and call import_order_xml + """ + xml_files_dict = self.get_xml_files_from_pdf(document) + if not xml_files_dict: + raise UserError( + _("There are no embedded XML file in this PDF file.") + ) + for xml_filename, xml_root in xml_files_dict.iteritems(): + logger.info("Trying to parse XML file %s", xml_filename) + try: + parsed_order_document = self.parse_xml_order_document(xml_root) + return parsed_order_document + except: + continue + raise UserError( + _( + "This type of XML Order Document is not supported. Did you " + "install the module to support this XML format?" + ) + ) + + # Format of parsed order response + # { + # 'ref': 'SO01234' # the buyer party identifier + # # (specified into the Order document -> po's name) + # 'supplier': {'vat': 'FR25499247138'}, + # 'company': {'vat': 'FR12123456789'}, # Only used to check we are not + # # importing the quote in the + # # wrong company by mistake + # 'status': 'acknowledgement | accepted | rejected | + # conditionally_accepted' + # 'currency': {'iso': 'EUR', 'symbol': u'€'}, + # 'note': 'some notes', + # 'chatter_msg': ['msg1', 'msg2'] + # 'lines': [{ + # 'id': 123456, + # 'qty': 2.5, + # 'uom': {'unece_code': 'C62'}, + # 'status': 5, + # 'note': 'my note' + # 'backorder_qty: None # if provided and qty != expected + # # the backorder qty will be delivered + # # in a next shipping + # }] + + @api.model + def parse_order_response(self, document, filename): + if not document: + raise UserError(_("Missing document file")) + if not filename: + raise UserError(_("Missing document filename")) + filetype = mimetypes.guess_type(filename)[0] + logger.debug("OrderResponse file mimetype: %s", filetype) + if filetype in ["application/xml", "text/xml"]: + try: + xml_root = etree.fromstring(document) + except: + raise UserError(_("This XML file is not XML-compliant")) + if logger.isEnabledFor(logging.DEBUG): + pretty_xml_string = etree.tostring( + xml_root, + pretty_print=True, + encoding="UTF-8", + xml_declaration=True, + ) + logger.debug("Starting to import the following XML file:") + logger.debug(pretty_xml_string) + parsed_order_document = self.parse_xml_order_document(xml_root) + elif filetype == "application/pdf": + parsed_order_document = self.parse_pdf_order_document(document) + else: + raise UserError( + _( + "This file '%s' is not recognised as XML nor PDF file. " + "Please check the file and it's extension." + ) + % filename + ) + logger.debug( + "Result of OrderResponse parsing: ", parsed_order_document + ) + if "attachments" not in parsed_order_document: + parsed_order_document["attachments"] = {} + parsed_order_document["attachments"][filename] = document.encode( + "base64" + ) + if "chatter_msg" not in parsed_order_document: + parsed_order_document["chatter_msg"] = [] + if ( + parsed_order_document.get("company") + and not config["test_enable"] + and not self._context.get("edi_skip_company_check") + ): + self.env["business.document.import"]._check_company( + parsed_order_document["company"], + parsed_order_document["chatter_msg"], + ) + return parsed_order_document + + @api.multi + def process_document(self): + self.ensure_one() + parsed_order_document = self.parse_order_response( + self.document.decode("base64"), self.filename + ) + self.process_data(parsed_order_document) + + @api.model + def process_data(self, parsed_order_document): + bdio = self.env["business.document.import"] + po_name = parsed_order_document.get("ref") + order = self.env["purchase.order"].search([("name", "=", po_name)]) + if not order: + self.env["business.document.import"].user_error_wrap( + _("No purchase order found for name %s.") % po_name + ) + + currency = bdio._match_currency( + parsed_order_document.get("currency"), + parsed_order_document["chatter_msg"], + ) + partner = bdio._match_partner( + parsed_order_document["supplier"], + parsed_order_document["chatter_msg"], + partner_type="supplier", + ) + if ( + partner.commercial_partner_id + != order.partner_id.commercial_partner_id + ): + bdio.user_error_wrap( + _( + "The supplier of the imported OrderResponse (%s) " + "is different from the supplier of the purchase order " + "(%s)." + % ( + partner.commercial_partner_id.name, + order.partner_id.commercial_partner_id.name, + ) + ) + ) + if currency and currency != order.currency_id: + bdio.user_error_wrap( + _( + "The currency of the imported OrderResponse (%s) " + "is different from the currency of the purchase order " + "(%s)." + ) + % (currency.name, order.currency_id.name) + ) + + status = parsed_order_document.get("status") + if status == ORDER_RESPONSE_STATUS_ACK: + self._process_ack(order, parsed_order_document) + elif status == ORDER_RESPONSE_STATUS_REJECTED: + self._process_rejected(order, parsed_order_document) + elif status == ORDER_RESPONSE_STATUS_ACCEPTED: + self._process_accepted(order, parsed_order_document) + elif status == ORDER_RESPONSE_STATUS_CONDITIONAL: + self._process_conditional(order, parsed_order_document) + else: + bdio.user_error_wrap(_("Unknown status '%s'.") % status) + + bdio.post_create_or_update(parsed_order_document, order) + logger.info( + "purchase.order ID %d updated via import of file %s.", + order.id, + self.filename, + ) + order.message_post( + _( + "This purchase order has been updated automatically" + " via the import of OrderResponse file %s." + ) + % self.filename + ) + return order.get_formview_action() + + @api.model + def _process_ack(self, purchase_order, parsed_order_document): + if not purchase_order.supplier_ack_dt: + purchase_order.supplier_ack_dt = fields.Datetime.now() + + @api.model + def _process_rejected(self, purchase_order, parsed_order_document): + parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + parsed_order_document["chatter_msg"].append( + _("PO cancelled by the supplier.") + ) + purchase_order.button_cancel() + + @api.model + def _process_accepted(self, purchase_order, parsed_order_document): + parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + parsed_order_document["chatter_msg"].append( + _("PO confirmed by the supplier.") + ) + purchase_order.button_approve() + + @api.model + def _process_conditional(self, purchase_order, parsed_order_document): + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + chatter = parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + chatter.append(_("PO confirmed with amendment by the supplier.")) + lines = parsed_order_document["lines"] + line_ids = [int(l["line_id"]) for l in lines if is_int(l["line_id"])] + if set(line_ids) != set(purchase_order.order_line.ids): + self.env["business.document.import"].user_error_wrap( + _( + "Unable to conditionally confirm the purchase order. \n" + "Line IDS into the parsed document differs from the " + "expected list of order line ids: \n " + "received: %s\n" + "expected: %s\n" + ) + % ( + [l["line_id"] for l in lines], + purchase_order.order_line.ids, + ) + ) + return + purchase_order.button_approve() + # apply changes to the created moves... + lines_by_id = {int(l["line_id"]): l for l in lines} + for order_line in purchase_order.order_line: + line_info = lines_by_id[order_line.id] + note = line_info.get("note") + move = order_line.move_ids.filtered( + lambda x: x.state not in ("cancel", "done") + ) + if len(move) != 1: + self.env["business.document.import"].user_error_wrap( + _( + "More than one move found for PO line.\n" + "Move IDs: %s\n" + "Line Info: %s" + ) + % (move.ids, line_info) + ) + if note: + move.write({"note": note}) + status = line_info["status"] + if status == LINE_STATUS_ACCEPTED: + continue + if status == LINE_STATUS_REJECTED: + order_line.move_ids.action_cancel() + elif status == LINE_STATUS_AMEND: + qty = line_info["qty"] + backorder_qty = line_info["backorder_qty"] + move_qty = move.product_qty + if ( + float_compare(qty, move_qty, precision_digits=precision) + < 0 + ): + self._check_picking_status(move.picking_id) + new_move_id = move.split(move_qty - qty) + new_move = move.browse(new_move_id) + to_cancel = None + if backorder_qty: + note = note + "\n" if note else "" + note += ( + _( + "%s items should be delivered into a next delivery." + ) + % backorder_qty + ) + move.note = note + # if the backorder qty is < than the remaining qty + # split and cancel the qty that will not be delivered + if ( + float_compare( + backorder_qty, + new_move.product_qty, + precision_digits=precision, + ) + < 0 + ): + to_cancel_id = new_move.split( + new_move.product_qty - backorder_qty + ) + to_cancel = move.browse(to_cancel_id) + else: + to_cancel = new_move + if to_cancel: + to_cancel.action_cancel() + to_cancel.write( + { + "note": _( + "No backorder planned by the supplier." + ) + } + ) + if new_move.state != "cancel": + # move the new move into an backorder picking to avoid + # that the scheduler merge the two moves into the same + # pack operation + self._add_move_to_backorder(new_move) + + # Reset Operations + move.picking_id.do_prepare_partial() + + @api.model + def _add_move_to_backorder(self, move): + """ + Add the move the picking's backorder + return the backorder associated to the current picking. If no backorder + exists, create a new one. + :param move: + """ + StockPicking = self.env["stock.picking"] + current_picking = move.picking_id + backorder = StockPicking.search( + [("backorder_id", "=", current_picking.id)] + ) + if not backorder: + date_done = current_picking.date_done + move.picking_id._create_backorder(backorder_moves=move) + # preserve date_done.... + current_picking.date_done = date_done + else: + move.write({"picking_id": backorder.id}) + backorder.action_confirm() + backorder.action_assign() + + @api.model + def _check_picking_status(self, picking): + """ + The picking operations have already begun + :param picking: + :return: + """ + if any( + operation.qty_done != 0 for operation in picking.pack_operation_ids + ): + raise ValidationError( + _( + "Some Pack Operations have already started! " + "Please validate or reset operations on " + "picking %s to ensure delivery slip to be computed." + ) + % picking.name + ) diff --git a/purchase_order_import/wizard/order_response_import_view.xml b/purchase_order_import/wizard/order_response_import_view.xml new file mode 100644 index 0000000000..bc60c7bbaf --- /dev/null +++ b/purchase_order_import/wizard/order_response_import_view.xml @@ -0,0 +1,62 @@ + + + + + + + order.response.import.form + order.response.import + +
+ +
+

Upload below the OrderResponse you received from your supplier. When you click on the import button:

+
    +
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module order_response_import_ubl.
  2. +
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. +
+

Then, Odoo will process the related purchase order as follow:

+
    +
  • If the status code is acknowledgement: update the acknowledge datetime on the PO
  • +
  • If the status code is Rejected: cancel the PO
  • +
  • If the status code is Accepted: confirm the PO and create the picking
  • +
  • If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document
  • +
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • +
+

Possible line amendments:

+
    +
  • The order line is refused: The picking operation is cancelled
  • +
  • The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled.
  • +
+
+
+ + + + +
+
+
+
+
+ + + Import Quotation + order.response.import + form + new + + + + UBL OrderResponse Importer + + + + + + +
From 4cd95bf479ccc8f4d4709116e9c98fa1817cfd1b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 9 Mar 2020 13:48:35 +0100 Subject: [PATCH 16/20] [IMP] purchase_order_import: log exception in case of parsing error --- purchase_order_import/wizard/order_response_import.py | 1 + 1 file changed, 1 insertion(+) diff --git a/purchase_order_import/wizard/order_response_import.py b/purchase_order_import/wizard/order_response_import.py index 1da878020d..ccd92fba48 100644 --- a/purchase_order_import/wizard/order_response_import.py +++ b/purchase_order_import/wizard/order_response_import.py @@ -118,6 +118,7 @@ def parse_order_response(self, document, filename): try: xml_root = etree.fromstring(document) except: + logger.exception("File is not XML-compliant") raise UserError(_("This XML file is not XML-compliant")) if logger.isEnabledFor(logging.DEBUG): pretty_xml_string = etree.tostring( From 21df54c578e0b904e38b56f5e2b9cfb89eed2a1e Mon Sep 17 00:00:00 2001 From: David Beal Date: Mon, 13 Jul 2020 19:25:24 +0200 Subject: [PATCH 17/20] FIX purchase_order_import_*: version number after manual merge --- purchase_order_import/__manifest__.py | 2 +- purchase_order_import/i18n/es.po | 304 ++++++++++++++++++ purchase_order_import/i18n/fr.po | 304 ++++++++++++++++++ .../i18n/purchase_order_import.pot | 269 ++++++++++++++++ 4 files changed, 878 insertions(+), 1 deletion(-) diff --git a/purchase_order_import/__manifest__.py b/purchase_order_import/__manifest__.py index 1d66441b06..0f7f7dcf08 100644 --- a/purchase_order_import/__manifest__.py +++ b/purchase_order_import/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Purchase Order Import', - 'version': '10.0.1.0.0', + 'version': '10.0.2.0.0', 'category': 'Purchase Management', 'license': 'AGPL-3', 'summary': 'Update RFQ via the import of quotations from suppliers', diff --git a/purchase_order_import/i18n/es.po b/purchase_order_import/i18n/es.po index 3a4389ebb2..12db9a7b5d 100644 --- a/purchase_order_import/i18n/es.po +++ b/purchase_order_import/i18n/es.po @@ -31,36 +31,57 @@ msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:578 +#: code:addons/purchase_order_import/wizard/order_response_import.py:334 +#, python-format +msgid "%s items should be delivered into a next delivery." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Cancel" msgstr "Cancelar" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid msgid "Created by" msgstr "Creado por" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date msgid "Created on" msgstr "Creado el" #. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "" +"Date and time of the acknowledgement by the supplier. This field is filled " +"by Odoo when processing a OrderResponse document." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_display_name #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name msgid "Display Name" msgstr "Nombre mostrado" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_filename #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename msgid "Filename" msgstr "Archivo" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_id #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id msgid "ID" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is a PDF file, Odoo will try to find an XML file in the attachments of " @@ -68,6 +89,7 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is an XML file, Odoo will parse it if the module that adds support for " @@ -75,6 +97,37 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Accepted: confirm the PO and create the picking" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"If the status code is Conditionally accepted: confirm the PO and create the " +"picking and update the picking operations according to the amendments " +"specified into the document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Rejected: cancel the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"If the status code is acknowledgement: update the acknowledge datetime on " +"the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import OrderResponse File from Supplier" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.order_response_import_action #: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action msgid "Import Quotation" msgstr "" @@ -90,20 +143,91 @@ msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import___last_update #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update msgid "Last Modified on" msgstr "Última modificación el" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid msgid "Last Updated by" msgstr "Última modificación por" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date msgid "Last Updated on" msgstr "Última actualización el" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:112 +#, python-format +msgid "Missing document file" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:114 +#, python-format +msgid "Missing document filename" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:305 +#, python-format +msgid "" +"More than one move found for PO line.\n" +"Move IDs: %s\n" +"Line Info: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:586 +#: code:addons/purchase_order_import/wizard/order_response_import.py:360 +#, python-format +msgid "No backorder planned by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:179 +#, python-format +msgid "No purchase order found for name %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:125 +#, python-format +msgid "No purchase order found for name 123456." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:254 +#, python-format +msgid "PO cancelled by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:264 +#, python-format +msgid "PO confirmed by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:276 +#, python-format +msgid "PO confirmed with amendment by the supplier." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Possible line amendments:" +msgstr "" + #. module: purchase_order_import #: selection:purchase.order.import,update_option:0 msgid "Price" @@ -114,16 +238,55 @@ msgstr "" msgid "Price and Quantity" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order +msgid "Purchase Order" +msgstr "" + #. module: purchase_order_import #: model:ir.model,name:purchase_order_import.model_purchase_order_import msgid "Purchase Order Import from Files" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_order_response_import +msgid "Purchase Order Response Import from Files" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id msgid "RFQ to Update" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:408 +#, python-format +msgid "" +"Some Pack Operations have already started! Please validate or reset " +"operations on picking %s to ensure delivery slip to be computed." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "Supplier Acknowledgement Date" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:208 +#, python-format +msgid "" +"The currency of the imported OrderResponse (%s) is different from the " +"currency of the purchase order (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:158 +#, python-format +msgid "" +"The currency of the imported OrderResponse (USD) is different from the " +"currency of the purchase order (EUR)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format @@ -140,6 +303,19 @@ msgid "" "file '%s'" msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"The order line is accepted with change: The picking operation is modified. " +"(split to stock move). If a backorder qty is provided, a backorder is " +"created. Otherwise the stock.move for the missing quantities is cancelled." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "The order line is refused: The picking operation is cancelled" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format @@ -148,6 +324,14 @@ msgid "" "%s %s." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:200 +#, python-format +msgid "" +"The supplier of the imported OrderResponse (%s) is different from the " +"supplier of the purchase order (%s)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format @@ -170,6 +354,12 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Then, Odoo will process the related purchase order as follow:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:69 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." @@ -183,12 +373,14 @@ msgid "" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:122 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:137 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "" @@ -196,12 +388,36 @@ msgid "" "and it's extension." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:235 +#, python-format +msgid "" +"This purchase order has been updated automatically via the import of " +"OrderResponse file %s." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:79 +#, python-format +msgid "" +"This type of XML Order Document is not supported. Did you install the module " +"to support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:55 +#, python-format +msgid "" +"This type of XML Order Response is not supported. Did you install the module " +"to support this XML format?" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 @@ -212,10 +428,61 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.menu,name:purchase_order_import.order_response_import_importer_menu +msgid "UBL OrderResponse Importer" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:281 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: %s\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:259 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: [%s]\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:230 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: []\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "Universal Business Language" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:226 +#, python-format +msgid "Unknown status '%s'." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:141 +#, python-format +msgid "Unknown status 'unknown'." +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option msgid "Update Option" @@ -233,6 +500,20 @@ msgid "" "formats: XML and PDF (PDF with an embeded XML file)." msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_order_response_import_document +msgid "" +"Upload an Order response file that you received from your supplier. " +"Supported formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"Upload below the OrderResponse you received from your supplier. When you " +"click on the import button:" +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" @@ -240,6 +521,11 @@ msgid "" "as XML or PDF file. When you click on the Update RFQ button:" msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_document +msgid "XML or PDF Order response" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file msgid "XML or PDF Quotation" @@ -252,6 +538,7 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for lines that are present both in the imported quotation and in the RFQ, " @@ -273,9 +560,26 @@ msgid "" "example), Odoo will add them to the RFQ," msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"format (UBL), you should install the module order_response_import_ubl." +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "format (UBL), you should install the module purchase_order_import_ubl." msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:434 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:500 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:523 +#, python-format +msgid "" +"my note\n" +"%s items should be delivered into a next delivery." +msgstr "" diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po index 5e79a5c45a..c485a32e43 100644 --- a/purchase_order_import/i18n/fr.po +++ b/purchase_order_import/i18n/fr.po @@ -31,36 +31,57 @@ msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:578 +#: code:addons/purchase_order_import/wizard/order_response_import.py:334 +#, python-format +msgid "%s items should be delivered into a next delivery." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Cancel" msgstr "Annuler" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid msgid "Created by" msgstr "Créé par" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date msgid "Created on" msgstr "Créé le" #. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "" +"Date and time of the acknowledgement by the supplier. This field is filled " +"by Odoo when processing a OrderResponse document." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_display_name #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name msgid "Display Name" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_filename #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename msgid "Filename" msgstr "Nom du fichier" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_id #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id msgid "ID" msgstr "ID" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is a PDF file, Odoo will try to find an XML file in the attachments of " @@ -68,6 +89,7 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is an XML file, Odoo will parse it if the module that adds support for " @@ -75,6 +97,37 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Accepted: confirm the PO and create the picking" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"If the status code is Conditionally accepted: confirm the PO and create the " +"picking and update the picking operations according to the amendments " +"specified into the document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Rejected: cancel the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"If the status code is acknowledgement: update the acknowledge datetime on " +"the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import OrderResponse File from Supplier" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.order_response_import_action #: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action msgid "Import Quotation" msgstr "" @@ -90,20 +143,91 @@ msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import___last_update #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update msgid "Last Modified on" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid msgid "Last Updated by" msgstr "Dernière mise-à-jour par" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date msgid "Last Updated on" msgstr "Dernière mise-à-jour le" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:112 +#, python-format +msgid "Missing document file" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:114 +#, python-format +msgid "Missing document filename" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:305 +#, python-format +msgid "" +"More than one move found for PO line.\n" +"Move IDs: %s\n" +"Line Info: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:586 +#: code:addons/purchase_order_import/wizard/order_response_import.py:360 +#, python-format +msgid "No backorder planned by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:179 +#, python-format +msgid "No purchase order found for name %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:125 +#, python-format +msgid "No purchase order found for name 123456." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:254 +#, python-format +msgid "PO cancelled by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:264 +#, python-format +msgid "PO confirmed by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:276 +#, python-format +msgid "PO confirmed with amendment by the supplier." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Possible line amendments:" +msgstr "" + #. module: purchase_order_import #: selection:purchase.order.import,update_option:0 msgid "Price" @@ -114,16 +238,55 @@ msgstr "" msgid "Price and Quantity" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order +msgid "Purchase Order" +msgstr "" + #. module: purchase_order_import #: model:ir.model,name:purchase_order_import.model_purchase_order_import msgid "Purchase Order Import from Files" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_order_response_import +msgid "Purchase Order Response Import from Files" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id msgid "RFQ to Update" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:408 +#, python-format +msgid "" +"Some Pack Operations have already started! Please validate or reset " +"operations on picking %s to ensure delivery slip to be computed." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "Supplier Acknowledgement Date" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:208 +#, python-format +msgid "" +"The currency of the imported OrderResponse (%s) is different from the " +"currency of the purchase order (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:158 +#, python-format +msgid "" +"The currency of the imported OrderResponse (USD) is different from the " +"currency of the purchase order (EUR)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format @@ -140,6 +303,19 @@ msgid "" "file '%s'" msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"The order line is accepted with change: The picking operation is modified. " +"(split to stock move). If a backorder qty is provided, a backorder is " +"created. Otherwise the stock.move for the missing quantities is cancelled." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "The order line is refused: The picking operation is cancelled" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format @@ -148,6 +324,14 @@ msgid "" "%s %s." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:200 +#, python-format +msgid "" +"The supplier of the imported OrderResponse (%s) is different from the " +"supplier of the purchase order (%s)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format @@ -170,6 +354,12 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Then, Odoo will process the related purchase order as follow:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:69 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." @@ -183,12 +373,14 @@ msgid "" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:122 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:137 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "" @@ -196,12 +388,36 @@ msgid "" "and it's extension." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:235 +#, python-format +msgid "" +"This purchase order has been updated automatically via the import of " +"OrderResponse file %s." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:79 +#, python-format +msgid "" +"This type of XML Order Document is not supported. Did you install the module " +"to support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:55 +#, python-format +msgid "" +"This type of XML Order Response is not supported. Did you install the module " +"to support this XML format?" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 @@ -212,10 +428,61 @@ msgid "" msgstr "" #. module: purchase_order_import +#: model:ir.ui.menu,name:purchase_order_import.order_response_import_importer_menu +msgid "UBL OrderResponse Importer" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:281 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: %s\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:259 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: [%s]\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:230 +#, python-format +msgid "" +"Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order " +"line ids: \n" +" received: []\n" +"expected: %s\n" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:226 +#, python-format +msgid "Unknown status '%s'." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:141 +#, python-format +msgid "Unknown status 'unknown'." +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option msgid "Update Option" @@ -233,6 +500,20 @@ msgid "" "formats: XML and PDF (PDF with an embeded XML file)." msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_order_response_import_document +msgid "" +"Upload an Order response file that you received from your supplier. " +"Supported formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"Upload below the OrderResponse you received from your supplier. When you " +"click on the import button:" +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" @@ -240,6 +521,11 @@ msgid "" "as XML or PDF file. When you click on the Update RFQ button:" msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_document +msgid "XML or PDF Order response" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file msgid "XML or PDF Quotation" @@ -252,6 +538,7 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for lines that are present both in the imported quotation and in the RFQ, " @@ -273,9 +560,26 @@ msgid "" "example), Odoo will add them to the RFQ," msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "" +"format (UBL), you should install the module order_response_import_ubl." +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "format (UBL), you should install the module purchase_order_import_ubl." msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:434 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:500 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:523 +#, python-format +msgid "" +"my note\n" +"%s items should be delivered into a next delivery." +msgstr "" diff --git a/purchase_order_import/i18n/purchase_order_import.pot b/purchase_order_import/i18n/purchase_order_import.pot index 5fb2b0b630..63baa32a1c 100644 --- a/purchase_order_import/i18n/purchase_order_import.pot +++ b/purchase_order_import/i18n/purchase_order_import.pot @@ -26,46 +26,92 @@ msgid "%d order line(s) are not in the imported quotation: %s" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:578 +#: code:addons/purchase_order_import/wizard/order_response_import.py:334 +#, python-format +msgid "%s items should be delivered into a next delivery." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Cancel" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_uid msgid "Created by" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_create_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_create_date msgid "Created on" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "Date and time of the acknowledgement by the supplier. This field is filled by Odoo when processing a OrderResponse document." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_display_name #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_display_name msgid "Display Name" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_filename #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_filename msgid "Filename" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_id #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_id msgid "ID" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file." msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Accepted: confirm the PO and create the picking" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is Rejected: cancel the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "If the status code is acknowledgement: update the acknowledge datetime on the PO" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import OrderResponse File from Supplier" +msgstr "" + +#. module: purchase_order_import +#: model:ir.actions.act_window,name:purchase_order_import.order_response_import_action #: model:ir.actions.act_window,name:purchase_order_import.purchase_order_import_action msgid "Import Quotation" msgstr "" @@ -81,20 +127,90 @@ msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Import document" +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import___last_update #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import___last_update msgid "Last Modified on" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_uid #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_uid msgid "Last Updated by" msgstr "" #. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_write_date #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_write_date msgid "Last Updated on" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:112 +#, python-format +msgid "Missing document file" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:114 +#, python-format +msgid "Missing document filename" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:305 +#, python-format +msgid "More than one move found for PO line.\n" +"Move IDs: %s\n" +"Line Info: %s" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:586 +#: code:addons/purchase_order_import/wizard/order_response_import.py:360 +#, python-format +msgid "No backorder planned by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:179 +#, python-format +msgid "No purchase order found for name %s." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:125 +#, python-format +msgid "No purchase order found for name 123456." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:254 +#, python-format +msgid "PO cancelled by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:264 +#, python-format +msgid "PO confirmed by the supplier." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:276 +#, python-format +msgid "PO confirmed with amendment by the supplier." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Possible line amendments:" +msgstr "" + #. module: purchase_order_import #: selection:purchase.order.import,update_option:0 msgid "Price" @@ -105,16 +221,49 @@ msgstr "" msgid "Price and Quantity" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_purchase_order +msgid "Purchase Order" +msgstr "" + #. module: purchase_order_import #: model:ir.model,name:purchase_order_import.model_purchase_order_import msgid "Purchase Order Import from Files" msgstr "" +#. module: purchase_order_import +#: model:ir.model,name:purchase_order_import.model_order_response_import +msgid "Purchase Order Response Import from Files" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_purchase_id msgid "RFQ to Update" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:408 +#, python-format +msgid "Some Pack Operations have already started! Please validate or reset operations on picking %s to ensure delivery slip to be computed." +msgstr "" + +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_supplier_ack_dt +msgid "Supplier Acknowledgement Date" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:208 +#, python-format +msgid "The currency of the imported OrderResponse (%s) is different from the currency of the purchase order (%s)." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:158 +#, python-format +msgid "The currency of the imported OrderResponse (USD) is different from the currency of the purchase order (EUR)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:250 #, python-format @@ -127,12 +276,28 @@ msgstr "" msgid "The incoterm has been updated from %s to %s upon import of the quotation file '%s'" msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "The order line is refused: The picking operation is cancelled" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:180 #, python-format msgid "The quantity has been updated on the RFQ line with product '%s' from %s to %s %s." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:200 +#, python-format +msgid "The supplier of the imported OrderResponse (%s) is different from the supplier of the purchase order (%s)." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:246 #, python-format @@ -151,6 +316,12 @@ msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Then, Odoo will process the related purchase order as follow:" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:69 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:53 #, python-format msgid "There are no embedded XML file in this PDF file." @@ -163,23 +334,43 @@ msgid "This RFQ has been updated automatically via the import of quotation file msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:122 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:100 #, python-format msgid "This XML file is not XML-compliant" msgstr "" #. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:137 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:110 #, python-format msgid "This file '%s' is not recognised as XML nor PDF file. Please check the file and it's extension." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:235 +#, python-format +msgid "This purchase order has been updated automatically via the import of OrderResponse file %s." +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:258 #, python-format msgid "This quotation doesn't have any line !" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:79 +#, python-format +msgid "This type of XML Order Document is not supported. Did you install the module to support this XML format?" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:55 +#, python-format +msgid "This type of XML Order Response is not supported. Did you install the module to support this XML format?" +msgstr "" + #. module: purchase_order_import #: code:addons/purchase_order_import/wizard/purchase_order_import.py:42 #: code:addons/purchase_order_import/wizard/purchase_order_import.py:62 @@ -188,10 +379,58 @@ msgid "This type of XML quotation is not supported. Did you install the module t msgstr "" #. module: purchase_order_import +#: model:ir.ui.menu,name:purchase_order_import.order_response_import_importer_menu +msgid "UBL OrderResponse Importer" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:281 +#, python-format +msgid "Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order line ids: \n" +" received: %s\n" +"expected: %s\n" +"" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:259 +#, python-format +msgid "Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order line ids: \n" +" received: [%s]\n" +"expected: %s\n" +"" +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:230 +#, python-format +msgid "Unable to conditionally confirm the purchase order. \n" +"Line IDS into the parsed document differs from the expected list of order line ids: \n" +" received: []\n" +"expected: %s\n" +"" +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/wizard/order_response_import.py:226 +#, python-format +msgid "Unknown status '%s'." +msgstr "" + +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:141 +#, python-format +msgid "Unknown status 'unknown'." +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_update_option msgid "Update Option" @@ -207,11 +446,26 @@ msgstr "" msgid "Upload a quotation file that you received from your supplier. Supported formats: XML and PDF (PDF with an embeded XML file)." msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,help:purchase_order_import.field_order_response_import_document +msgid "Upload an Order response file that you received from your supplier. Supported formats: XML and PDF (PDF with an embeded XML file)." +msgstr "" + +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "Upload below the OrderResponse you received from your supplier. When you click on the import button:" +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:" msgstr "" +#. module: purchase_order_import +#: model:ir.model.fields,field_description:purchase_order_import.field_order_response_import_document +msgid "XML or PDF Order response" +msgstr "" + #. module: purchase_order_import #: model:ir.model.fields,field_description:purchase_order_import.field_purchase_order_import_quote_file msgid "XML or PDF Quotation" @@ -224,6 +478,7 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option)," msgstr "" @@ -238,8 +493,22 @@ msgstr "" msgid "for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ," msgstr "" +#. module: purchase_order_import +#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form +msgid "format (UBL), you should install the module order_response_import_ubl." +msgstr "" + #. module: purchase_order_import #: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "format (UBL), you should install the module purchase_order_import_ubl." msgstr "" +#. module: purchase_order_import +#: code:addons/purchase_order_import/tests/test_order_response_import.py:434 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:500 +#: code:addons/purchase_order_import/tests/test_order_response_import.py:523 +#, python-format +msgid "my note\n" +"%s items should be delivered into a next delivery." +msgstr "" + From c32bb3632565ebbc1db9f84fe474ee6c466a42a3 Mon Sep 17 00:00:00 2001 From: Maksym Yankin Date: Mon, 27 Apr 2026 15:47:35 +0300 Subject: [PATCH 18/20] [IMP] purchase_order_import: pre-commit auto fixes --- purchase_order_import/__init__.py | 2 - purchase_order_import/__manifest__.py | 31 +- .../models/purchase_order.py | 2 - purchase_order_import/pyproject.toml | 3 + .../tests/test_order_response_import.py | 65 ++-- purchase_order_import/views/purchase.xml | 37 +- purchase_order_import/wizard/__init__.py | 2 - .../wizard/order_response_import.py | 61 +--- .../wizard/order_response_import_view.xml | 82 +++-- .../wizard/purchase_order_import.py | 340 ++++++++++-------- .../wizard/purchase_order_import_view.xml | 100 +++--- 11 files changed, 381 insertions(+), 344 deletions(-) create mode 100644 purchase_order_import/pyproject.toml diff --git a/purchase_order_import/__init__.py b/purchase_order_import/__init__.py index 3c4e748f01..134df27435 100644 --- a/purchase_order_import/__init__.py +++ b/purchase_order_import/__init__.py @@ -1,4 +1,2 @@ -# -*- coding: utf-8 -*- - from . import wizard from . import models diff --git a/purchase_order_import/__manifest__.py b/purchase_order_import/__manifest__.py index 0f7f7dcf08..84ca9bd10e 100644 --- a/purchase_order_import/__manifest__.py +++ b/purchase_order_import/__manifest__.py @@ -1,24 +1,19 @@ -# -*- coding: utf-8 -*- # © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Purchase Order Import', - 'version': '10.0.2.0.0', - 'category': 'Purchase Management', - 'license': 'AGPL-3', - 'summary': 'Update RFQ via the import of quotations from suppliers', - 'author': 'Akretion,Odoo Community Association (OCA)', - 'website': 'http://www.akretion.com', - 'depends': [ - 'purchase', - 'base_business_document_import_stock', - 'onchange_helper' - ], - 'data': [ - 'wizard/order_response_import_view.xml', - 'wizard/purchase_order_import_view.xml', - 'views/purchase.xml', + "name": "Purchase Order Import", + "version": "10.0.2.0.0", + "category": "Purchase Management", + "license": "AGPL-3", + "summary": "Update RFQ via the import of quotations from suppliers", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi", + "depends": ["purchase", "base_business_document_import_stock", "onchange_helper"], + "data": [ + "wizard/order_response_import_view.xml", + "wizard/purchase_order_import_view.xml", + "views/purchase.xml", ], - 'installable': True, + "installable": True, } diff --git a/purchase_order_import/models/purchase_order.py b/purchase_order_import/models/purchase_order.py index 7bf5a2acd6..67f3ed7ef7 100644 --- a/purchase_order_import/models/purchase_order.py +++ b/purchase_order_import/models/purchase_order.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -6,7 +5,6 @@ class PurchaseOrder(models.Model): - _inherit = "purchase.order" supplier_ack_dt = fields.Datetime( diff --git a/purchase_order_import/pyproject.toml b/purchase_order_import/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/purchase_order_import/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/purchase_order_import/tests/test_order_response_import.py b/purchase_order_import/tests/test_order_response_import.py index 536be02032..2ed4c74fea 100644 --- a/purchase_order_import/tests/test_order_response_import.py +++ b/purchase_order_import/tests/test_order_response_import.py @@ -1,26 +1,25 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, _ +from odoo import _, fields from odoo.exceptions import UserError from odoo.tests import SavepointCase from ..wizard.order_response_import import ( - ORDER_RESPONSE_STATUS_ACK, - ORDER_RESPONSE_STATUS_ACCEPTED, - ORDER_RESPONSE_STATUS_REJECTED, - ORDER_RESPONSE_STATUS_CONDITIONAL, LINE_STATUS_ACCEPTED, - LINE_STATUS_REJECTED, LINE_STATUS_AMEND, + LINE_STATUS_REJECTED, + ORDER_RESPONSE_STATUS_ACCEPTED, + ORDER_RESPONSE_STATUS_ACK, + ORDER_RESPONSE_STATUS_CONDITIONAL, + ORDER_RESPONSE_STATUS_REJECTED, ) class TestOrderResponseImportCommon(SavepointCase): @classmethod def setUpClass(cls): - super(TestOrderResponseImportCommon, cls).setUpClass() + super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.supplier = cls.env.ref("base.res_partner_12") cls.supplier.vat = "BE0477472701" @@ -30,17 +29,13 @@ def setUpClass(cls): cls.product_1 = cls.env["product.product"].create( { "name": "Product 1", - "seller_ids": [ - (0, 0, {"name": cls.supplier.id, "product_code": "P1"}) - ], + "seller_ids": [(0, 0, {"name": cls.supplier.id, "product_code": "P1"})], } ) cls.product_2 = cls.env["product.product"].create( { "name": "Product 2", - "seller_ids": [ - (0, 0, {"name": cls.supplier.id, "product_code": "P2"}) - ], + "seller_ids": [(0, 0, {"name": cls.supplier.id, "product_code": "P2"})], } ) cls.purchase_order = cls.env["purchase.order"].create( @@ -379,9 +374,7 @@ def test_12(self): self.assertTrue(self.purchase_order.picking_ids) move_ids = self.line1.move_ids self.assertEqual(len(move_ids), 2) - self.assertEqual( - sum(move_ids.mapped("product_qty")), self.line1.product_qty - ) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) assigned = move_ids.filtered(lambda s: s.state == "assigned") self.assertEqual(assigned.product_qty, confirmed_qty) cancel = move_ids.filtered(lambda s: s.state == "cancel") @@ -423,16 +416,13 @@ def test_13(self): self.assertEqual(len(self.purchase_order.picking_ids), 2) move_ids = self.line1.move_ids self.assertEqual(len(move_ids), 2) - self.assertEqual( - sum(move_ids.mapped("product_qty")), self.line1.product_qty - ) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) move_confirmed = move_ids.filtered( lambda s: s.state == "assigned" and s.product_qty == confirmed_qty ) self.assertTrue(move_confirmed) self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") - % "3", + _("my note\n%s items should be delivered into a next delivery.") % "3", move_confirmed.note, ) move_backorder = move_ids.filtered( @@ -440,7 +430,8 @@ def test_13(self): ) self.assertTrue(move_backorder) self.assertEqual( - move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, ) def test_14(self): @@ -492,13 +483,11 @@ def test_14(self): sum(line1_move_ids.mapped("product_qty")), self.line1.product_qty ) move_confirmed = line1_move_ids.filtered( - lambda s: s.state == "assigned" - and s.product_qty == line1_confirmed_qty + lambda s: s.state == "assigned" and s.product_qty == line1_confirmed_qty ) self.assertTrue(move_confirmed) self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") - % "3", + _("my note\n%s items should be delivered into a next delivery.") % "3", move_confirmed.note, ) move_backorder = line1_move_ids.filtered( @@ -506,7 +495,8 @@ def test_14(self): ) self.assertTrue(move_backorder) self.assertEqual( - move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, ) # lin1 line2_move_ids = self.line2.move_ids @@ -515,13 +505,11 @@ def test_14(self): sum(line2_move_ids.mapped("product_qty")), self.line2.product_qty ) move_confirmed = line2_move_ids.filtered( - lambda s: s.state == "assigned" - and s.product_qty == line2_confirmed_qty + lambda s: s.state == "assigned" and s.product_qty == line2_confirmed_qty ) self.assertTrue(move_confirmed) self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") - % "3", + _("my note\n%s items should be delivered into a next delivery.") % "3", move_confirmed.note, ) move_backorder = line2_move_ids.filtered( @@ -529,7 +517,8 @@ def test_14(self): ) self.assertTrue(move_backorder) self.assertEqual( - move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, ) def test_15(self): @@ -567,9 +556,7 @@ def test_15(self): self.assertEqual(len(self.purchase_order.picking_ids), 2) move_ids = self.line1.move_ids self.assertEqual(len(move_ids), 3) - self.assertEqual( - sum(move_ids.mapped("product_qty")), self.line1.product_qty - ) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) move_confirmed = move_ids.filtered( lambda s: s.state == "assigned" and s.product_qty == confirmed_qty ) @@ -583,12 +570,14 @@ def test_15(self): ) self.assertTrue(move_cancel) self.assertEqual( - _("No backorder planned by the supplier."), move_cancel.note, + _("No backorder planned by the supplier."), + move_cancel.note, ) move_backorder = move_ids.filtered( lambda s: s.state == "assigned" and s.product_qty == 2 ) self.assertTrue(move_backorder) self.assertEqual( - move_backorder.picking_id.backorder_id, move_confirmed.picking_id, + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, ) diff --git a/purchase_order_import/views/purchase.xml b/purchase_order_import/views/purchase.xml index 07159ecbed..497b05cc23 100644 --- a/purchase_order_import/views/purchase.xml +++ b/purchase_order_import/views/purchase.xml @@ -1,25 +1,26 @@ - + - - - - purchase_order_import.purchase.order.form - purchase.order - - - - - + + purchase_order_import.purchase.order.form + purchase.order + + + + + + - - - + diff --git a/purchase_order_import/wizard/__init__.py b/purchase_order_import/wizard/__init__.py index 5134c02956..5d8ab4abcd 100644 --- a/purchase_order_import/wizard/__init__.py +++ b/purchase_order_import/wizard/__init__.py @@ -1,4 +1,2 @@ -# -*- coding: utf-8 -*- - from . import purchase_order_import from . import order_response_import diff --git a/purchase_order_import/wizard/order_response_import.py b/purchase_order_import/wizard/order_response_import.py index ccd92fba48..7eaf05310c 100644 --- a/purchase_order_import/wizard/order_response_import.py +++ b/purchase_order_import/wizard/order_response_import.py @@ -1,14 +1,15 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ -from odoo.tools import config, float_compare -from odoo.exceptions import UserError, ValidationError import logging import mimetypes + from lxml import etree +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import config, float_compare + logger = logging.getLogger(__name__) ORDER_RESPONSE_STATUS_ACK = "acknowledgement" @@ -35,9 +36,7 @@ class OrderResponseImport(models.TransientModel): @api.model def _get_purchase_id(self): - assert ( - self._context["active_model"] == "purchase.order" - ), "bad active_model" + assert self._context["active_model"] == "purchase.order", "bad active_model" return self.env["purchase.order"].browse(self._context["active_id"]) document = fields.Binary( @@ -65,9 +64,7 @@ def parse_pdf_order_document(self, document): """ xml_files_dict = self.get_xml_files_from_pdf(document) if not xml_files_dict: - raise UserError( - _("There are no embedded XML file in this PDF file.") - ) + raise UserError(_("There are no embedded XML file in this PDF file.")) for xml_filename, xml_root in xml_files_dict.iteritems(): logger.info("Trying to parse XML file %s", xml_filename) try: @@ -140,14 +137,10 @@ def parse_order_response(self, document, filename): ) % filename ) - logger.debug( - "Result of OrderResponse parsing: ", parsed_order_document - ) + logger.debug("Result of OrderResponse parsing: ", parsed_order_document) if "attachments" not in parsed_order_document: parsed_order_document["attachments"] = {} - parsed_order_document["attachments"][filename] = document.encode( - "base64" - ) + parsed_order_document["attachments"][filename] = document.encode("base64") if "chatter_msg" not in parsed_order_document: parsed_order_document["chatter_msg"] = [] if ( @@ -188,10 +181,7 @@ def process_data(self, parsed_order_document): parsed_order_document["chatter_msg"], partner_type="supplier", ) - if ( - partner.commercial_partner_id - != order.partner_id.commercial_partner_id - ): + if partner.commercial_partner_id != order.partner_id.commercial_partner_id: bdio.user_error_wrap( _( "The supplier of the imported OrderResponse (%s) " @@ -250,9 +240,7 @@ def _process_rejected(self, purchase_order, parsed_order_document): parsed_order_document["chatter_msg"] = ( parsed_order_document["chatter_msg"] or [] ) - parsed_order_document["chatter_msg"].append( - _("PO cancelled by the supplier.") - ) + parsed_order_document["chatter_msg"].append(_("PO cancelled by the supplier.")) purchase_order.button_cancel() @api.model @@ -260,9 +248,7 @@ def _process_accepted(self, purchase_order, parsed_order_document): parsed_order_document["chatter_msg"] = ( parsed_order_document["chatter_msg"] or [] ) - parsed_order_document["chatter_msg"].append( - _("PO confirmed by the supplier.") - ) + parsed_order_document["chatter_msg"].append(_("PO confirmed by the supplier.")) purchase_order.button_approve() @api.model @@ -320,10 +306,7 @@ def _process_conditional(self, purchase_order, parsed_order_document): qty = line_info["qty"] backorder_qty = line_info["backorder_qty"] move_qty = move.product_qty - if ( - float_compare(qty, move_qty, precision_digits=precision) - < 0 - ): + if float_compare(qty, move_qty, precision_digits=precision) < 0: self._check_picking_status(move.picking_id) new_move_id = move.split(move_qty - qty) new_move = move.browse(new_move_id) @@ -331,9 +314,7 @@ def _process_conditional(self, purchase_order, parsed_order_document): if backorder_qty: note = note + "\n" if note else "" note += ( - _( - "%s items should be delivered into a next delivery." - ) + _("%s items should be delivered into a next delivery.") % backorder_qty ) move.note = note @@ -356,11 +337,7 @@ def _process_conditional(self, purchase_order, parsed_order_document): if to_cancel: to_cancel.action_cancel() to_cancel.write( - { - "note": _( - "No backorder planned by the supplier." - ) - } + {"note": _("No backorder planned by the supplier.")} ) if new_move.state != "cancel": # move the new move into an backorder picking to avoid @@ -381,9 +358,7 @@ def _add_move_to_backorder(self, move): """ StockPicking = self.env["stock.picking"] current_picking = move.picking_id - backorder = StockPicking.search( - [("backorder_id", "=", current_picking.id)] - ) + backorder = StockPicking.search([("backorder_id", "=", current_picking.id)]) if not backorder: date_done = current_picking.date_done move.picking_id._create_backorder(backorder_moves=move) @@ -401,9 +376,7 @@ def _check_picking_status(self, picking): :param picking: :return: """ - if any( - operation.qty_done != 0 for operation in picking.pack_operation_ids - ): + if any(operation.qty_done != 0 for operation in picking.pack_operation_ids): raise ValidationError( _( "Some Pack Operations have already started! " diff --git a/purchase_order_import/wizard/order_response_import_view.xml b/purchase_order_import/wizard/order_response_import_view.xml index bc60c7bbaf..e87a93c1ef 100644 --- a/purchase_order_import/wizard/order_response_import_view.xml +++ b/purchase_order_import/wizard/order_response_import_view.xml @@ -1,44 +1,60 @@ - + - - order.response.import.form order.response.import - +
-
-

Upload below the OrderResponse you received from your supplier. When you click on the import button:

-
    -
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module order_response_import_ubl.
  2. -
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. -
-

Then, Odoo will process the related purchase order as follow:

-
    -
  • If the status code is acknowledgement: update the acknowledge datetime on the PO
  • -
  • If the status code is Rejected: cancel the PO
  • -
  • If the status code is Accepted: confirm the PO and create the picking
  • -
  • If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document
  • -
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • -
-

Possible line amendments:

-
    -
  • The order line is refused: The picking operation is cancelled
  • -
  • The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled.
  • -
-
+
+

Upload below the OrderResponse you received from your supplier. When you click on the import button:

+
    +
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module order_response_import_ubl.
  2. +
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. +
+

Then, Odoo will process the related purchase order as follow:

+
    +
  • If the status code is acknowledgement: update the acknowledge datetime on the PO
  • +
  • If the status code is Rejected: cancel the PO
  • +
  • If the status code is Accepted: confirm the PO and create the picking
  • +
  • If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document
  • +
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • +
+

Possible line amendments:

+
    +
  • The order line is refused: The picking operation is cancelled
  • +
  • The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled.
  • +
+
- - + +
-
@@ -53,10 +69,8 @@ UBL OrderResponse Importer - - - + + + - -
diff --git a/purchase_order_import/wizard/purchase_order_import.py b/purchase_order_import/wizard/purchase_order_import.py index 6d3887980c..54e8928c87 100644 --- a/purchase_order_import/wizard/purchase_order_import.py +++ b/purchase_order_import/wizard/purchase_order_import.py @@ -1,47 +1,60 @@ -# -*- coding: utf-8 -*- # Copyright 2016-2018 Akretion France (http://www.akretion.com/) # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ -from odoo.tools import float_is_zero, config -from odoo.exceptions import UserError import logging import mimetypes + from lxml import etree +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools import config, float_is_zero + logger = logging.getLogger(__name__) class PurchaseOrderImport(models.TransientModel): - _name = 'purchase.order.import' - _description = 'Purchase Order Import from Files' + _name = "purchase.order.import" + _description = "Purchase Order Import from Files" @api.model def _get_purchase_id(self): - assert self._context['active_model'] == 'purchase.order',\ - 'bad active_model' - return self.env['purchase.order'].browse(self._context['active_id']) + assert self._context["active_model"] == "purchase.order", "bad active_model" + return self.env["purchase.order"].browse(self._context["active_id"]) quote_file = fields.Binary( - string='XML or PDF Quotation', required=True, + string="XML or PDF Quotation", + required=True, help="Upload a quotation file that you received from " "your supplier. Supported formats: XML and PDF " - "(PDF with an embeded XML file).") - quote_filename = fields.Char(string='Filename') - update_option = fields.Selection([ - ('price', 'Price'), - ('all', 'Price and Quantity'), - ], default='price', string='Update Option', required=True) + "(PDF with an embeded XML file).", + ) + quote_filename = fields.Char(string="Filename") + update_option = fields.Selection( + [ + ("price", "Price"), + ("all", "Price and Quantity"), + ], + default="price", + string="Update Option", + required=True, + ) purchase_id = fields.Many2one( - 'purchase.order', string='RFQ to Update', default=_get_purchase_id, - readonly=True) + "purchase.order", + string="RFQ to Update", + default=_get_purchase_id, + readonly=True, + ) @api.model def parse_xml_quote(self, xml_root): - raise UserError(_( - "This type of XML quotation is not supported. Did you install " - "the module to support this XML format?")) + raise UserError( + _( + "This type of XML quotation is not supported. Did you install " + "the module to support this XML format?" + ) + ) @api.model def parse_pdf_quote(self, quote_file): @@ -50,18 +63,20 @@ def parse_pdf_quote(self, quote_file): """ xml_files_dict = self.get_xml_files_from_pdf(quote_file) if not xml_files_dict: - raise UserError(_( - 'There are no embedded XML file in this PDF file.')) + raise UserError(_("There are no embedded XML file in this PDF file.")) for xml_filename, xml_root in xml_files_dict.iteritems(): - logger.info('Trying to parse XML file %s', xml_filename) + logger.info("Trying to parse XML file %s", xml_filename) try: parsed_quote = self.parse_xml_quote(xml_root) return parsed_quote except: continue - raise UserError(_( - "This type of XML quotation is not supported. Did you install " - "the module to support this XML format?")) + raise UserError( + _( + "This type of XML quotation is not supported. Did you install " + "the module to support this XML format?" + ) + ) # Format of parsed_quote # { @@ -89,180 +104,221 @@ def parse_pdf_quote(self, quote_file): @api.model def parse_quote(self, quote_file, quote_filename): - assert quote_file, 'Missing quote file' - assert quote_filename, 'Missing quote filename' + assert quote_file, "Missing quote file" + assert quote_filename, "Missing quote filename" filetype = mimetypes.guess_type(quote_filename)[0] - logger.debug('Quote file mimetype: %s', filetype) - if filetype in ['application/xml', 'text/xml']: + logger.debug("Quote file mimetype: %s", filetype) + if filetype in ["application/xml", "text/xml"]: try: xml_root = etree.fromstring(quote_file) except: raise UserError(_("This XML file is not XML-compliant")) pretty_xml_string = etree.tostring( - xml_root, pretty_print=True, encoding='UTF-8', - xml_declaration=True) - logger.debug('Starting to import the following XML file:') + xml_root, pretty_print=True, encoding="UTF-8", xml_declaration=True + ) + logger.debug("Starting to import the following XML file:") logger.debug(pretty_xml_string) parsed_quote = self.parse_xml_quote(xml_root) - elif filetype == 'application/pdf': + elif filetype == "application/pdf": parsed_quote = self.parse_pdf_quote(quote_file) else: - raise UserError(_( - "This file '%s' is not recognised as XML nor PDF file. " - "Please check the file and it's extension.") % quote_filename) - logger.debug('Result of quotation parsing: %s', parsed_quote) - if 'attachments' not in parsed_quote: - parsed_quote['attachments'] = {} - parsed_quote['attachments'][quote_filename] =\ - quote_file.encode('base64') - if 'chatter_msg' not in parsed_quote: - parsed_quote['chatter_msg'] = [] + raise UserError( + _( + "This file '%s' is not recognised as XML nor PDF file. " + "Please check the file and it's extension." + ) + % quote_filename + ) + logger.debug("Result of quotation parsing: %s", parsed_quote) + if "attachments" not in parsed_quote: + parsed_quote["attachments"] = {} + parsed_quote["attachments"][quote_filename] = quote_file.encode("base64") + if "chatter_msg" not in parsed_quote: + parsed_quote["chatter_msg"] = [] if ( - parsed_quote.get('company') and - not config['test_enable'] and - not self._context.get('edi_skip_company_check')): - self.env['business.document.import']._check_company( - parsed_quote['company'], parsed_quote['chatter_msg']) + parsed_quote.get("company") + and not config["test_enable"] + and not self._context.get("edi_skip_company_check") + ): + self.env["business.document.import"]._check_company( + parsed_quote["company"], parsed_quote["chatter_msg"] + ) return parsed_quote @api.model def _prepare_update_order_vals(self, parsed_quote, order): vals = {} - incoterm = self.env['business.document.import']._match_incoterm( - parsed_quote.get('incoterm'), parsed_quote['chatter_msg']) + incoterm = self.env["business.document.import"]._match_incoterm( + parsed_quote.get("incoterm"), parsed_quote["chatter_msg"] + ) if incoterm and incoterm != order.incoterm_id: - parsed_quote['chatter_msg'].append(_( - "The incoterm has been updated from %s to %s upon import " - "of the quotation file '%s'") % ( - order.incoterm_id.code, incoterm.code, - self.quote_filename)) - vals['incoterm_id'] = incoterm.id + parsed_quote["chatter_msg"].append( + _( + "The incoterm has been updated from %s to %s upon import " + "of the quotation file '%s'" + ) + % (order.incoterm_id.code, incoterm.code, self.quote_filename) + ) + vals["incoterm_id"] = incoterm.id return vals @api.multi def update_order_lines(self, parsed_quote, order): - polo = self.env['purchase.order.line'] - chatter = parsed_quote['chatter_msg'] - dpo = self.env['decimal.precision'] - bdio = self.env['business.document.import'] - qty_prec = dpo.precision_get('Product Unit of Measure') + polo = self.env["purchase.order.line"] + chatter = parsed_quote["chatter_msg"] + dpo = self.env["decimal.precision"] + bdio = self.env["business.document.import"] + qty_prec = dpo.precision_get("Product Unit of Measure") existing_lines = [] for oline in order.order_line: price_unit = 0.0 - if not float_is_zero( - oline.product_qty, precision_digits=qty_prec): + if not float_is_zero(oline.product_qty, precision_digits=qty_prec): price_unit = oline.price_subtotal / float(oline.product_qty) - existing_lines.append({ - 'product': oline.product_id, - 'name': oline.name, - 'qty': oline.product_qty, - 'uom': oline.product_uom, - 'price_unit': price_unit, - 'line': oline, - }) + existing_lines.append( + { + "product": oline.product_id, + "name": oline.name, + "qty": oline.product_qty, + "uom": oline.product_uom, + "price_unit": price_unit, + "line": oline, + } + ) compare_res = bdio.compare_lines( - existing_lines, parsed_quote['lines'], chatter, - seller=order.partner_id.commercial_partner_id) + existing_lines, + parsed_quote["lines"], + chatter, + seller=order.partner_id.commercial_partner_id, + ) update_option = self.update_option - for oline, cdict in compare_res['to_update'].iteritems(): + for oline, cdict in compare_res["to_update"].iteritems(): write_vals = {} - if cdict.get('price_unit'): - chatter.append(_( - "The unit price has been updated on the RFQ line with " - "product '%s' from %s to %s %s.") % ( + if cdict.get("price_unit"): + chatter.append( + _( + "The unit price has been updated on the RFQ line with " + "product '%s' from %s to %s %s." + ) + % ( oline.product_id.display_name, - cdict['price_unit'][0], cdict['price_unit'][1], - order.currency_id.name)) - write_vals['price_unit'] = cdict['price_unit'][1] # TODO - if update_option == 'all' and cdict.get('qty'): - chatter.append(_( - "The quantity has been updated on the RFQ line with " - "product '%s' from %s to %s %s.") % ( + cdict["price_unit"][0], + cdict["price_unit"][1], + order.currency_id.name, + ) + ) + write_vals["price_unit"] = cdict["price_unit"][1] # TODO + if update_option == "all" and cdict.get("qty"): + chatter.append( + _( + "The quantity has been updated on the RFQ line with " + "product '%s' from %s to %s %s." + ) + % ( oline.product_id.display_name, - cdict['qty'][0], cdict['qty'][1], - oline.product_uom.name)) - write_vals['product_qty'] = cdict['qty'][1] + cdict["qty"][0], + cdict["qty"][1], + oline.product_uom.name, + ) + ) + write_vals["product_qty"] = cdict["qty"][1] if write_vals: oline.write(write_vals) - if compare_res['to_remove']: # we don't delete the lines, only warn + if compare_res["to_remove"]: # we don't delete the lines, only warn warn_label = [ - '%s %s x %s' % ( - l.product_qty, l.product_uom.name, l.product_id.name) - for l in compare_res['to_remove']] - chatter.append(_( - "%d order line(s) are not in the imported quotation: %s") % ( - len(compare_res['to_remove']), - ', '.join(warn_label))) - if compare_res['to_add']: + "%s %s x %s" % (l.product_qty, l.product_uom.name, l.product_id.name) + for l in compare_res["to_remove"] + ] + chatter.append( + _("%d order line(s) are not in the imported quotation: %s") + % (len(compare_res["to_remove"]), ", ".join(warn_label)) + ) + if compare_res["to_add"]: to_create_label = [] - for add in compare_res['to_add']: + for add in compare_res["to_add"]: line_vals = self._prepare_create_order_line( - add['product'], add['uom'], add['import_line'], - order) - line_vals['order_id'] = order.id + add["product"], add["uom"], add["import_line"], order + ) + line_vals["order_id"] = order.id new_line = polo.create(line_vals) - to_create_label.append('%s %s x %s' % ( - new_line.product_qty, - new_line.product_uom.name, - new_line.name)) - chatter.append(_("%d new order line(s) created: %s") % ( - len(compare_res['to_add']), ', '.join(to_create_label))) + to_create_label.append( + "%s %s x %s" + % (new_line.product_qty, new_line.product_uom.name, new_line.name) + ) + chatter.append( + _("%d new order line(s) created: %s") + % (len(compare_res["to_add"]), ", ".join(to_create_label)) + ) return True @api.model def _prepare_create_order_line(self, product, uom, import_line, order): - polo = self.env['purchase.order.line'] + polo = self.env["purchase.order.line"] vals = { - 'product_id': product.id, - 'order_id': order, - 'price_unit': import_line['price_unit'], + "product_id": product.id, + "order_id": order, + "price_unit": import_line["price_unit"], } - vals.update(polo.play_onchanges(vals, ['product_id'])) - vals.pop('order_id') + vals.update(polo.play_onchanges(vals, ["product_id"])) + vals.pop("order_id") return vals @api.multi def update_rfq_button(self): self.ensure_one() - bdio = self.env['business.document.import'] + bdio = self.env["business.document.import"] order = self.purchase_id - assert order, 'No link to PO' + assert order, "No link to PO" if not order: - raise UserError(_('You must select a quotation to update.')) + raise UserError(_("You must select a quotation to update.")) parsed_quote = self.parse_quote( - self.quote_file.decode('base64'), self.quote_filename) + self.quote_file.decode("base64"), self.quote_filename + ) currency = bdio._match_currency( - parsed_quote.get('currency'), parsed_quote['chatter_msg']) + parsed_quote.get("currency"), parsed_quote["chatter_msg"] + ) partner = bdio._match_partner( - parsed_quote['partner'], parsed_quote['chatter_msg'], - partner_type='supplier') - if ( - partner.commercial_partner_id != - order.partner_id.commercial_partner_id): - raise UserError(_( - "The supplier of the imported quotation (%s) is different " - "from the supplier of the RFQ (%s)." % ( - partner.commercial_partner_id.name, - order.partner_id.commercial_partner_id.name))) + parsed_quote["partner"], + parsed_quote["chatter_msg"], + partner_type="supplier", + ) + if partner.commercial_partner_id != order.partner_id.commercial_partner_id: + raise UserError( + _( + "The supplier of the imported quotation (%s) is different " + "from the supplier of the RFQ (%s)." + % ( + partner.commercial_partner_id.name, + order.partner_id.commercial_partner_id.name, + ) + ) + ) if currency != order.currency_id: - raise UserError(_( - "The currency of the imported quotation (%s) is different " - "from the currency of the RFQ (%s)") % ( - currency.name, order.currency_id.name)) + raise UserError( + _( + "The currency of the imported quotation (%s) is different " + "from the currency of the RFQ (%s)" + ) + % (currency.name, order.currency_id.name) + ) vals = self._prepare_update_order_vals(parsed_quote, order) if vals: order.write(vals) - if not parsed_quote.get('lines'): - raise UserError(_( - "This quotation doesn't have any line !")) + if not parsed_quote.get("lines"): + raise UserError(_("This quotation doesn't have any line !")) self.update_order_lines(parsed_quote, order) bdio.post_create_or_update(parsed_quote, order) logger.info( - 'purchase.order ID %d updated via import of file %s', order.id, - self.quote_filename) - order.message_post(_( - "This RFQ has been updated automatically via the import of " - "quotation file %s") % self.quote_filename) + "purchase.order ID %d updated via import of file %s", + order.id, + self.quote_filename, + ) + order.message_post( + _( + "This RFQ has been updated automatically via the import of " + "quotation file %s" + ) + % self.quote_filename + ) return True diff --git a/purchase_order_import/wizard/purchase_order_import_view.xml b/purchase_order_import/wizard/purchase_order_import_view.xml index 3a0c3d68c2..5a80af9494 100644 --- a/purchase_order_import/wizard/purchase_order_import_view.xml +++ b/purchase_order_import/wizard/purchase_order_import_view.xml @@ -1,51 +1,63 @@ - + - + + purchase.order.import.form + purchase.order.import + +
+ +
+

Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:

+
    +
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module purchase_order_import_ubl.
  2. +
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. +
+

Then, Odoo will compare the imported quotation and the current RFQ:

+
    +
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • +
  • for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ,
  • +
  • for the lines that are present only in the RFQ, Odoo will put a warning message in the chatter (it won't delete them automatically).
  • +
+
+
+ + + + + + +
+
+
+
+
- - purchase.order.import.form - purchase.order.import - -
- -
-

Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:

-
    -
  1. If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the Universal Business Language format (UBL), you should install the module purchase_order_import_ubl.
  2. -
  3. If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.
  4. -
-

Then, Odoo will compare the imported quotation and the current RFQ:

-
    -
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • -
  • for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ,
  • -
  • for the lines that are present only in the RFQ, Odoo will put a warning message in the chatter (it won't delete them automatically).
  • -
-
-
- - - - - - -
-
-
-
-
- - - Import Quotation - purchase.order.import - form - new - - + + Import Quotation + purchase.order.import + form + new +
From 9321421a8cdb3e48d0d22adf137579777f297b36 Mon Sep 17 00:00:00 2001 From: Maksym Yankin Date: Mon, 27 Apr 2026 19:03:13 +0300 Subject: [PATCH 19/20] [MIG] purchase_order_import: Migration to 19.0 --- purchase_order_import/README.rst | 143 +++-- purchase_order_import/__manifest__.py | 21 +- purchase_order_import/i18n/es.po | 27 - purchase_order_import/i18n/fr.po | 27 - .../i18n/purchase_order_import.pot | 27 - purchase_order_import/readme/CONTRIBUTORS.md | 4 + purchase_order_import/readme/DESCRIPTION.md | 36 ++ .../security/ir.model.access.csv | 3 + .../static/description/index.html | 470 ++++++++++++++ purchase_order_import/tests/__init__.py | 2 +- purchase_order_import/tests/common.py | 133 ++++ .../tests/test_order_response_import.py | 583 ------------------ .../test_order_response_import_wizard.py | 271 ++++++++ .../{purchase.xml => purchase_order.xml} | 5 +- purchase_order_import/wizard/__init__.py | 4 +- .../wizard/order_response_import.py | 387 ------------ .../wizard/order_response_import_wizard.py | 453 ++++++++++++++ ... => order_response_import_wizard_view.xml} | 36 +- .../wizard/purchase_order_import.py | 324 ---------- .../wizard/purchase_order_import_wizard.py | 380 ++++++++++++ ... => purchase_order_import_wizard_view.xml} | 17 +- 21 files changed, 1896 insertions(+), 1457 deletions(-) create mode 100644 purchase_order_import/readme/CONTRIBUTORS.md create mode 100644 purchase_order_import/readme/DESCRIPTION.md create mode 100644 purchase_order_import/security/ir.model.access.csv create mode 100644 purchase_order_import/static/description/index.html create mode 100644 purchase_order_import/tests/common.py delete mode 100644 purchase_order_import/tests/test_order_response_import.py create mode 100644 purchase_order_import/tests/test_order_response_import_wizard.py rename purchase_order_import/views/{purchase.xml => purchase_order.xml} (86%) delete mode 100644 purchase_order_import/wizard/order_response_import.py create mode 100644 purchase_order_import/wizard/order_response_import_wizard.py rename purchase_order_import/wizard/{order_response_import_view.xml => order_response_import_wizard_view.xml} (66%) delete mode 100644 purchase_order_import/wizard/purchase_order_import.py create mode 100644 purchase_order_import/wizard/purchase_order_import_wizard.py rename purchase_order_import/wizard/{purchase_order_import_view.xml => purchase_order_import_wizard_view.xml} (84%) diff --git a/purchase_order_import/README.rst b/purchase_order_import/README.rst index 3ade7d3aab..a6b8d44710 100644 --- a/purchase_order_import/README.rst +++ b/purchase_order_import/README.rst @@ -1,72 +1,123 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association ===================== Purchase Order Import ===================== -This module adds support for the import of electronic quotations. This module provides the base methods to import electronic quotations ; it requires additional modules to support specific order formats: - -* module *purchase_order_import_ubl*: adds support for `Universal Business Language (UBL) `_ quotations as: - - - XML file, - - PDF file with an embedded XML file. - -Configuration -============= - -No configuration is needed. - -Usage -===== - -This module adds a button *Import Quotation File* on Requests for Quotation. This button starts a wizard that will propose you to select the quotation file. The wizard will also propose you an update option: - -* only update the prices of the draft purchase order from the quotation file (default option), -* update prices and quantities of the draft purchase order from the quotation file. - -When you click on the button *Update RFQ*: - -* if Odoo has a line in the quotation file that is not in the draft purchase order, it will create a new purchase order line, -* if Odoo has a line in the draft purchase order that is not in the quotation file, it will write a warning in the chatter of the purchase order (it will not delete the purchase order line), -* for all the lines that are both in the draft purchase order and in the quotation file, the purchase order line will be updated if needed. -* if the incoterm of the quotation file is not the same as the incoterm of the draft purchase order, Odoo will update the incoterm of the purchase order. -* the imported quotation file is attached to the purchase order. - -Once the quotation file is imported, you should read the messages in the chatter of the purchase order because it may contain important information about the import. - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/226/10.0 +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:608df04cfb2ce80c875dde9fba3a7fdd4e4d06c50264907b6a50e1b9021b9b8f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/19.0/purchase_order_import + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-19-0/edi-19-0-purchase_order_import + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides the base wizards and hooks to import supplier +quotation documents and supplier OrderResponse documents on purchase +orders. It provides the generic import workflow; format-specific modules +implement the XML parsers. + +The import wizards accept XML files directly and PDF files containing +embedded XML attachments. For the Universal Business Language (UBL) +format, install: + +- ``purchase_order_import_ubl`` to import supplier quotations. +- ``order_response_import_ubl`` to import supplier OrderResponse + documents. + +On a Request for Quotation, the **Import Quotation File** button opens a +wizard to upload the supplier quotation and choose how the RFQ should be +updated: + +- update only the prices of the draft purchase order from the quotation + file (default option); +- update both prices and quantities. + +When the quotation is imported, Odoo compares it with the current RFQ: + +- lines present in the quotation but missing from the RFQ are created on + the purchase order; +- lines present in the RFQ but missing from the quotation are kept, and + a warning is added to the chatter; +- lines present in both documents are updated when prices or, depending + on the selected option, quantities differ; +- the incoterm is updated when the quotation contains a different + incoterm; +- the imported file is attached to the purchase order. + +After importing a quotation, review the purchase order chatter because +it may contain important warnings or import details. + +OrderResponse imports are matched to a purchase order by reference and +can acknowledge, accept, reject, or conditionally accept the order. +Conditional acceptance updates receipt moves according to line +amendments, including splitting moves, creating backorders, or +cancelling remaining quantities when no backorder is planned. + +**Table of contents** + +.. contents:: + :local: Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= +Authors +------- + +* Akretion + Contributors ------------ -* Alexis de Lattre +- Alexis de Lattre +- Laurent Mignon +- Alexandre Fayolle +- Maksym Yankin -Maintainer ----------- +Maintainers +----------- + +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. +This module is part of the `OCA/edi `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_order_import/__manifest__.py b/purchase_order_import/__manifest__.py index 84ca9bd10e..2fc89ea7be 100644 --- a/purchase_order_import/__manifest__.py +++ b/purchase_order_import/__manifest__.py @@ -3,17 +3,28 @@ { "name": "Purchase Order Import", - "version": "10.0.2.0.0", + "version": "19.0.1.0.0", "category": "Purchase Management", "license": "AGPL-3", "summary": "Update RFQ via the import of quotations from suppliers", "author": "Akretion,Odoo Community Association (OCA)", "website": "https://github.com/OCA/edi", - "depends": ["purchase", "base_business_document_import_stock", "onchange_helper"], + "depends": [ + # Odoo/core + "purchase_stock", + # OCA/edi + "base_business_document_import", + # OCA/reporting-engine + "pdf_xml_attachment", + ], "data": [ - "wizard/order_response_import_view.xml", - "wizard/purchase_order_import_view.xml", - "views/purchase.xml", + # Security + "security/ir.model.access.csv", + # Wizard + "wizard/order_response_import_wizard_view.xml", + "wizard/purchase_order_import_wizard_view.xml", + # Views + "views/purchase_order.xml", ], "installable": True, } diff --git a/purchase_order_import/i18n/es.po b/purchase_order_import/i18n/es.po index 12db9a7b5d..cb193d4282 100644 --- a/purchase_order_import/i18n/es.po +++ b/purchase_order_import/i18n/es.po @@ -81,28 +81,22 @@ msgid "ID" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is a PDF file, Odoo will try to find an XML file in the attachments of " "the PDF file and then use this XML file." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is an XML file, Odoo will parse it if the module that adds support for " "this XML format is installed. For the" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Accepted: confirm the PO and create the picking" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "If the status code is Conditionally accepted: confirm the PO and create the " "picking and update the picking operations according to the amendments " @@ -110,19 +104,16 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Rejected: cancel the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "If the status code is acknowledgement: update the acknowledge datetime on " "the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import OrderResponse File from Supplier" msgstr "" @@ -138,12 +129,10 @@ msgid "Import Quotation File" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import document" msgstr "" @@ -224,7 +213,6 @@ msgid "PO confirmed with amendment by the supplier." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Possible line amendments:" msgstr "" @@ -304,7 +292,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "The order line is accepted with change: The picking operation is modified. " "(split to stock move). If a backorder qty is provided, a backorder is " @@ -312,7 +299,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "The order line is refused: The picking operation is cancelled" msgstr "" @@ -349,12 +335,10 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Then, Odoo will process the related purchase order as follow:" msgstr "" @@ -466,8 +450,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "Universal Business Language" @@ -489,7 +471,6 @@ msgid "Update Option" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Update RFQ" msgstr "" @@ -508,14 +489,12 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "Upload below the OrderResponse you received from your supplier. When you " "click on the import button:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "Upload below the quotation that you received from your supplier for this RFQ " "as XML or PDF file. When you click on the Update RFQ button:" @@ -538,8 +517,6 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for lines that are present both in the imported quotation and in the RFQ, " "Odoo will update the unit prices (and also the quantities, depending on the " @@ -547,28 +524,24 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the RFQ, Odoo will put a warning " "message in the chatter (it won't delete them automatically)." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the quotation (shipping costs for " "example), Odoo will add them to the RFQ," msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "format (UBL), you should install the module order_response_import_ubl." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "format (UBL), you should install the module purchase_order_import_ubl." diff --git a/purchase_order_import/i18n/fr.po b/purchase_order_import/i18n/fr.po index c485a32e43..3c210e28d5 100644 --- a/purchase_order_import/i18n/fr.po +++ b/purchase_order_import/i18n/fr.po @@ -81,28 +81,22 @@ msgid "ID" msgstr "ID" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is a PDF file, Odoo will try to find an XML file in the attachments of " "the PDF file and then use this XML file." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "If it is an XML file, Odoo will parse it if the module that adds support for " "this XML format is installed. For the" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Accepted: confirm the PO and create the picking" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "If the status code is Conditionally accepted: confirm the PO and create the " "picking and update the picking operations according to the amendments " @@ -110,19 +104,16 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Rejected: cancel the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "If the status code is acknowledgement: update the acknowledge datetime on " "the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import OrderResponse File from Supplier" msgstr "" @@ -138,12 +129,10 @@ msgid "Import Quotation File" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import document" msgstr "" @@ -224,7 +213,6 @@ msgid "PO confirmed with amendment by the supplier." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Possible line amendments:" msgstr "" @@ -304,7 +292,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "The order line is accepted with change: The picking operation is modified. " "(split to stock move). If a backorder qty is provided, a backorder is " @@ -312,7 +299,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "The order line is refused: The picking operation is cancelled" msgstr "" @@ -349,12 +335,10 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Then, Odoo will process the related purchase order as follow:" msgstr "" @@ -466,8 +450,6 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "" @@ -489,7 +471,6 @@ msgid "Update Option" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Update RFQ" msgstr "" @@ -508,14 +489,12 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "Upload below the OrderResponse you received from your supplier. When you " "click on the import button:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "Upload below the quotation that you received from your supplier for this RFQ " "as XML or PDF file. When you click on the Update RFQ button:" @@ -538,8 +517,6 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for lines that are present both in the imported quotation and in the RFQ, " "Odoo will update the unit prices (and also the quantities, depending on the " @@ -547,28 +524,24 @@ msgid "" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the RFQ, Odoo will put a warning " "message in the chatter (it won't delete them automatically)." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "for the lines that are present only in the quotation (shipping costs for " "example), Odoo will add them to the RFQ," msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "" "format (UBL), you should install the module order_response_import_ubl." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "" "format (UBL), you should install the module purchase_order_import_ubl." diff --git a/purchase_order_import/i18n/purchase_order_import.pot b/purchase_order_import/i18n/purchase_order_import.pot index 63baa32a1c..3c36a37fad 100644 --- a/purchase_order_import/i18n/purchase_order_import.pot +++ b/purchase_order_import/i18n/purchase_order_import.pot @@ -74,39 +74,30 @@ msgid "ID" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "If it is an XML file, Odoo will parse it if the module that adds support for this XML format is installed. For the" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Accepted: confirm the PO and create the picking" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is Rejected: cancel the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "If the status code is acknowledgement: update the acknowledge datetime on the PO" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import OrderResponse File from Supplier" msgstr "" @@ -122,12 +113,10 @@ msgid "Import Quotation File" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Import Quotations Files from Suppliers" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Import document" msgstr "" @@ -207,7 +196,6 @@ msgid "PO confirmed with amendment by the supplier." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Possible line amendments:" msgstr "" @@ -277,12 +265,10 @@ msgid "The incoterm has been updated from %s to %s upon import of the quotation msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "The order line is refused: The picking operation is cancelled" msgstr "" @@ -311,12 +297,10 @@ msgid "The unit price has been updated on the RFQ line with product '%s' from %s msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Then, Odoo will compare the imported quotation and the current RFQ:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Then, Odoo will process the related purchase order as follow:" msgstr "" @@ -414,8 +398,6 @@ msgid "Unable to conditionally confirm the purchase order. \n" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Universal Business Language" msgstr "" @@ -437,7 +419,6 @@ msgid "Update Option" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Update RFQ" msgstr "" @@ -452,12 +433,10 @@ msgid "Upload an Order response file that you received from your supplier. Suppo msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "Upload below the OrderResponse you received from your supplier. When you click on the import button:" msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "Upload below the quotation that you received from your supplier for this RFQ as XML or PDF file. When you click on the Update RFQ button:" msgstr "" @@ -478,28 +457,22 @@ msgid "You must select a quotation to update." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option)," msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "for the lines that are present only in the RFQ, Odoo will put a warning message in the chatter (it won't delete them automatically)." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "for the lines that are present only in the quotation (shipping costs for example), Odoo will add them to the RFQ," msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.order_response_import_form msgid "format (UBL), you should install the module order_response_import_ubl." msgstr "" #. module: purchase_order_import -#: model:ir.ui.view,arch_db:purchase_order_import.purchase_order_import_form msgid "format (UBL), you should install the module purchase_order_import_ubl." msgstr "" diff --git a/purchase_order_import/readme/CONTRIBUTORS.md b/purchase_order_import/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..517ec4d96f --- /dev/null +++ b/purchase_order_import/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Alexis de Lattre \<\> +- Laurent Mignon \<\> +- Alexandre Fayolle \<\> +- Maksym Yankin \<\> diff --git a/purchase_order_import/readme/DESCRIPTION.md b/purchase_order_import/readme/DESCRIPTION.md new file mode 100644 index 0000000000..33863b4d5f --- /dev/null +++ b/purchase_order_import/readme/DESCRIPTION.md @@ -0,0 +1,36 @@ +This module provides the base wizards and hooks to import supplier quotation +documents and supplier OrderResponse documents on purchase orders. It provides +the generic import workflow; format-specific modules implement the XML parsers. + +The import wizards accept XML files directly and PDF files containing embedded +XML attachments. For the Universal Business Language (UBL) format, install: + +- `purchase_order_import_ubl` to import supplier quotations. +- `order_response_import_ubl` to import supplier OrderResponse documents. + +On a Request for Quotation, the **Import Quotation File** button opens a wizard +to upload the supplier quotation and choose how the RFQ should be updated: + +- update only the prices of the draft purchase order from the quotation file + (default option); +- update both prices and quantities. + +When the quotation is imported, Odoo compares it with the current RFQ: + +- lines present in the quotation but missing from the RFQ are created on the + purchase order; +- lines present in the RFQ but missing from the quotation are kept, and a + warning is added to the chatter; +- lines present in both documents are updated when prices or, depending on the + selected option, quantities differ; +- the incoterm is updated when the quotation contains a different incoterm; +- the imported file is attached to the purchase order. + +After importing a quotation, review the purchase order chatter because it may +contain important warnings or import details. + +OrderResponse imports are matched to a purchase order by reference and can +acknowledge, accept, reject, or conditionally accept the order. Conditional +acceptance updates receipt moves according to line amendments, including +splitting moves, creating backorders, or cancelling remaining quantities when no +backorder is planned. diff --git a/purchase_order_import/security/ir.model.access.csv b/purchase_order_import/security/ir.model.access.csv new file mode 100644 index 0000000000..5eaa41197c --- /dev/null +++ b/purchase_order_import/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_order_response_import_wizard,access.purchase.order.response.import.wizard,purchase_order_import.model_purchase_order_response_import_wizard,base.group_user,1,1,1,0 +access_purchase_order_import_wizard,access.purchase.order.import.wizard,purchase_order_import.model_purchase_order_import_wizard,base.group_user,1,1,1,0 diff --git a/purchase_order_import/static/description/index.html b/purchase_order_import/static/description/index.html new file mode 100644 index 0000000000..54ca9ae75c --- /dev/null +++ b/purchase_order_import/static/description/index.html @@ -0,0 +1,470 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Purchase Order Import

+ +

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

This module provides the base wizards and hooks to import supplier +quotation documents and supplier OrderResponse documents on purchase +orders. It provides the generic import workflow; format-specific modules +implement the XML parsers.

+

The import wizards accept XML files directly and PDF files containing +embedded XML attachments. For the Universal Business Language (UBL) +format, install:

+
    +
  • purchase_order_import_ubl to import supplier quotations.
  • +
  • order_response_import_ubl to import supplier OrderResponse +documents.
  • +
+

On a Request for Quotation, the Import Quotation File button opens a +wizard to upload the supplier quotation and choose how the RFQ should be +updated:

+
    +
  • update only the prices of the draft purchase order from the quotation +file (default option);
  • +
  • update both prices and quantities.
  • +
+

When the quotation is imported, Odoo compares it with the current RFQ:

+
    +
  • lines present in the quotation but missing from the RFQ are created on +the purchase order;
  • +
  • lines present in the RFQ but missing from the quotation are kept, and +a warning is added to the chatter;
  • +
  • lines present in both documents are updated when prices or, depending +on the selected option, quantities differ;
  • +
  • the incoterm is updated when the quotation contains a different +incoterm;
  • +
  • the imported file is attached to the purchase order.
  • +
+

After importing a quotation, review the purchase order chatter because +it may contain important warnings or import details.

+

OrderResponse imports are matched to a purchase order by reference and +can acknowledge, accept, reject, or conditionally accept the order. +Conditional acceptance updates receipt moves according to line +amendments, including splitting moves, creating backorders, or +cancelling remaining quantities when no backorder is planned.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/edi project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/purchase_order_import/tests/__init__.py b/purchase_order_import/tests/__init__.py index 315c6f8bdb..e8b554df68 100644 --- a/purchase_order_import/tests/__init__.py +++ b/purchase_order_import/tests/__init__.py @@ -1 +1 @@ -from . import test_order_response_import +from . import test_order_response_import_wizard diff --git a/purchase_order_import/tests/common.py b/purchase_order_import/tests/common.py new file mode 100644 index 0000000000..dc50c32c95 --- /dev/null +++ b/purchase_order_import/tests/common.py @@ -0,0 +1,133 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import Command, fields + +from odoo.addons.base.tests.common import BaseCommon + + +class TestOrderResponseImportCommon(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.OrderResponseImport = cls.env["purchase.order.response.import.wizard"] + cls.env.user.company_id.partner_id.vat = "BE0421801233" + cls.currency_euro = cls._enable_currency("EUR") + cls.currency_usd = cls._enable_currency("USD") + cls.product_uom_unit = cls.env.ref("uom.product_uom_unit") + cls.supplier = cls._create_supplier() + cls.product_1 = cls._create_product("Product 1", "P1") + cls.product_2 = cls._create_product("Product 2", "P2") + cls.purchase_order = cls._create_purchase_order() + cls.line1 = cls._create_purchase_order_line( + cls.purchase_order, + cls.product_1, + qty=10, + price_unit=15, + ) + cls.line2 = cls._create_purchase_order_line( + cls.purchase_order, + cls.product_2, + qty=5, + price_unit=25, + ) + + @classmethod + def _create_supplier(cls): + return cls.env["res.partner"].create( + { + "name": "Order Response Supplier", + "supplier_rank": 1, + "vat": "BE0477472701", + } + ) + + @classmethod + def _create_product(cls, name, product_code): + return cls.env["product.product"].create( + { + "name": name, + "is_storable": True, + "seller_ids": [ + Command.create( + { + "partner_id": cls.supplier.id, + "product_code": product_code, + }, + ) + ], + } + ) + + @classmethod + def _create_purchase_order(cls): + return cls.env["purchase.order"].create( + { + "partner_id": cls.supplier.id, + "date_order": fields.Datetime.now(), + "date_planned": fields.Datetime.now(), + "currency_id": cls.currency_euro.id, + } + ) + + @classmethod + def _create_purchase_order_line( + cls, + order, + product, + qty, + price_unit, + ): + return cls.env["purchase.order.line"].create( + { + "order_id": order.id, + "product_id": product.id, + "name": product.name, + "date_planned": fields.Datetime.now(), + "product_qty": qty, + "product_uom_id": cls.product_uom_unit.id, + "price_unit": price_unit, + } + ) + + def _get_base_data(self, **values): + """Return a normalized parsed order response payload.""" + data = { + "status": "acknowledgement", + "company": {"vat": "BE0421801233"}, + "currency": {"iso": "EUR"}, + "date": "2020-02-04", + "chatter_msg": [], + "lines": [], + "note": "Note1\nNote2", + "time": "22:10:30", + "supplier": {"vat": "BE0477472701"}, + "ref": str(self.purchase_order.name), + } + data.update(values) + return data + + def _order_line_to_data( + self, + order_line, + qty=None, + status="accepted", + backorder_qty=None, + note=None, + ): + """Return parsed order response data for a purchase order line.""" + return { + "status": status, + "backorder_qty": backorder_qty, + "qty": qty if qty is not None else order_line.product_qty, + "note": note, + "line_id": str(order_line.id), + "uom": {"unece_code": order_line.product_uom_id.unece_code}, + } + + def _line_response_data(self, *lines): + """Return conditional acceptance payload with the provided lines.""" + return self._get_base_data( + status="conditionally_accepted", + lines=list(lines), + ) diff --git a/purchase_order_import/tests/test_order_response_import.py b/purchase_order_import/tests/test_order_response_import.py deleted file mode 100644 index 2ed4c74fea..0000000000 --- a/purchase_order_import/tests/test_order_response_import.py +++ /dev/null @@ -1,583 +0,0 @@ -# Copyright 2020 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import _, fields -from odoo.exceptions import UserError -from odoo.tests import SavepointCase - -from ..wizard.order_response_import import ( - LINE_STATUS_ACCEPTED, - LINE_STATUS_AMEND, - LINE_STATUS_REJECTED, - ORDER_RESPONSE_STATUS_ACCEPTED, - ORDER_RESPONSE_STATUS_ACK, - ORDER_RESPONSE_STATUS_CONDITIONAL, - ORDER_RESPONSE_STATUS_REJECTED, -) - - -class TestOrderResponseImportCommon(SavepointCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.supplier = cls.env.ref("base.res_partner_12") - cls.supplier.vat = "BE0477472701" - cls.env.user.company_id.partner_id.vat = "BE0421801233" - cls.currency_euro = cls.env.ref("base.EUR") - cls.currency_usd = cls.env.ref("base.USD") - cls.product_1 = cls.env["product.product"].create( - { - "name": "Product 1", - "seller_ids": [(0, 0, {"name": cls.supplier.id, "product_code": "P1"})], - } - ) - cls.product_2 = cls.env["product.product"].create( - { - "name": "Product 2", - "seller_ids": [(0, 0, {"name": cls.supplier.id, "product_code": "P2"})], - } - ) - cls.purchase_order = cls.env["purchase.order"].create( - { - "partner_id": cls.supplier.id, - "date_order": fields.Datetime.now(), - "date_planned": fields.Datetime.now(), - "currency_id": cls.currency_euro.id, - } - ) - cls.line1 = cls.purchase_order.order_line.create( - { - "order_id": cls.purchase_order.id, - "product_id": cls.product_1.id, - "name": cls.product_2.name, - "date_planned": fields.Datetime.now(), - "product_qty": 10, - "product_uom": cls.env.ref("product.product_uom_unit").id, - "price_unit": 15, - } - ) - cls.line2 = cls.purchase_order.order_line.create( - { - "order_id": cls.purchase_order.id, - "product_id": cls.product_2.id, - "name": cls.product_2.name, - "date_planned": fields.Datetime.now(), - "product_qty": 5, - "product_uom": cls.env.ref("product.product_uom_unit").id, - "price_unit": 25, - } - ) - cls.OrderResponseImport = cls.env["order.response.import"] - - def order_line_to_data( - self, - order_line, - qty=None, - status=LINE_STATUS_ACCEPTED, - backorder_qty=None, - note=None, - ): - return { - "status": status, - "backorder_qty": backorder_qty, - "qty": qty if qty is not None else order_line.product_qty, - "note": note, - "line_id": str(order_line.id), - "uom": {"unece_code": order_line.product_uom.unece_code}, - } - - -class TestOrderResponseImport(TestOrderResponseImportCommon): - def _get_base_data(self): - return { - "status": ORDER_RESPONSE_STATUS_ACK, - "company": {"vat": "BE0421801233"}, - "currency": {"iso": "EUR"}, - "date": "2020-02-04", - "chatter_msg": [], - "lines": [], - "note": "Note1\nNote2", - "time": "22:10:30", - "supplier": {"vat": "BE0477472701"}, - "ref": str(self.purchase_order.name), - } - - def test_01(self): - """ - Data: - Data with unknown PO reference - Test Case: - Process data - Expected result: - UserError is raised - """ - data = self._get_base_data() - data["ref"] = "123456" - with self.assertRaises(UserError) as ue: - self.OrderResponseImport.process_data(data) - self.assertEqual( - ue.exception.name, _("No purchase order found for name 123456.") - ) - - def test_02(self): - """ - Data: - Data with unknown PO status - Test Case: - Process data - Expected result: - UserError is raised - """ - data = self._get_base_data() - data["status"] = "unknown" - with self.assertRaises(UserError) as ue: - self.OrderResponseImport.process_data(data) - self.assertEqual(ue.exception.name, _("Unknown status 'unknown'.")) - - def test_03(self): - """ - Data: - Data with an other currency - Test Case: - Process data - Expected result: - UserError is raised - """ - data = self._get_base_data() - data["currency"] = {"iso": self.currency_usd.name} - with self.assertRaises(UserError) as ue: - self.OrderResponseImport.process_data(data) - self.assertEqual( - ue.exception.name, - _( - "The currency of the imported OrderResponse (USD) is " - "different from the currency of the purchase order (EUR)." - ), - ) - - def test_04(self): - """ - Data: - Data with status ack. - Test Case: - Process data - Expected result: - The ack info is filled - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_ACK - self.assertFalse(self.purchase_order.supplier_ack_dt) - self.OrderResponseImport.process_data(data) - self.assertTrue(self.purchase_order.supplier_ack_dt) - - def test_05(self): - """ - Data: - Data with status accepted - PO not yet confirmed - Test Case: - Process data - Expected result: - PO is confirmed - A picking is created - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_ACCEPTED - self.assertFalse(self.purchase_order.picking_ids) - self.assertEqual(self.purchase_order.state, "draft") - self.OrderResponseImport.process_data(data) - self.assertTrue(self.purchase_order.picking_ids) - self.assertEqual(self.purchase_order.state, "purchase") - - def test_06(self): - """ - Data: - Data with status rejected - PO not yet confirmed - Test Case: - Process data - Expected result: - PO is cancelled - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_REJECTED - self.assertEqual(self.purchase_order.state, "draft") - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "cancel") - - def test_07(self): - """ - Data: - Data with status 'conditionally_accepted' and without lines - Test Case: - Process data - Expected result: - UserError is raised since a all line details must be provided with - this status - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - data["lines"] = [] - with self.assertRaises(UserError) as ue: - self.OrderResponseImport.process_data(data) - expected = ( - _( - "Unable to conditionally confirm the purchase order. \n" - "Line IDS into the parsed document differs from the " - "expected list of order line ids: \n " - "received: []\n" - "expected: %s\n" - ) - % self.purchase_order.order_line.ids - ) - self.assertEqual(ue.exception.name, expected) - - def test_08(self): - """ - Data: - Data with status 'conditionally_accepted' and with a wrong line id - Test Case: - Process data - Expected result: - UserError is raised since a all line details must be provided with - this status - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - data["lines"] = [self.order_line_to_data(self.line1)] - line2 = self.order_line_to_data(self.line2) - line2["line_id"] = "WRONG" - data["lines"].append(line2) - with self.assertRaises(UserError) as ue: - self.OrderResponseImport.process_data(data) - expected = _( - "Unable to conditionally confirm the purchase order. \n" - "Line IDS into the parsed document differs from the " - "expected list of order line ids: \n " - "received: [%s]\n" - "expected: %s\n" - ) % ( - [str(self.line1.id), "WRONG"], - self.purchase_order.order_line.ids, - ) - self.assertEqual(ue.exception.name, expected) - - def test_09(self): - """ - Data: - Data with status 'conditionally_accepted' and all line accepted - Test Case: - Process data - Expected result: - PO is confirmed - A picking is created with one move by po line in state assigned - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - data["lines"] = [ - self.order_line_to_data(self.line1), - self.order_line_to_data(self.line2), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertTrue(self.purchase_order.picking_ids) - self.assertEqual(self.line1.move_ids.state, "assigned") - self.assertEqual(self.line2.move_ids.state, "assigned") - - def test_10(self): - """ - Data: - Data with status 'conditionally_accepted' and one line accepted - and another one rejected - Test Case: - Process data - Expected result: - PO is confirmed - A picking is created with one move by po line - The move linked to the accepted line is in state assigned - The move linked to the rejected line is in state cancel - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - data["lines"] = [ - self.order_line_to_data(self.line1), - self.order_line_to_data( - self.line2, - status=LINE_STATUS_REJECTED, - note="cancel by import", - ), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertTrue(self.purchase_order.picking_ids) - self.assertEqual(self.line1.move_ids.state, "assigned") - self.assertEqual(self.line2.move_ids.state, "cancel") - self.assertEqual(self.line2.move_ids.note, "cancel by import") - - def test_11(self): - """ - Data: - Data with status 'conditionally_accepted' and one line accepted - and another one rejected - Test Case: - Process data - Expected result: - PO is confirmed - A picking is created with one move by po line - The move linked to the accepted line is in state assigned - The move linked to the rejected line is in state cancel - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - data["lines"] = [ - self.order_line_to_data(self.line1), - self.order_line_to_data( - self.line2, - status=LINE_STATUS_REJECTED, - note="cancel by import", - ), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertTrue(self.purchase_order.picking_ids) - self.assertEqual(self.line1.move_ids.state, "assigned") - self.assertEqual(self.line2.move_ids.state, "cancel") - self.assertEqual(self.line2.move_ids.note, "cancel by import") - - def test_12(self): - """ - Data: - Data with status 'conditionally_accepted' - * line1 amended with less qty than ordered and without - backorder qty - * line2 accepted - Test Case: - Process data - Expected result: - PO is confirmed - A picking is created with two moves for the amended line - * line1 move 1 assigned with qty = confirmed qty - * line1 move 2 cancel with qty = expected qty -confirmed qty - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - confirmed_qty = self.line1.product_qty - 3 - data["lines"] = [ - self.order_line_to_data( - self.line1, status=LINE_STATUS_AMEND, qty=confirmed_qty - ), - self.order_line_to_data(self.line2), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertTrue(self.purchase_order.picking_ids) - move_ids = self.line1.move_ids - self.assertEqual(len(move_ids), 2) - self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) - assigned = move_ids.filtered(lambda s: s.state == "assigned") - self.assertEqual(assigned.product_qty, confirmed_qty) - cancel = move_ids.filtered(lambda s: s.state == "cancel") - self.assertEqual(cancel.product_qty, 3) - self.assertEqual(cancel.note, "No backorder planned by the supplier.") - - def test_13(self): - """ - Data: - Data with status 'conditionally_accepted' - * line1 amended with less qty than ordered and with - backorder qty equal to remaining qty - * line2 accepted - Test Case: - Process data - Expected result: - PO is confirmed - One picking is created with two moves - * line1 assigned with qty = confirmed qty - * line2 assigned with qty = confirmed qty - One backorder picking is created with one move - * line1 assigned with qty = expected qty - confirmed qty - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - confirmed_qty = self.line1.product_qty - 3 - data["lines"] = [ - self.order_line_to_data( - self.line1, - status=LINE_STATUS_AMEND, - qty=confirmed_qty, - backorder_qty=3, - note="my note", - ), - self.order_line_to_data(self.line2), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertEqual(len(self.purchase_order.picking_ids), 2) - move_ids = self.line1.move_ids - self.assertEqual(len(move_ids), 2) - self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) - move_confirmed = move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == confirmed_qty - ) - self.assertTrue(move_confirmed) - self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") % "3", - move_confirmed.note, - ) - move_backorder = move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == 3 - ) - self.assertTrue(move_backorder) - self.assertEqual( - move_backorder.picking_id.backorder_id, - move_confirmed.picking_id, - ) - - def test_14(self): - """ - Data: - Data with status 'conditionally_accepted' - * line1 amended with less qty than ordered and with - backorder qty equal to remaining qty - * line2 amended with less qty than ordered and with - backorder qty equal to remaining qty - Test Case: - Process data - Expected result: - PO is confirmed - One picking is created with two moves - * line1 assigned with qty = confirmed qty - * line2 assigned with qty = confirmed qty - One backorder picking is created with two moves - * line1 assigned with qty = expected qty - confirmed qty - * line2 assigned with qty = expected qty - confirmed qty - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - line1_confirmed_qty = self.line1.product_qty - 3 - line2_confirmed_qty = self.line2.product_qty - 3 - data["lines"] = [ - self.order_line_to_data( - self.line1, - status=LINE_STATUS_AMEND, - qty=line1_confirmed_qty, - backorder_qty=3, - note="my note", - ), - self.order_line_to_data( - self.line2, - status=LINE_STATUS_AMEND, - qty=line2_confirmed_qty, - backorder_qty=3, - note="my note", - ), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertEqual(len(self.purchase_order.picking_ids), 2) - # line1 - line1_move_ids = self.line1.move_ids - self.assertEqual(len(line1_move_ids), 2) - self.assertEqual( - sum(line1_move_ids.mapped("product_qty")), self.line1.product_qty - ) - move_confirmed = line1_move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == line1_confirmed_qty - ) - self.assertTrue(move_confirmed) - self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") % "3", - move_confirmed.note, - ) - move_backorder = line1_move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == 3 - ) - self.assertTrue(move_backorder) - self.assertEqual( - move_backorder.picking_id.backorder_id, - move_confirmed.picking_id, - ) - # lin1 - line2_move_ids = self.line2.move_ids - self.assertEqual(len(line2_move_ids), 2) - self.assertEqual( - sum(line2_move_ids.mapped("product_qty")), self.line2.product_qty - ) - move_confirmed = line2_move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == line2_confirmed_qty - ) - self.assertTrue(move_confirmed) - self.assertEqual( - _("my note\n%s items should be delivered into a next delivery.") % "3", - move_confirmed.note, - ) - move_backorder = line2_move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == 3 - ) - self.assertTrue(move_backorder) - self.assertEqual( - move_backorder.picking_id.backorder_id, - move_confirmed.picking_id, - ) - - def test_15(self): - """ - Data: - Data with status 'conditionally_accepted' - * line1 amended with less qty than ordered and with - backorder qty less than the remaining qty - * line2 accepted - Test Case: - Process data - Expected result: - PO is confirmed - One picking is created with three moves - * line1 assigned with qty = confirmed qty - * line1 cancel with qty = qty that will not be delivered - * line2 assigned with qty = confirmed qty - One backorder picking is created with one move - * line1 assigned with qty = planned backorder qty - """ - data = self._get_base_data() - data["status"] = ORDER_RESPONSE_STATUS_CONDITIONAL - confirmed_qty = self.line1.product_qty - 3 - data["lines"] = [ - self.order_line_to_data( - self.line1, - status=LINE_STATUS_AMEND, - qty=confirmed_qty, - backorder_qty=2, - ), - self.order_line_to_data(self.line2), - ] - self.OrderResponseImport.process_data(data) - self.assertEqual(self.purchase_order.state, "purchase") - self.assertEqual(len(self.purchase_order.picking_ids), 2) - move_ids = self.line1.move_ids - self.assertEqual(len(move_ids), 3) - self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) - move_confirmed = move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == confirmed_qty - ) - self.assertTrue(move_confirmed) - self.assertEqual( - _("%s items should be delivered into a next delivery.") % "2", - move_confirmed.note, - ) - move_cancel = move_ids.filtered( - lambda s: s.state == "cancel" and s.product_qty == 1 - ) - self.assertTrue(move_cancel) - self.assertEqual( - _("No backorder planned by the supplier."), - move_cancel.note, - ) - move_backorder = move_ids.filtered( - lambda s: s.state == "assigned" and s.product_qty == 2 - ) - self.assertTrue(move_backorder) - self.assertEqual( - move_backorder.picking_id.backorder_id, - move_confirmed.picking_id, - ) diff --git a/purchase_order_import/tests/test_order_response_import_wizard.py b/purchase_order_import/tests/test_order_response_import_wizard.py new file mode 100644 index 0000000000..c4f294df34 --- /dev/null +++ b/purchase_order_import/tests/test_order_response_import_wizard.py @@ -0,0 +1,271 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError + +from .common import TestOrderResponseImportCommon + + +class TestOrderResponseImportWizard(TestOrderResponseImportCommon): + def test_unknown_purchase_order_reference(self): + """Raise an error when no purchase order matches the response reference.""" + data = self._get_base_data(ref="123456") + with self.assertRaisesRegex( + UserError, + "No purchase order found for name 123456.", + ): + self.OrderResponseImport.process_data(data) + + def test_unknown_status(self): + """Raise an error when the response status is not supported.""" + data = self._get_base_data(status="unknown") + with self.assertRaisesRegex(UserError, "Unknown status 'unknown'."): + self.OrderResponseImport.process_data(data) + + def test_different_currency(self): + """Raise an error when the response currency differs from the order.""" + data = self._get_base_data(currency={"iso": self.currency_usd.name}) + with self.assertRaisesRegex( + UserError, + "The currency of the imported OrderResponse", + ): + self.OrderResponseImport.process_data(data) + + def test_acknowledgement_sets_supplier_acknowledgement_date(self): + """Set the supplier acknowledgement date on acknowledgement.""" + data = self._get_base_data(status="acknowledgement") + self.assertFalse(self.purchase_order.supplier_ack_dt) + self.OrderResponseImport.process_data(data) + self.assertTrue(self.purchase_order.supplier_ack_dt) + + def test_accepted_response_confirms_order(self): + """Confirm the purchase order and create a receipt on acceptance.""" + data = self._get_base_data(status="accepted") + self.assertFalse(self.purchase_order.picking_ids) + self.assertEqual(self.purchase_order.state, "draft") + self.OrderResponseImport.process_data(data) + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.purchase_order.state, "purchase") + + def test_rejected_response_cancels_order(self): + """Cancel the purchase order on rejection.""" + data = self._get_base_data(status="rejected") + self.assertEqual(self.purchase_order.state, "draft") + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "cancel") + + def test_conditional_response_requires_all_lines(self): + """Raise an error when conditional acceptance omits order lines.""" + data = self._line_response_data() + with self.assertRaisesRegex( + UserError, + "Unable to conditionally confirm the purchase order.", + ): + self.OrderResponseImport.process_data(data) + + def test_conditional_response_rejects_wrong_line_id(self): + """Raise an error when conditional acceptance references unknown lines.""" + line2 = self._order_line_to_data(self.line2) + line2["line_id"] = "WRONG" + data = self._line_response_data( + self._order_line_to_data(self.line1), + line2, + ) + with self.assertRaisesRegex( + UserError, + "Unable to conditionally confirm the purchase order.", + ): + self.OrderResponseImport.process_data(data) + + def test_conditional_response_accepts_all_lines(self): + """Confirm the order when all conditional response lines are accepted.""" + data = self._line_response_data( + self._order_line_to_data(self.line1), + self._order_line_to_data(self.line2), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.line1.move_ids.state, "assigned") + self.assertEqual(self.line2.move_ids.state, "assigned") + + def test_conditional_response_rejects_a_line(self): + """Cancel the receipt move for a rejected conditional response line.""" + data = self._line_response_data( + self._order_line_to_data(self.line1), + self._order_line_to_data( + self.line2, + status="rejected", + note="cancel by import", + ), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + self.assertEqual(self.line1.move_ids.state, "assigned") + self.assertEqual(self.line2.move_ids.state, "cancel") + self.assertIn("cancel by import", self.line2.move_ids.description_picking) + + def test_conditional_response_amends_qty_without_backorder(self): + """Cancel the remaining receipt quantity when no backorder is planned.""" + confirmed_qty = self.line1.product_qty - 3 + data = self._line_response_data( + self._order_line_to_data( + self.line1, + status="amend", + qty=confirmed_qty, + ), + self._order_line_to_data(self.line2), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertTrue(self.purchase_order.picking_ids) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 2) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) + assigned = move_ids.filtered(lambda move: move.state == "assigned") + self.assertEqual(assigned.product_qty, confirmed_qty) + cancel = move_ids.filtered(lambda move: move.state == "cancel") + self.assertEqual(cancel.product_qty, 3) + self.assertEqual( + cancel.description_picking, + "No backorder planned by the supplier.", + ) + + def test_conditional_response_amends_qty_with_full_backorder(self): + """Split the remaining receipt quantity into a backorder.""" + confirmed_qty = self.line1.product_qty - 3 + data = self._line_response_data( + self._order_line_to_data( + self.line1, + status="amend", + qty=confirmed_qty, + backorder_qty=3, + note="my note", + ), + self._order_line_to_data(self.line2), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 2) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) + move_confirmed = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertEqual( + "my note\n3 items should be delivered into a next delivery.", + move_confirmed.description_picking, + ) + move_backorder = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == 3 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, + ) + + def test_conditional_response_amends_multiple_lines_with_backorder(self): + """Create one backorder containing amended quantities for several lines.""" + line1_confirmed_qty = self.line1.product_qty - 3 + line2_confirmed_qty = self.line2.product_qty - 3 + data = self._line_response_data( + self._order_line_to_data( + self.line1, + status="amend", + qty=line1_confirmed_qty, + backorder_qty=3, + note="my note", + ), + self._order_line_to_data( + self.line2, + status="amend", + qty=line2_confirmed_qty, + backorder_qty=3, + note="my note", + ), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + self._assert_amended_moves(self.line1, line1_confirmed_qty, 3, "my note") + self._assert_amended_moves(self.line2, line2_confirmed_qty, 3, "my note") + + def test_conditional_response_amends_qty_with_partial_backorder(self): + """Cancel the unplanned part of an amended remaining quantity.""" + confirmed_qty = self.line1.product_qty - 3 + data = self._line_response_data( + self._order_line_to_data( + self.line1, + status="amend", + qty=confirmed_qty, + backorder_qty=2, + ), + self._order_line_to_data(self.line2), + ) + self.OrderResponseImport.process_data(data) + self.assertEqual(self.purchase_order.state, "purchase") + self.assertEqual(len(self.purchase_order.picking_ids), 2) + move_ids = self.line1.move_ids + self.assertEqual(len(move_ids), 3) + self.assertEqual(sum(move_ids.mapped("product_qty")), self.line1.product_qty) + move_confirmed = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == confirmed_qty + ) + self.assertTrue(move_confirmed) + self.assertIn( + "2 items should be delivered into a next delivery.", + move_confirmed.description_picking, + ) + move_cancel = move_ids.filtered( + lambda move: move.state == "cancel" and move.product_qty == 1 + ) + self.assertTrue(move_cancel) + self.assertEqual( + "No backorder planned by the supplier.", + move_cancel.description_picking, + ) + move_backorder = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == 2 + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, + ) + + def _assert_amended_moves( + self, + order_line, + confirmed_qty: float, + backorder_qty: float, + note: str | None = None, + ): + """Assert that amended moves are split between receipt and backorder.""" + move_ids = order_line.move_ids + self.assertEqual(len(move_ids), 2) + self.assertEqual(sum(move_ids.mapped("product_qty")), order_line.product_qty) + move_confirmed = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == confirmed_qty + ) + self.assertTrue(move_confirmed) + expected_note = ( + f"{backorder_qty} items should be delivered into a next delivery." + ) + if note: + expected_note = f"{note}\n{expected_note}" + self.assertEqual( + expected_note, + move_confirmed.description_picking, + ) + move_backorder = move_ids.filtered( + lambda move: move.state == "assigned" and move.product_qty == backorder_qty + ) + self.assertTrue(move_backorder) + self.assertEqual( + move_backorder.picking_id.backorder_id, + move_confirmed.picking_id, + ) diff --git a/purchase_order_import/views/purchase.xml b/purchase_order_import/views/purchase_order.xml similarity index 86% rename from purchase_order_import/views/purchase.xml rename to purchase_order_import/views/purchase_order.xml index 497b05cc23..90d2392013 100644 --- a/purchase_order_import/views/purchase.xml +++ b/purchase_order_import/views/purchase_order.xml @@ -5,7 +5,6 @@ --> - purchase_order_import.purchase.order.form purchase.order @@ -13,8 +12,8 @@ diff --git a/purchase_order_import/wizard/__init__.py b/purchase_order_import/wizard/__init__.py index 5d8ab4abcd..ac38de4c93 100644 --- a/purchase_order_import/wizard/__init__.py +++ b/purchase_order_import/wizard/__init__.py @@ -1,2 +1,2 @@ -from . import purchase_order_import -from . import order_response_import +from . import purchase_order_import_wizard +from . import order_response_import_wizard diff --git a/purchase_order_import/wizard/order_response_import.py b/purchase_order_import/wizard/order_response_import.py deleted file mode 100644 index 7eaf05310c..0000000000 --- a/purchase_order_import/wizard/order_response_import.py +++ /dev/null @@ -1,387 +0,0 @@ -# Copyright 2020 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import logging -import mimetypes - -from lxml import etree - -from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError -from odoo.tools import config, float_compare - -logger = logging.getLogger(__name__) - -ORDER_RESPONSE_STATUS_ACK = "acknowledgement" -ORDER_RESPONSE_STATUS_ACCEPTED = "accepted" -ORDER_RESPONSE_STATUS_REJECTED = "rejected" -ORDER_RESPONSE_STATUS_CONDITIONAL = "conditionally_accepted" - -LINE_STATUS_ACCEPTED = "accepted" -LINE_STATUS_REJECTED = "rejected" -LINE_STATUS_AMEND = "amend" - - -def is_int(val): - try: - int(val) - return True - except ValueError: - return False - - -class OrderResponseImport(models.TransientModel): - _name = "order.response.import" - _description = "Purchase Order Response Import from Files" - - @api.model - def _get_purchase_id(self): - assert self._context["active_model"] == "purchase.order", "bad active_model" - return self.env["purchase.order"].browse(self._context["active_id"]) - - document = fields.Binary( - string="XML or PDF Order response", - required=True, - help="Upload an Order response file that you received from " - "your supplier. Supported formats: XML and PDF " - "(PDF with an embeded XML file).", - ) - filename = fields.Char(string="Filename") - - @api.model - def parse_xml_order_document(self, xml_root): - raise UserError( - _( - "This type of XML Order Response is not supported. Did you " - "install the module to support this XML format?" - ) - ) - - @api.model - def parse_pdf_order_document(self, document): - """ - Get PDF attachments, filter on XML files and call import_order_xml - """ - xml_files_dict = self.get_xml_files_from_pdf(document) - if not xml_files_dict: - raise UserError(_("There are no embedded XML file in this PDF file.")) - for xml_filename, xml_root in xml_files_dict.iteritems(): - logger.info("Trying to parse XML file %s", xml_filename) - try: - parsed_order_document = self.parse_xml_order_document(xml_root) - return parsed_order_document - except: - continue - raise UserError( - _( - "This type of XML Order Document is not supported. Did you " - "install the module to support this XML format?" - ) - ) - - # Format of parsed order response - # { - # 'ref': 'SO01234' # the buyer party identifier - # # (specified into the Order document -> po's name) - # 'supplier': {'vat': 'FR25499247138'}, - # 'company': {'vat': 'FR12123456789'}, # Only used to check we are not - # # importing the quote in the - # # wrong company by mistake - # 'status': 'acknowledgement | accepted | rejected | - # conditionally_accepted' - # 'currency': {'iso': 'EUR', 'symbol': u'€'}, - # 'note': 'some notes', - # 'chatter_msg': ['msg1', 'msg2'] - # 'lines': [{ - # 'id': 123456, - # 'qty': 2.5, - # 'uom': {'unece_code': 'C62'}, - # 'status': 5, - # 'note': 'my note' - # 'backorder_qty: None # if provided and qty != expected - # # the backorder qty will be delivered - # # in a next shipping - # }] - - @api.model - def parse_order_response(self, document, filename): - if not document: - raise UserError(_("Missing document file")) - if not filename: - raise UserError(_("Missing document filename")) - filetype = mimetypes.guess_type(filename)[0] - logger.debug("OrderResponse file mimetype: %s", filetype) - if filetype in ["application/xml", "text/xml"]: - try: - xml_root = etree.fromstring(document) - except: - logger.exception("File is not XML-compliant") - raise UserError(_("This XML file is not XML-compliant")) - if logger.isEnabledFor(logging.DEBUG): - pretty_xml_string = etree.tostring( - xml_root, - pretty_print=True, - encoding="UTF-8", - xml_declaration=True, - ) - logger.debug("Starting to import the following XML file:") - logger.debug(pretty_xml_string) - parsed_order_document = self.parse_xml_order_document(xml_root) - elif filetype == "application/pdf": - parsed_order_document = self.parse_pdf_order_document(document) - else: - raise UserError( - _( - "This file '%s' is not recognised as XML nor PDF file. " - "Please check the file and it's extension." - ) - % filename - ) - logger.debug("Result of OrderResponse parsing: ", parsed_order_document) - if "attachments" not in parsed_order_document: - parsed_order_document["attachments"] = {} - parsed_order_document["attachments"][filename] = document.encode("base64") - if "chatter_msg" not in parsed_order_document: - parsed_order_document["chatter_msg"] = [] - if ( - parsed_order_document.get("company") - and not config["test_enable"] - and not self._context.get("edi_skip_company_check") - ): - self.env["business.document.import"]._check_company( - parsed_order_document["company"], - parsed_order_document["chatter_msg"], - ) - return parsed_order_document - - @api.multi - def process_document(self): - self.ensure_one() - parsed_order_document = self.parse_order_response( - self.document.decode("base64"), self.filename - ) - self.process_data(parsed_order_document) - - @api.model - def process_data(self, parsed_order_document): - bdio = self.env["business.document.import"] - po_name = parsed_order_document.get("ref") - order = self.env["purchase.order"].search([("name", "=", po_name)]) - if not order: - self.env["business.document.import"].user_error_wrap( - _("No purchase order found for name %s.") % po_name - ) - - currency = bdio._match_currency( - parsed_order_document.get("currency"), - parsed_order_document["chatter_msg"], - ) - partner = bdio._match_partner( - parsed_order_document["supplier"], - parsed_order_document["chatter_msg"], - partner_type="supplier", - ) - if partner.commercial_partner_id != order.partner_id.commercial_partner_id: - bdio.user_error_wrap( - _( - "The supplier of the imported OrderResponse (%s) " - "is different from the supplier of the purchase order " - "(%s)." - % ( - partner.commercial_partner_id.name, - order.partner_id.commercial_partner_id.name, - ) - ) - ) - if currency and currency != order.currency_id: - bdio.user_error_wrap( - _( - "The currency of the imported OrderResponse (%s) " - "is different from the currency of the purchase order " - "(%s)." - ) - % (currency.name, order.currency_id.name) - ) - - status = parsed_order_document.get("status") - if status == ORDER_RESPONSE_STATUS_ACK: - self._process_ack(order, parsed_order_document) - elif status == ORDER_RESPONSE_STATUS_REJECTED: - self._process_rejected(order, parsed_order_document) - elif status == ORDER_RESPONSE_STATUS_ACCEPTED: - self._process_accepted(order, parsed_order_document) - elif status == ORDER_RESPONSE_STATUS_CONDITIONAL: - self._process_conditional(order, parsed_order_document) - else: - bdio.user_error_wrap(_("Unknown status '%s'.") % status) - - bdio.post_create_or_update(parsed_order_document, order) - logger.info( - "purchase.order ID %d updated via import of file %s.", - order.id, - self.filename, - ) - order.message_post( - _( - "This purchase order has been updated automatically" - " via the import of OrderResponse file %s." - ) - % self.filename - ) - return order.get_formview_action() - - @api.model - def _process_ack(self, purchase_order, parsed_order_document): - if not purchase_order.supplier_ack_dt: - purchase_order.supplier_ack_dt = fields.Datetime.now() - - @api.model - def _process_rejected(self, purchase_order, parsed_order_document): - parsed_order_document["chatter_msg"] = ( - parsed_order_document["chatter_msg"] or [] - ) - parsed_order_document["chatter_msg"].append(_("PO cancelled by the supplier.")) - purchase_order.button_cancel() - - @api.model - def _process_accepted(self, purchase_order, parsed_order_document): - parsed_order_document["chatter_msg"] = ( - parsed_order_document["chatter_msg"] or [] - ) - parsed_order_document["chatter_msg"].append(_("PO confirmed by the supplier.")) - purchase_order.button_approve() - - @api.model - def _process_conditional(self, purchase_order, parsed_order_document): - precision = self.env["decimal.precision"].precision_get( - "Product Unit of Measure" - ) - chatter = parsed_order_document["chatter_msg"] = ( - parsed_order_document["chatter_msg"] or [] - ) - chatter.append(_("PO confirmed with amendment by the supplier.")) - lines = parsed_order_document["lines"] - line_ids = [int(l["line_id"]) for l in lines if is_int(l["line_id"])] - if set(line_ids) != set(purchase_order.order_line.ids): - self.env["business.document.import"].user_error_wrap( - _( - "Unable to conditionally confirm the purchase order. \n" - "Line IDS into the parsed document differs from the " - "expected list of order line ids: \n " - "received: %s\n" - "expected: %s\n" - ) - % ( - [l["line_id"] for l in lines], - purchase_order.order_line.ids, - ) - ) - return - purchase_order.button_approve() - # apply changes to the created moves... - lines_by_id = {int(l["line_id"]): l for l in lines} - for order_line in purchase_order.order_line: - line_info = lines_by_id[order_line.id] - note = line_info.get("note") - move = order_line.move_ids.filtered( - lambda x: x.state not in ("cancel", "done") - ) - if len(move) != 1: - self.env["business.document.import"].user_error_wrap( - _( - "More than one move found for PO line.\n" - "Move IDs: %s\n" - "Line Info: %s" - ) - % (move.ids, line_info) - ) - if note: - move.write({"note": note}) - status = line_info["status"] - if status == LINE_STATUS_ACCEPTED: - continue - if status == LINE_STATUS_REJECTED: - order_line.move_ids.action_cancel() - elif status == LINE_STATUS_AMEND: - qty = line_info["qty"] - backorder_qty = line_info["backorder_qty"] - move_qty = move.product_qty - if float_compare(qty, move_qty, precision_digits=precision) < 0: - self._check_picking_status(move.picking_id) - new_move_id = move.split(move_qty - qty) - new_move = move.browse(new_move_id) - to_cancel = None - if backorder_qty: - note = note + "\n" if note else "" - note += ( - _("%s items should be delivered into a next delivery.") - % backorder_qty - ) - move.note = note - # if the backorder qty is < than the remaining qty - # split and cancel the qty that will not be delivered - if ( - float_compare( - backorder_qty, - new_move.product_qty, - precision_digits=precision, - ) - < 0 - ): - to_cancel_id = new_move.split( - new_move.product_qty - backorder_qty - ) - to_cancel = move.browse(to_cancel_id) - else: - to_cancel = new_move - if to_cancel: - to_cancel.action_cancel() - to_cancel.write( - {"note": _("No backorder planned by the supplier.")} - ) - if new_move.state != "cancel": - # move the new move into an backorder picking to avoid - # that the scheduler merge the two moves into the same - # pack operation - self._add_move_to_backorder(new_move) - - # Reset Operations - move.picking_id.do_prepare_partial() - - @api.model - def _add_move_to_backorder(self, move): - """ - Add the move the picking's backorder - return the backorder associated to the current picking. If no backorder - exists, create a new one. - :param move: - """ - StockPicking = self.env["stock.picking"] - current_picking = move.picking_id - backorder = StockPicking.search([("backorder_id", "=", current_picking.id)]) - if not backorder: - date_done = current_picking.date_done - move.picking_id._create_backorder(backorder_moves=move) - # preserve date_done.... - current_picking.date_done = date_done - else: - move.write({"picking_id": backorder.id}) - backorder.action_confirm() - backorder.action_assign() - - @api.model - def _check_picking_status(self, picking): - """ - The picking operations have already begun - :param picking: - :return: - """ - if any(operation.qty_done != 0 for operation in picking.pack_operation_ids): - raise ValidationError( - _( - "Some Pack Operations have already started! " - "Please validate or reset operations on " - "picking %s to ensure delivery slip to be computed." - ) - % picking.name - ) diff --git a/purchase_order_import/wizard/order_response_import_wizard.py b/purchase_order_import/wizard/order_response_import_wizard.py new file mode 100644 index 0000000000..6b67bd61af --- /dev/null +++ b/purchase_order_import/wizard/order_response_import_wizard.py @@ -0,0 +1,453 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import mimetypes +from base64 import b64decode, b64encode +from typing import Any + +from lxml import etree + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.fields import Domain + +logger = logging.getLogger(__name__) + + +class OrderResponseImportWizard(models.TransientModel): + _name = "purchase.order.response.import.wizard" + _description = "Purchase Order Response Import from Files" + + document = fields.Binary( + string="XML or PDF Order response", + required=True, + help="Upload an Order response file that you received from " + "your supplier. Supported formats: XML and PDF " + "(PDF with an embeded XML file).", + ) + filename = fields.Char() + + @api.model + def parse_xml_order_document(self, xml_root: etree._Element) -> dict[str, Any]: + """Parse an XML order response document. + + The hook method to be implemented by other modules supporting + specific XML formats. It should return the parsed document in a + normalized dictionary format. + """ + raise UserError( + self.env._( + "This type of XML Order Response is not supported. Did you " + "install the module to support this XML format?" + ) + ) + + @api.model + def parse_pdf_order_document(self, document: bytes) -> dict[str, Any]: + """Get PDF attachments, filter on XML files and call import_order_xml.""" + xml_files_dict = self.env["pdf.xml.tool"].pdf_get_xml_files(document) + if not xml_files_dict: + raise UserError( + self.env._("There are no embedded XML files in this PDF file.") + ) + for xml_filename, xml_root in xml_files_dict.items(): + logger.info("Trying to parse XML file %s", xml_filename) + try: + return self.parse_xml_order_document(xml_root) + except UserError: + continue + raise UserError( + self.env._( + "This type of XML Order Document is not supported. Did you " + "install the module to support this XML format?" + ) + ) + + # Format of parsed order response + # { + # 'ref': 'SO01234' # the buyer party identifier + # # (specified into the Order document -> po's name) + # 'supplier': {'vat': 'FR25499247138'}, + # 'company': {'vat': 'FR12123456789'}, # Only used to check we are not + # # importing the quote in the + # # wrong company by mistake + # 'status': 'acknowledgement | accepted | rejected | + # conditionally_accepted' + # 'currency': {'iso': 'EUR', 'symbol': u'€'}, + # 'note': 'some notes', + # 'chatter_msg': ['msg1', 'msg2'] + # 'lines': [{ + # 'id': 123456, + # 'qty': 2.5, + # 'uom': {'unece_code': 'C62'}, + # 'status': 5, + # 'note': 'my note' + # 'backorder_qty: None # if provided and qty != expected + # # the backorder qty will be delivered + # # in a next shipping + # }] + + @api.model + def parse_order_response(self, document: bytes, filename: str) -> dict[str, Any]: + """Parse an uploaded order response file into normalized import data.""" + if not document: + raise UserError(self.env._("Missing document file.")) + if not filename: + raise UserError(self.env._("Missing document filename.")) + filetype = mimetypes.guess_type(filename)[0] + logger.debug("OrderResponse file mimetype: %s", filetype) + if filetype in ["application/xml", "text/xml"]: + try: + xml_root = etree.fromstring(document) + except etree.XMLSyntaxError as e: + logger.exception("File is not XML-compliant") + raise UserError( + self.env._("This XML file is not XML-compliant.") + ) from e + if logger.isEnabledFor(logging.DEBUG): + pretty_xml_string = etree.tostring( + xml_root, + pretty_print=True, + encoding="UTF-8", + xml_declaration=True, + ) + logger.debug("Starting to import the following XML file:") + logger.debug(pretty_xml_string) + parsed_order_document = self.parse_xml_order_document(xml_root) + elif filetype == "application/pdf": + parsed_order_document = self.parse_pdf_order_document(document) + else: + raise UserError( + self.env._( + "This file '%(filename)s' is not recognised as XML nor PDF file. " + "Please check the file and it's extension.", + filename=filename, + ) + ) + logger.debug("Result of OrderResponse parsing: %s", parsed_order_document) + if "attachments" not in parsed_order_document: + parsed_order_document["attachments"] = {} + parsed_order_document["attachments"][filename] = b64encode(document).decode() + if "chatter_msg" not in parsed_order_document: + parsed_order_document["chatter_msg"] = [] + if parsed_order_document.get("company") and not self.env.context.get( + "edi_skip_company_check" + ): + self.env["business.document.import"]._check_company( + parsed_order_document["company"], + parsed_order_document["chatter_msg"], + ) + return parsed_order_document + + def process_document(self) -> dict[str, Any]: + """Process the uploaded document from the import wizard.""" + self.ensure_one() + parsed_order_document = self.parse_order_response( + b64decode(self.document), self.filename + ) + return self.process_data(parsed_order_document) + + @api.model + def process_data(self, parsed_order_document: dict[str, Any]) -> dict[str, Any]: + """Apply parsed order response data to its matching purchase order.""" + bdio = self.env["business.document.import"] + po_name = parsed_order_document.get("ref") + order = self.env["purchase.order"].search([Domain("name", "=", po_name)]) + if not order: + bdio.user_error_wrap( + "process_data", + parsed_order_document, + self.env._( + "No purchase order found for name %(po_name)s.", + po_name=po_name, + ), + parsed_order_document["chatter_msg"], + True, + ) + + currency = bdio._match_currency( + parsed_order_document.get("currency"), + parsed_order_document["chatter_msg"], + ) + partner = bdio._match_partner( + parsed_order_document["supplier"], + parsed_order_document["chatter_msg"], + partner_type="supplier", + ) + if partner.commercial_partner_id != order.partner_id.commercial_partner_id: + bdio.user_error_wrap( + "process_data", + parsed_order_document, + self.env._( + "The supplier of the imported OrderResponse (%(supplier)s) " + "is different from the supplier of the purchase order " + "(%(order_supplier)s).", + supplier=partner.commercial_partner_id.name, + order_supplier=order.partner_id.commercial_partner_id.name, + ), + parsed_order_document["chatter_msg"], + True, + ) + if currency and currency != order.currency_id: + bdio.user_error_wrap( + "process_data", + parsed_order_document, + self.env._( + "The currency of the imported OrderResponse (%(currency)s) " + "is different from the currency of the purchase order " + "(%(order_currency)s).", + currency=currency.name, + order_currency=order.currency_id.name, + ), + parsed_order_document["chatter_msg"], + True, + ) + + status = parsed_order_document.get("status") + if status == "acknowledgement": + self._process_ack(order, parsed_order_document) + elif status == "rejected": + self._process_rejected(order, parsed_order_document) + elif status == "accepted": + self._process_accepted(order, parsed_order_document) + elif status == "conditionally_accepted": + self._process_conditional(order, parsed_order_document) + else: + bdio.user_error_wrap( + "process_data", + parsed_order_document, + self.env._("Unknown status '%(status)s'.", status=status), + parsed_order_document["chatter_msg"], + True, + ) + + bdio.post_create_or_update(parsed_order_document, order) + logger.info( + "purchase.order ID %d updated via import of file %s.", + order.id, + self.filename, + ) + order.message_post( + body=self.env._( + "This purchase order has been updated automatically via the import " + "of OrderResponse file %(filename)s.", + filename=self.filename, + ) + ) + return order.get_formview_action() + + @api.model + def _process_ack( + self, purchase_order: models.Model, parsed_order_document: dict[str, Any] + ): + """Store the supplier acknowledgement date on the purchase order.""" + if not purchase_order.supplier_ack_dt: + purchase_order.supplier_ack_dt = fields.Datetime.now() + + @api.model + def _process_rejected( + self, purchase_order: models.Model, parsed_order_document: dict[str, Any] + ): + """Cancel the purchase order rejected by the supplier.""" + parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + parsed_order_document["chatter_msg"].append( + self.env._("PO cancelled by the supplier.") + ) + purchase_order.button_cancel() + + @api.model + def _process_accepted( + self, purchase_order: models.Model, parsed_order_document: dict[str, Any] + ): + """Confirm the purchase order accepted by the supplier.""" + parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + parsed_order_document["chatter_msg"].append( + self.env._("PO confirmed by the supplier.") + ) + purchase_order.button_approve() + + @api.model + def _process_conditional( + self, purchase_order: models.Model, parsed_order_document: dict[str, Any] + ): + """Confirm an amended order response and synchronize receipt moves. + + A conditional supplier acceptance must describe every PO line. Accepted + lines keep their receipt move, rejected lines cancel it, and amended + lines split the receipt between accepted, backordered, and cancelled + quantities according to the supplier response. + """ + chatter = parsed_order_document["chatter_msg"] = ( + parsed_order_document["chatter_msg"] or [] + ) + chatter.append(self.env._("PO confirmed with amendment by the supplier.")) + lines = parsed_order_document["lines"] + lines_by_id = self._get_conditional_lines_by_id( + purchase_order, parsed_order_document, lines, chatter + ) + if lines_by_id is None: + return + purchase_order.button_approve() + for order_line in purchase_order.order_line: + self._process_conditional_line( + order_line, + lines_by_id[order_line.id], + parsed_order_document, + chatter, + ) + + @api.model + def _get_conditional_lines_by_id( + self, + purchase_order: models.Model, + parsed_order_document: dict[str, Any], + lines: list[dict[str, Any]], + chatter: list[str], + ) -> dict[int, dict[str, Any]] | None: + """Return conditional response lines keyed by PO line id.""" + try: + line_ids = {int(line["line_id"]) for line in lines} + except (KeyError, TypeError, ValueError): + line_ids = set() + if line_ids != set(purchase_order.order_line.ids): + self.env["business.document.import"].user_error_wrap( + "_process_conditional", + parsed_order_document, + self.env._( + "Unable to conditionally confirm the purchase order. \n" + "Line IDS into the parsed document differs from the " + "expected list of order line ids: \n " + "received: %(received_line_ids)s\n" + "expected: %(expected_line_ids)s\n", + received_line_ids=[line.get("line_id") for line in lines], + expected_line_ids=purchase_order.order_line.ids, + ), + chatter, + True, + ) + return None + return {int(line["line_id"]): line for line in lines} + + @api.model + def _process_conditional_line( + self, + order_line: models.Model, + line_info: dict[str, Any], + parsed_order_document: dict[str, Any], + chatter: list[str], + ): + """Apply one conditional response line to its receipt move.""" + note = line_info.get("note") + move = order_line.move_ids.filtered(lambda x: x.state not in ("cancel", "done")) + if len(move) != 1: + self.env["business.document.import"].user_error_wrap( + "_process_conditional", + parsed_order_document, + self.env._( + "More than one move found for PO line.\n" + "Move IDs: %(move_ids)s\n" + "Line Info: %(line_info)s", + move_ids=move.ids, + line_info=line_info, + ), + chatter, + True, + ) + if note: + move.write({"description_picking": note}) + status = line_info["status"] + if status == "accepted": + return + if status == "rejected": + order_line.move_ids._action_cancel() + elif status == "amend": + self._process_amended_move(move, line_info, note) + + @api.model + def _process_amended_move( + self, move: models.Model, line_info: dict[str, Any], note: str | None + ): + """Split amended receipt quantities into kept, backordered, and cancelled.""" + qty = line_info["qty"] + backorder_qty = line_info["backorder_qty"] + move_qty = move.product_qty + if move.product_uom.compare(qty, move_qty) >= 0: + return + self._check_picking_status(move.picking_id) + new_move = self._split_move(move, move_qty - qty) + to_cancel = None + if backorder_qty: + self._add_backorder_note(move, note, backorder_qty) + if new_move.product_uom.compare(backorder_qty, new_move.product_qty) < 0: + to_cancel = self._split_move( + new_move, new_move.product_qty - backorder_qty + ) + else: + to_cancel = new_move + if to_cancel: + to_cancel._action_cancel() + to_cancel.write( + { + "description_picking": self.env._( + "No backorder planned by the supplier." + ) + } + ) + if new_move.state != "cancel": + self._add_move_to_backorder(new_move) + move.picking_id.action_assign() + + @api.model + def _add_backorder_note( + self, move: models.Model, note: str | None, backorder_qty: float + ): + """Append backorder information on the confirmed receipt move.""" + note = note + "\n" if note else "" + move.description_picking = note + self.env._( + "%(qty)s items should be delivered into a next delivery.", + qty=backorder_qty, + ) + + @api.model + def _split_move(self, move: models.Model, qty: float) -> models.Model: + """Split a stock move and create the new move with modern stock APIs.""" + new_move_vals = move._split(qty) + new_move = self.env["stock.move"].create(new_move_vals) + new_move._action_confirm(merge=False, create_proc=False) + return new_move + + @api.model + def _add_move_to_backorder(self, move: models.Model): + """Move a split stock move to the receipt backorder.""" + StockPicking = self.env["stock.picking"] + current_picking = move.picking_id + backorder = StockPicking.search( + [Domain("backorder_id", "=", current_picking.id)] + ) + if not backorder: + date_done = current_picking.date_done + move.picking_id._create_backorder(backorder_moves=move) + # preserve date_done.... + current_picking.date_done = date_done + else: + move.write({"picking_id": backorder.id}) + backorder.action_confirm() + backorder.action_assign() + + @api.model + def _check_picking_status(self, picking: models.Model): + """Block amendments when receipt operations have already started.""" + if any(move_line.picked for move_line in picking.move_line_ids): + raise ValidationError( + self.env._( + "Some operations have already started! " + "Please validate or reset operations on " + "picking %(picking)s to ensure delivery slip to be computed.", + picking=picking.name, + ) + ) diff --git a/purchase_order_import/wizard/order_response_import_view.xml b/purchase_order_import/wizard/order_response_import_wizard_view.xml similarity index 66% rename from purchase_order_import/wizard/order_response_import_view.xml rename to purchase_order_import/wizard/order_response_import_wizard_view.xml index e87a93c1ef..813e61d74b 100644 --- a/purchase_order_import/wizard/order_response_import_view.xml +++ b/purchase_order_import/wizard/order_response_import_wizard_view.xml @@ -2,9 +2,9 @@ - - order.response.import.form - order.response.import + + purchase.order.response.import.wizard.form + purchase.order.response.import.wizard
@@ -22,7 +22,7 @@ >If it is a PDF file, Odoo will try to find an XML file in the attachments of the PDF file and then use this XML file.

Then, Odoo will process the related purchase order as follow:

+ >Then, Odoo will process the related purchase order as follows:

  • If the status code is acknowledgement: update the acknowledge datetime on the PO
  • @@ -30,21 +30,19 @@
  • If the status code is Accepted: confirm the PO and create the picking
  • If the status code is Conditionally accepted: confirm the PO and create the picking and update the picking operations according to the amendments specified into the document
  • -
  • for lines that are present both in the imported quotation and in the RFQ, Odoo will update the unit prices (and also the quantities, depending on the chosen option),
  • + >If the status code is Conditionally accepted: confirm the PO, create the picking, and update the receipt moves according to the amendments specified in the document

Possible line amendments:

  • The order line is refused: The picking operation is cancelled
  • + >The order line is refused: The receipt move is cancelled
  • The order line is accepted with change: The picking operation is modified. (split to stock move). If a backorder qty is provided, a backorder is created. Otherwise the stock.move for the missing quantities is cancelled.
  • + >The order line is accepted with change: The receipt move is split. If a backorder quantity is provided, a backorder is created. Otherwise the move for the remaining quantity is cancelled.
- +