From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/l10n_it_stock_ddt/__init__.py | 17 ++ addons/l10n_it_stock_ddt/__manifest__.py | 35 ++++ .../data/l10n_it_ddt_template.xml | 20 +++ addons/l10n_it_stock_ddt/models/__init__.py | 5 + addons/l10n_it_stock_ddt/models/account_invoice.py | 85 ++++++++++ addons/l10n_it_stock_ddt/models/stock_picking.py | 71 +++++++++ .../report/l10n_it_ddt_report.xml | 177 +++++++++++++++++++++ addons/l10n_it_stock_ddt/tests/__init__.py | 2 + addons/l10n_it_stock_ddt/tests/test_ddt.py | 90 +++++++++++ .../views/account_invoice_views.xml | 17 ++ .../views/stock_picking_views.xml | 50 ++++++ 11 files changed, 569 insertions(+) create mode 100644 addons/l10n_it_stock_ddt/__init__.py create mode 100644 addons/l10n_it_stock_ddt/__manifest__.py create mode 100644 addons/l10n_it_stock_ddt/data/l10n_it_ddt_template.xml create mode 100644 addons/l10n_it_stock_ddt/models/__init__.py create mode 100644 addons/l10n_it_stock_ddt/models/account_invoice.py create mode 100644 addons/l10n_it_stock_ddt/models/stock_picking.py create mode 100644 addons/l10n_it_stock_ddt/report/l10n_it_ddt_report.xml create mode 100644 addons/l10n_it_stock_ddt/tests/__init__.py create mode 100644 addons/l10n_it_stock_ddt/tests/test_ddt.py create mode 100644 addons/l10n_it_stock_ddt/views/account_invoice_views.xml create mode 100644 addons/l10n_it_stock_ddt/views/stock_picking_views.xml (limited to 'addons/l10n_it_stock_ddt') diff --git a/addons/l10n_it_stock_ddt/__init__.py b/addons/l10n_it_stock_ddt/__init__.py new file mode 100644 index 00000000..b75ff7a4 --- /dev/null +++ b/addons/l10n_it_stock_ddt/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from . import models +from odoo import api, SUPERUSER_ID + + +def _create_picking_seq(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + ptypes = env['stock.picking.type'].search([('code', '=', 'outgoing'), ('warehouse_id', '!=', False)]) + for ptype in ptypes: + wh = ptype.warehouse_id + ptype.l10n_it_ddt_sequence_id = env['ir.sequence'].create({ + 'name': wh.name + ' ' + ' Sequence ' + ' ' + ptype.sequence_code, + 'prefix': wh.code + '/' + ptype.sequence_code + '/DDT', 'padding': 5, + 'company_id': wh.company_id.id, + 'implementation': 'no_gap', + }).id \ No newline at end of file diff --git a/addons/l10n_it_stock_ddt/__manifest__.py b/addons/l10n_it_stock_ddt/__manifest__.py new file mode 100644 index 00000000..e60decd0 --- /dev/null +++ b/addons/l10n_it_stock_ddt/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +{ + 'name': "l10n_it_stock_ddt", + 'website': 'https://www.odoo.com', + 'category': 'Accounting/Localizations/EDI', + 'version': '0.1', + 'description': """ +Documento di Trasporto (DDT) + +Whenever goods are transferred between A and B, the DDT serves +as a legitimation e.g. when the police would stop you. + +When you want to print an outgoing picking in an Italian company, +it will print you the DDT instead. It is like the delivery +slip, but it also contains the value of the product, +the transportation reason, the carrier, ... which make it a DDT. + +We also use a separate sequence for the DDT as the number should not +have any gaps and should only be applied at the moment the goods are sent. + +When invoices are related to their sale order and the sale order with the +delivery, the system will automatically calculate the linked DDTs for every +invoice line to export in the FatturaPA XML. + """, + 'depends': ['l10n_it_edi', 'delivery'], + 'data': [ + 'report/l10n_it_ddt_report.xml', + 'views/stock_picking_views.xml', + 'views/account_invoice_views.xml', + 'data/l10n_it_ddt_template.xml', + ], + 'auto_install': True, + 'post_init_hook': '_create_picking_seq', + 'license': 'LGPL-3', +} diff --git a/addons/l10n_it_stock_ddt/data/l10n_it_ddt_template.xml b/addons/l10n_it_stock_ddt/data/l10n_it_ddt_template.xml new file mode 100644 index 00000000..259bba48 --- /dev/null +++ b/addons/l10n_it_stock_ddt/data/l10n_it_ddt_template.xml @@ -0,0 +1,20 @@ + + + + diff --git a/addons/l10n_it_stock_ddt/models/__init__.py b/addons/l10n_it_stock_ddt/models/__init__.py new file mode 100644 index 00000000..08842da1 --- /dev/null +++ b/addons/l10n_it_stock_ddt/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import stock_picking +from . import account_invoice \ No newline at end of file diff --git a/addons/l10n_it_stock_ddt/models/account_invoice.py b/addons/l10n_it_stock_ddt/models/account_invoice.py new file mode 100644 index 00000000..b7713b27 --- /dev/null +++ b/addons/l10n_it_stock_ddt/models/account_invoice.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, api, fields, _ +from odoo.tools.float_utils import float_compare + + +class AccountMove(models.Model): + _inherit = 'account.move' + + l10n_it_ddt_ids = fields.Many2many('stock.picking', compute="_compute_ddt_ids") + l10n_it_ddt_count = fields.Integer(compute="_compute_ddt_ids") + + def _get_ddt_values(self): + """ + We calculate the link between the invoice lines and the deliveries related to the invoice through the + links with the sale order(s). We assume that the first picking was invoiced first. (FIFO) + :return: a dictionary with as key the picking and value the invoice line numbers (by counting) + """ + self.ensure_one() + # We don't consider returns/credit notes as we suppose they will lead to more deliveries/invoices as well + if self.move_type != "out_invoice" or self.state != 'posted': + return {} + line_count = 0 + invoice_line_pickings = {} + for line in self.invoice_line_ids.filtered(lambda l: not l.display_type): + line_count += 1 + done_moves_related = line.sale_line_ids.mapped('move_ids').filtered(lambda m: m.state == 'done' and m.location_dest_id.usage == 'customer') + if len(done_moves_related) <= 1: + if done_moves_related and line_count not in invoice_line_pickings.get(done_moves_related.picking_id, []): + invoice_line_pickings.setdefault(done_moves_related.picking_id, []).append(line_count) + else: + total_invoices = done_moves_related.mapped('sale_line_id.invoice_lines').filtered( + lambda l: l.move_id.state == 'posted' and l.move_id.move_type == 'out_invoice').sorted(lambda l: l.move_id.invoice_date) + total_invs = [(i.product_uom_id._compute_quantity(i.quantity, i.product_id.uom_id), i) for i in total_invoices] + inv = total_invs.pop(0) + # Match all moves and related invoice lines FIFO looking for when the matched invoice_line matches line + for move in done_moves_related.sorted(lambda m: m.date): + rounding = move.product_uom.rounding + move_qty = move.product_qty + while (float_compare(move_qty, 0, precision_rounding=rounding) > 0): + if float_compare(inv[0], move_qty, precision_rounding=rounding) > 0: + inv = (inv[0] - move_qty, inv[1]) + invoice_line = inv[1] + move_qty = 0 + if float_compare(inv[0], move_qty, precision_rounding=rounding) <= 0: + move_qty -= inv[0] + invoice_line = inv[1] + if total_invs: + inv = total_invs.pop(0) + else: + move_qty = 0 #abort when not enough matched invoices + # If in our FIFO iteration we stumble upon the line we were checking + if invoice_line == line and line_count not in invoice_line_pickings.get(move.picking_id, []): + invoice_line_pickings.setdefault(move.picking_id, []).append(line_count) + return invoice_line_pickings + + @api.depends('invoice_line_ids', 'invoice_line_ids.sale_line_ids') + def _compute_ddt_ids(self): + it_out_invoices = self.filtered(lambda i: i.move_type == 'out_invoice' and i.company_id.country_id.code == 'IT') + for invoice in it_out_invoices: + invoice_line_pickings = invoice._get_ddt_values() + pickings = self.env['stock.picking'] + for picking in invoice_line_pickings: + pickings |= picking + invoice.l10n_it_ddt_ids = pickings + invoice.l10n_it_ddt_count = len(pickings) + for invoice in self - it_out_invoices: + invoice.l10n_it_ddt_ids = self.env['stock.picking'] + invoice.l10n_it_ddt_count = 0 + + def get_linked_ddts(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'view_mode': 'tree,form', + 'name': _("Linked deliveries"), + 'res_model': 'stock.picking', + 'domain': [('id', 'in', self.l10n_it_ddt_ids.ids)], + } + + def _prepare_fatturapa_export_values(self): + template_values = super()._prepare_fatturapa_export_values() + template_values['ddt_dict'] = self._get_ddt_values() + return template_values diff --git a/addons/l10n_it_stock_ddt/models/stock_picking.py b/addons/l10n_it_stock_ddt/models/stock_picking.py new file mode 100644 index 00000000..439d5c9f --- /dev/null +++ b/addons/l10n_it_stock_ddt/models/stock_picking.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api, _ + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + l10n_it_transport_reason = fields.Selection([('sale', 'Sale'), + ('outsourcing', 'Outsourcing'), + ('evaluation', 'Evaluation'), + ('gift', 'Gift'), + ('transfer', 'Transfer'), + ('substitution', 'Substitution'), + ('attemped_sale', 'Attempted Sale'), + ('loaned_use', 'Loaned for Use'), + ('repair', 'Repair')], default="sale", tracking=True, string='Transport Reason') + l10n_it_transport_method = fields.Selection([('sender', 'Sender'), ('recipient', 'Recipient'), ('courier', 'Courier service')], + default="sender", string='Transport Method') + l10n_it_transport_method_details = fields.Char('Transport Note') + l10n_it_parcels = fields.Integer(string="Parcels", default=1) + l10n_it_country_code = fields.Char(related="company_id.country_id.code") + l10n_it_ddt_number = fields.Char('DDT Number', readonly=True) + + def _action_done(self): + super(StockPicking, self)._action_done() + for picking in self.filtered(lambda p: p.picking_type_id.l10n_it_ddt_sequence_id): + picking.l10n_it_ddt_number = picking.picking_type_id.l10n_it_ddt_sequence_id.next_by_id() + + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + l10n_it_ddt_sequence_id = fields.Many2one('ir.sequence') + + def _get_dtt_ir_seq_vals(self, warehouse_id, sequence_code): + if warehouse_id: + wh = self.env['stock.warehouse'].browse(warehouse_id) + ir_seq_name = wh.name + ' ' + _('Sequence') + ' ' + sequence_code + ir_seq_prefix = wh.code + '/' + sequence_code + '/DDT' + else: + ir_seq_name = _('Sequence') + ' ' + sequence_code + ir_seq_prefix = sequence_code + '/DDT' + return ir_seq_name, ir_seq_prefix + + @api.model + def create(self, vals): + company = self.env['res.company'].browse(vals['company_id']) + if 'l10n_it_ddt_sequence_id' not in vals or not vals['l10n_it_ddt_sequence_id'] and vals['code'] == 'outgoing' \ + and company.country_id.code == 'IT': + ir_seq_name, ir_seq_prefix = self._get_dtt_ir_seq_vals(vals.get('warehouse_id'), vals['sequence_code']) + vals['l10n_it_ddt_sequence_id'] = self.env['ir.sequence'].create({ + 'name': ir_seq_name, + 'prefix': ir_seq_prefix, + 'padding': 5, + 'company_id': vals['company_id'], + 'implementation': 'no_gap', + }).id + return super(StockPickingType, self).create(vals) + + def write(self, vals): + if 'sequence_code' in vals: + for picking_type in self.filtered(lambda p: p.l10n_it_ddt_sequence_id): + warehouse = picking_type.warehouse_id.id if 'warehouse_id' not in vals else vals['warehouse_ids'] + ir_seq_name, ir_seq_prefix = self._get_dtt_ir_seq_vals(warehouse, vals['sequence_code']) + picking_type.l10n_it_ddt_sequence_id.write({ + 'name': ir_seq_name, + 'prefix': ir_seq_prefix, + }) + return super(StockPickingType, self).write(vals) \ No newline at end of file diff --git a/addons/l10n_it_stock_ddt/report/l10n_it_ddt_report.xml b/addons/l10n_it_stock_ddt/report/l10n_it_ddt_report.xml new file mode 100644 index 00000000..42d84e08 --- /dev/null +++ b/addons/l10n_it_stock_ddt/report/l10n_it_ddt_report.xml @@ -0,0 +1,177 @@ + + + + + DDT report + stock.picking + qweb-pdf + l10n_it_stock_ddt.report_ddt_view + report_ddt + 'DDT - %s - %s' % (object.partner_id.name or '', object.l10n_it_ddt_number) + + diff --git a/addons/l10n_it_stock_ddt/tests/__init__.py b/addons/l10n_it_stock_ddt/tests/__init__.py new file mode 100644 index 00000000..310ceb45 --- /dev/null +++ b/addons/l10n_it_stock_ddt/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_ddt diff --git a/addons/l10n_it_stock_ddt/tests/test_ddt.py b/addons/l10n_it_stock_ddt/tests/test_ddt.py new file mode 100644 index 00000000..d0d1bfd8 --- /dev/null +++ b/addons/l10n_it_stock_ddt/tests/test_ddt.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.sale.tests.common import TestSaleCommon +from odoo.tests import tagged, Form + + +@tagged('post_install', '-at_install') +class TestDDT(TestSaleCommon): + + @classmethod + def setUpClass(cls, chart_template_ref='l10n_it.l10n_it_chart_template_generic'): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.company_data['company'].write({ + 'vat':"IT12345670017", + 'country_id': cls.env.ref('base.it'), + 'l10n_it_codice_fiscale': '01234560157', + 'l10n_it_tax_system': 'RF01', + 'street': 'Via Giovanni Maria Platina 66', + 'zip': '26100', + 'city': 'Cremona', + }) + bank_account = cls.env['res.partner.bank'].create({ + 'acc_number': 'IT60X0542811101000000123456', + 'partner_id': cls.company_data['company'].partner_id.id, + }) + cls.partner_a.write({ + 'street': 'Piazza Guglielmo Marconi 5', + 'zip': '26100', + 'city': 'Cremona', + 'country_id': cls.env.ref('base.it'), + 'vat': 'IT12345670124' + }) + + + def test_ddt_flow(self): + """ + We confirm a sale order and handle its delivery partially. + This should have created a DDT number and when we generate and the invoice, + the delivery should be linked to it as DDT. + """ + self.so = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + 'order_line': [(0, 0, {'name': p.name, + 'product_id': p.id, + 'product_uom_qty': 5, + 'product_uom': p.uom_id.id, + 'price_unit': p.list_price, + 'tax_id': self.company_data['default_tax_sale']}) + for p in ( + self.company_data['product_order_no'], + self.company_data['product_service_delivery'], + self.company_data['product_service_order'], + self.company_data['product_delivery_no'], + )], + 'pricelist_id': self.company_data['default_pricelist'].id, + 'picking_policy': 'direct', + }) + self.so.action_confirm() + + # deliver partially + pick = self.so.picking_ids + pick.move_lines.write({'quantity_done': 1}) + wiz_act = pick.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + + self.assertTrue(pick.l10n_it_ddt_number, 'The outgoing picking should have a DDT number') + self.inv1 = self.so._create_invoices() + self.inv1.action_post() + self.assertEqual(self.inv1.l10n_it_ddt_ids.ids, pick.ids, 'DDT should be linked to the invoice') + + # deliver partially + pickx1 = self.so.picking_ids.filtered(lambda p: p.state != 'done') + pickx1.move_lines.write({'quantity_done': 1}) + wiz_act = pickx1.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + + # and again + pickx2 = self.so.picking_ids.filtered(lambda p: p.state != 'done') + pickx2.move_lines.write({'quantity_done': 2}) + wiz_act = pickx2.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + + self.inv2 = self.so._create_invoices() + self.inv2.action_post() + self.assertEqual(self.inv2.l10n_it_ddt_ids.ids, (pickx1 | pickx2).ids, 'DDTs should be linked to the invoice') diff --git a/addons/l10n_it_stock_ddt/views/account_invoice_views.xml b/addons/l10n_it_stock_ddt/views/account_invoice_views.xml new file mode 100644 index 00000000..60a5967c --- /dev/null +++ b/addons/l10n_it_stock_ddt/views/account_invoice_views.xml @@ -0,0 +1,17 @@ + + + + + account.invoice.form.inherit.ddt + account.move + + + + + + + + + diff --git a/addons/l10n_it_stock_ddt/views/stock_picking_views.xml b/addons/l10n_it_stock_ddt/views/stock_picking_views.xml new file mode 100644 index 00000000..96a472c3 --- /dev/null +++ b/addons/l10n_it_stock_ddt/views/stock_picking_views.xml @@ -0,0 +1,50 @@ + + + + stock.picking.form.l10n.it.ddt + stock.picking + + + + +