From c3d84d61e8104741b657204a5e79fe0aedebfbc5 Mon Sep 17 00:00:00 2001 From: kongrattapong Date: Mon, 19 Aug 2019 13:41:42 +0700 Subject: [PATCH 01/49] [12.0][ADD] Purchase Invoice Plan --- purchase_invoice_plan/__init__.py | 5 + purchase_invoice_plan/__manifest__.py | 23 + purchase_invoice_plan/models/__init__.py | 4 + purchase_invoice_plan/models/purchase.py | 243 ++++++++++ purchase_invoice_plan/readme/CONTRIBUTORS.rst | 2 + purchase_invoice_plan/readme/DESCRIPTION.rst | 4 + purchase_invoice_plan/readme/USAGE.rst | 7 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 435 ++++++++++++++++++ purchase_invoice_plan/tests/__init__.py | 4 + .../tests/test_purchase_invoice_plan.py | 102 ++++ purchase_invoice_plan/views/purchase_view.xml | 149 ++++++ purchase_invoice_plan/wizard/__init__.py | 4 + .../wizard/purchase_create_invoice_plan.py | 53 +++ .../purchase_create_invoice_plan_view.xml | 48 ++ .../wizard/purchase_make_planned_invoice.py | 22 + .../purchase_make_planned_invoice_view.xml | 41 ++ 17 files changed, 1148 insertions(+) create mode 100644 purchase_invoice_plan/__init__.py create mode 100644 purchase_invoice_plan/__manifest__.py create mode 100644 purchase_invoice_plan/models/__init__.py create mode 100644 purchase_invoice_plan/models/purchase.py create mode 100644 purchase_invoice_plan/readme/CONTRIBUTORS.rst create mode 100644 purchase_invoice_plan/readme/DESCRIPTION.rst create mode 100644 purchase_invoice_plan/readme/USAGE.rst create mode 100644 purchase_invoice_plan/security/ir.model.access.csv create mode 100644 purchase_invoice_plan/static/description/index.html create mode 100644 purchase_invoice_plan/tests/__init__.py create mode 100644 purchase_invoice_plan/tests/test_purchase_invoice_plan.py create mode 100644 purchase_invoice_plan/views/purchase_view.xml create mode 100644 purchase_invoice_plan/wizard/__init__.py create mode 100644 purchase_invoice_plan/wizard/purchase_create_invoice_plan.py create mode 100644 purchase_invoice_plan/wizard/purchase_create_invoice_plan_view.xml create mode 100644 purchase_invoice_plan/wizard/purchase_make_planned_invoice.py create mode 100644 purchase_invoice_plan/wizard/purchase_make_planned_invoice_view.xml diff --git a/purchase_invoice_plan/__init__.py b/purchase_invoice_plan/__init__.py new file mode 100644 index 00000000000..d5a964721a2 --- /dev/null +++ b/purchase_invoice_plan/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import models +from . import wizard diff --git a/purchase_invoice_plan/__manifest__.py b/purchase_invoice_plan/__manifest__.py new file mode 100644 index 00000000000..73faaa341df --- /dev/null +++ b/purchase_invoice_plan/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + 'name': 'Purchase Invoice Plan', + 'summary': 'Add to purchases order, ability to manage future invoice plan', + 'version': '12.0.1.0.0', + 'author': 'Ecosoft,Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://github.com/OCA/purchase-workflow/', + 'category': 'Purchase', + 'depends': ['account', + 'purchase_open_qty', + 'purchase_stock', + ], + 'data': ['security/ir.model.access.csv', + 'wizard/purchase_create_invoice_plan_view.xml', + 'wizard/purchase_make_planned_invoice_view.xml', + 'views/purchase_view.xml', + ], + 'installable': True, + 'development_status': 'alpha', +} diff --git a/purchase_invoice_plan/models/__init__.py b/purchase_invoice_plan/models/__init__.py new file mode 100644 index 00000000000..8f3730cc9fe --- /dev/null +++ b/purchase_invoice_plan/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import purchase diff --git a/purchase_invoice_plan/models/purchase.py b/purchase_invoice_plan/models/purchase.py new file mode 100644 index 00000000000..1862206c33c --- /dev/null +++ b/purchase_invoice_plan/models/purchase.py @@ -0,0 +1,243 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from dateutil.relativedelta import relativedelta +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +from odoo.addons import decimal_precision as dp +from odoo.tools.float_utils import float_compare, float_round + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + invoice_plan_ids = fields.One2many( + comodel_name='purchase.invoice.plan', + inverse_name='purchase_id', + string='Inovice Plan', + copy=False, + readonly=True, + states={'draft': [('readonly', False)]}, + ) + use_invoice_plan = fields.Boolean( + string='Use Invoice Plan', + default=False, + copy=False, + ) + ip_invoice_plan = fields.Boolean( + string='Invoice Plan In Process', + compute='_compute_ip_invoice_plan', + help="At least one invoice plan line pending to create invoice", + ) + + @api.multi + def _compute_ip_invoice_plan(self): + for rec in self: + rec.ip_invoice_plan = rec.use_invoice_plan and \ + rec.invoice_plan_ids and \ + len(rec.invoice_plan_ids.filtered(lambda l: not l.invoiced)) + + @api.constrains('state') + def _check_invoice_plan(self): + for rec in self: + if rec.state != 'draft': + if rec.invoice_plan_ids.filtered(lambda l: not l.percent): + raise ValidationError( + _('Please fill percentage for all invoice plan lines')) + + @api.multi + def action_confirm(self): + if self.filtered(lambda r: r.use_invoice_plan + and not r.invoice_plan_ids): + raise UserError( + _('Use Invoice Plan selected, but no plan created')) + return super().action_confirm() + + @api.multi + def create_invoice_plan(self, num_installment, installment_date, + interval, interval_type): + self.ensure_one() + self.invoice_plan_ids.unlink() + invoice_plans = [] + Decimal = self.env['decimal.precision'] + prec = Decimal.precision_get('Product Unit of Measure') + percent = float_round(1.0 / num_installment * 100, prec) + percent_last = 100 - (percent * (num_installment-1)) + for i in range(num_installment): + this_installment = i+1 + if num_installment == this_installment: + percent = percent_last + vals = {'installment': this_installment, + 'plan_date': installment_date, + 'invoice_type': 'installment', + 'percent': percent} + invoice_plans.append((0, 0, vals)) + installment_date = self._next_date(installment_date, + interval, interval_type) + self.write({'invoice_plan_ids': invoice_plans}) + return True + + @api.multi + def remove_invoice_plan(self): + self.ensure_one() + self.invoice_plan_ids.unlink() + return True + + @api.model + def _next_date(self, installment_date, interval, interval_type): + installment_date = fields.Date.from_string(installment_date) + if interval_type == 'month': + next_date = installment_date + relativedelta(months=+interval) + elif interval_type == 'year': + next_date = installment_date + relativedelta(years=+interval) + else: + next_date = installment_date + relativedelta(days=+interval) + next_date = fields.Date.to_string(next_date) + return next_date + + @api.multi + def action_invoice_create(self): + self.ensure_one() + pre_inv = self.env['account.invoice'].new({ + 'type': 'in_invoice', + 'purchase_id': self.id, + 'currency_id': self.currency_id.id, + 'company_id': self.company_id.id, + 'origin': self.name, + 'name': self.partner_ref or '', + 'comment': self.notes + }) + pre_inv.purchase_order_change() + inv_data = pre_inv._convert_to_write(pre_inv._cache) + invoice = self.env['account.invoice'].create(inv_data) + invoice.compute_taxes() + if not invoice.invoice_line_ids: + raise UserError( + _("There is no invoiceable line. If a product has a" + "Delivered quantities invoicing policy, please make sure" + "that a quantity has been delivered.")) + po_payment_term_id = invoice.payment_term_id.id + fp_invoice = invoice.fiscal_position_id + invoice._onchange_partner_id() + invoice.fiscal_position_id = fp_invoice + invoice.payment_term_id = po_payment_term_id + invoice.message_post_with_view( + 'mail.message_origin_link', + values={'self': invoice, + 'origin': self, }, + subtype_id=self.env.ref('mail.mt_note').id) + invoice_plan_id = self._context.get('invoice_plan_id') + if invoice_plan_id: + plan = self.env['purchase.invoice.plan'].browse(invoice_plan_id) + plan._compute_new_invoice_quantity(invoice) + invoice.date_invoice = plan.plan_date + plan.invoice_ids += invoice + return invoice + + +class PurchaseInvoicePlan(models.Model): + _name = 'purchase.invoice.plan' + _description = 'Invoice Planning Detail' + _order = 'installment' + + purchase_id = fields.Many2one( + comodel_name='purchase.order', + string='Purchases Order', + index=True, + readonly=True, + ondelete='cascade', + ) + partner_id = fields.Many2one( + comodel_name='res.partner', + string='Supplier', + related='purchase_id.partner_id', + store=True, + index=True, + ) + state = fields.Selection( + [('draft', 'RFQ'), + ('sent', 'RFQ Sent'), + ('to approve', 'To Approve'), + ('purchase', 'Purchase Order'), + ('done', 'Locked'), + ('cancel', 'Cancelled'), ], + string='Status', + related='purchase_id.state', + store=True, + index=True, + ) + installment = fields.Integer( + string='Installment', + ) + plan_date = fields.Date( + string='Plan Date', + required=True, + ) + invoice_type = fields.Selection( + [('installment', 'Installment')], + string='Type', + required=True, + default='installment', + ) + percent = fields.Float( + string='Percent', + digits=dp.get_precision('Product Unit of Measure'), + help="This percent will be used to calculate new quantity" + ) + invoice_ids = fields.Many2many( + 'account.invoice', + relation="purchase_invoice_plan_invoice_rel", + column1='plan_id', + column2='invoice_id', + string='Invoices', + readonly=True, + ) + to_invoice = fields.Boolean( + string='Next Invoice', + compute='_compute_to_invoice', + help="If this line is ready to create new invoice", + ) + invoiced = fields.Boolean( + string='Invoice Created', + compute='_compute_invoiced', + help="If this line already invoiced", + ) + + @api.multi + def _compute_to_invoice(self): + """ If any invoice is in draft/open/paid do not allow to create inv + Only if previous to_invoice is False, it is eligible to_invoice + """ + for rec in self.sorted('installment'): + rec.to_invoice = False + if rec.purchase_id.state != 'purchase': + # Not confirmed, no to_invoice + continue + if not rec.invoiced: + rec.to_invoice = True + break + + @api.multi + def _compute_invoiced(self): + for rec in self: + invoiced = rec.invoice_ids.filtered( + lambda l: l.state in ('draft', 'open', 'paid')) + rec.invoiced = invoiced and True or False + + @api.multi + def _compute_new_invoice_quantity(self, invoice): + self.ensure_one() + percent = self.percent + for line in invoice.invoice_line_ids: + assert len(line.purchase_line_id) >= 0, \ + 'No matched order line for invoice line' + order_line = fields.first(line.purchase_line_id) + plan_qty = order_line.product_qty * (percent/100) + prec = order_line.product_uom.rounding + if float_compare(plan_qty, line.quantity, prec) == 1: + raise ValidationError( + _('Plan quantity: %s, exceed invoiceable quantity: %s' + '\nProduct should be delivered before invoice') % + (plan_qty, line.quantity)) + line.write({'quantity': plan_qty}) + invoice.compute_taxes() diff --git a/purchase_invoice_plan/readme/CONTRIBUTORS.rst b/purchase_invoice_plan/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..b461d809d66 --- /dev/null +++ b/purchase_invoice_plan/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Kitti Upariphutthiphong +* Rattapong Chokmasermkul diff --git a/purchase_invoice_plan/readme/DESCRIPTION.rst b/purchase_invoice_plan/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..5d596ce4aa6 --- /dev/null +++ b/purchase_invoice_plan/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +By standard feature, user can gradually create partial invoices, one by one and +in step create invoice the standard call invoice. +This module add ability to create invoices based on the predefined invoice plan, +either all at once, or one by one. diff --git a/purchase_invoice_plan/readme/USAGE.rst b/purchase_invoice_plan/readme/USAGE.rst new file mode 100644 index 00000000000..9d62284fe2a --- /dev/null +++ b/purchase_invoice_plan/readme/USAGE.rst @@ -0,0 +1,7 @@ +- Create new purchase quotation as per normal process +- Select option "Use Invoice Plan", a new Invoice Plan tab will appear +- Click on "=> Create Invoice Plan" link to open invoice planning wizard +- Do plan for number of installment, start date and interval +- Double check that each installment has correct plan percentage +- After confirm purchases order, we have new option to "Create Bill by Plan" +- User can create only next bill or create all bills at the same time diff --git a/purchase_invoice_plan/security/ir.model.access.csv b/purchase_invoice_plan/security/ir.model.access.csv new file mode 100644 index 00000000000..cea36980261 --- /dev/null +++ b/purchase_invoice_plan/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_invoice_plan,access_purchase_invoice_plan,model_purchase_invoice_plan,,1,1,1,1 diff --git a/purchase_invoice_plan/static/description/index.html b/purchase_invoice_plan/static/description/index.html new file mode 100644 index 00000000000..c153a0b72e4 --- /dev/null +++ b/purchase_invoice_plan/static/description/index.html @@ -0,0 +1,435 @@ + + + + + + +Purchase Invoice Plan + + + +
+

Purchase Invoice Plan

+ + +

License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runbot

+

By standard feature, user can gradually create partial invoices, one by one and +in step create invoice the standard call invoice. +This module add ability to create invoices based on the predefined invoice plan, +either all at once, or one by one.

+

Table of contents

+ +
+

Usage

+
    +
  • Create new purchase quotation as per normal process
  • +
  • Select option “Use Invoice Plan”, a new Invoice Plan tab will appear
  • +
  • Click on “=> Create Invoice Plan” link to open invoice planning wizard
  • +
  • Do plan for number of installment, start date and interval
  • +
  • Double check that each installment has correct plan percentage
  • +
  • After confirm purchases order, we have new option to “Create Invoice by Plan”
  • +
  • User can create only next invoice or create all invoices at the same time
  • +
+
+
+

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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

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/purchase-workflow project on GitHub.

+

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

+
+
+
+ + diff --git a/purchase_invoice_plan/tests/__init__.py b/purchase_invoice_plan/tests/__init__.py new file mode 100644 index 00000000000..f718d3b69c9 --- /dev/null +++ b/purchase_invoice_plan/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import test_purchase_invoice_plan diff --git a/purchase_invoice_plan/tests/test_purchase_invoice_plan.py b/purchase_invoice_plan/tests/test_purchase_invoice_plan.py new file mode 100644 index 00000000000..f4561b512ad --- /dev/null +++ b/purchase_invoice_plan/tests/test_purchase_invoice_plan.py @@ -0,0 +1,102 @@ +# Copyright 2019 Ecosoft (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.tests.common import TransactionCase, Form +from odoo import fields +from odoo.exceptions import ValidationError + + +class TestPurchaseInvoicePlan(TransactionCase): + + def setUp(self): + super(TestPurchaseInvoicePlan, self).setUp() + # Create a PO + self.PurchaseOrder = self.env['purchase.order'] + self.PurchaseInvoicePlan = self.env['purchase.create.invoice.plan'] + self.StockBackorderConfirm = self.env['stock.backorder.confirmation'] + self.StockPicking = self.env['stock.picking'] + + self.test_partner = self.env.ref('base.res_partner_12') + self.test_service = self.env.ref('product.product_product_2') + self.test_product = self.env.ref('product.product_product_7') + + self.test_po_service = self.env['purchase.order'].create({ + 'partner_id': self.test_partner.id, + 'use_invoice_plan': True, + 'order_line': [ + (0, 0, {'name': 'PO-Service', + 'product_id': self.test_service.id, + 'date_planned': fields.Datetime.now(), + 'product_qty': 1, + 'product_uom': self.test_service.uom_id.id, + 'price_unit': 500, + }) + ], + }) + self.test_po_product = self.env['purchase.order'].create({ + 'partner_id': self.test_partner.id, + 'use_invoice_plan': True, + 'order_line': [ + (0, 0, {'name': 'PO-Product', + 'product_id': self.test_product.id, + 'date_planned': fields.Datetime.now(), + 'product_qty': 10, + 'product_uom': self.test_product.uom_id.id, + 'price_unit': 1000, + }) + ], + }) + + def test_invoice_plan(self): + ctx = {'active_id': self.test_po_product.id, + 'active_ids': [self.test_po_product.id], + 'all_remain_invoices': True} + # Create purchase plan + with Form(self.PurchaseInvoicePlan) as p: + p.num_installment = 5 + purchase_plan = p.save() + purchase_plan.with_context(ctx).purchase_create_invoice_plan() + self.test_po_product.button_confirm() + self.assertEqual(self.test_po_product.state, 'purchase') + # Receive all products + receive = self.test_po_product.picking_ids.filtered(lambda l: + l.state != 'done') + receive.move_ids_without_package.quantity_done = 10.0 + receive.action_done() + purchase_create = self.env['purchase.make.planned.invoice'].create({}) + purchase_create.with_context(ctx).create_invoices_by_plan() + + def test_unlink_invoice_plan(self): + ctx = {'active_id': self.test_po_product.id, + 'active_ids': [self.test_po_product.id]} + with Form(self.PurchaseInvoicePlan) as p: + p.num_installment = 5 + plan = p.save() + plan.with_context(ctx).purchase_create_invoice_plan() + # Remove it + self.test_po_product.remove_invoice_plan() + self.assertFalse(self.test_po_product.invoice_plan_ids) + + def test_error(self): + ctx = {'active_id': self.test_po_product.id, + 'active_ids': [self.test_po_product.id], + 'all_remain_invoices': True} + # Create purchase plan + with Form(self.PurchaseInvoicePlan) as p: + p.num_installment = 5 + purchase_plan = p.save() + purchase_plan.with_context(ctx).purchase_create_invoice_plan() + self.test_po_product.button_confirm() + self.assertEqual(self.test_po_product.state, 'purchase') + # Receive product 1 unit + receive = self.test_po_product.picking_ids.filtered(lambda l: + l.state != 'done') + receive.move_ids_without_package.quantity_done = 1.0 + receive.action_done() + # ValidationError Create all invoice plan - Receive < Invoice require + purchase_create = self.env['purchase.make.planned.invoice'].create({}) + with self.assertRaises(ValidationError) as e: + purchase_create.with_context(ctx).create_invoices_by_plan() + error_message = ('Plan quantity: 2.0, exceed invoiceable quantity: 1.0' + '\nProduct should be delivered before invoice') + self.assertEqual(e.exception.name, error_message) diff --git a/purchase_invoice_plan/views/purchase_view.xml b/purchase_invoice_plan/views/purchase_view.xml new file mode 100644 index 00000000000..c5e36039e98 --- /dev/null +++ b/purchase_invoice_plan/views/purchase_view.xml @@ -0,0 +1,149 @@ + + + + view.purchase.invoice.plan.tree + purchase.invoice.plan + + + + + + + + + + + + + + view.purchase.invoice.plan.form + purchase.invoice.plan + +
+ + + + + + + + + + + + + + + + +
+ + + purchase_order_form + purchase.order + + + +
+ +
+
+ + +