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/sale/wizard/__init__.py | 7 + .../wizard/payment_acquirer_onboarding_wizard.py | 37 ++++ addons/sale/wizard/sale_make_invoice_advance.py | 197 +++++++++++++++++++++ .../wizard/sale_make_invoice_advance_views.xml | 64 +++++++ addons/sale/wizard/sale_order_cancel.py | 20 +++ addons/sale/wizard/sale_order_cancel_views.xml | 20 +++ addons/sale/wizard/sale_payment_link.py | 49 +++++ addons/sale/wizard/sale_payment_link_views.xml | 14 ++ 8 files changed, 408 insertions(+) create mode 100644 addons/sale/wizard/__init__.py create mode 100644 addons/sale/wizard/payment_acquirer_onboarding_wizard.py create mode 100644 addons/sale/wizard/sale_make_invoice_advance.py create mode 100644 addons/sale/wizard/sale_make_invoice_advance_views.xml create mode 100644 addons/sale/wizard/sale_order_cancel.py create mode 100644 addons/sale/wizard/sale_order_cancel_views.xml create mode 100644 addons/sale/wizard/sale_payment_link.py create mode 100644 addons/sale/wizard/sale_payment_link_views.xml (limited to 'addons/sale/wizard') diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py new file mode 100644 index 00000000..b0b5d571 --- /dev/null +++ b/addons/sale/wizard/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment_acquirer_onboarding_wizard +from . import sale_make_invoice_advance +from . import sale_order_cancel +from . import sale_payment_link diff --git a/addons/sale/wizard/payment_acquirer_onboarding_wizard.py b/addons/sale/wizard/payment_acquirer_onboarding_wizard.py new file mode 100644 index 00000000..6a7f3b37 --- /dev/null +++ b/addons/sale/wizard/payment_acquirer_onboarding_wizard.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class PaymentWizard(models.TransientModel): + """ Override for the sale quotation onboarding panel. """ + + _inherit = 'payment.acquirer.onboarding.wizard' + _name = 'sale.payment.acquirer.onboarding.wizard' + _description = 'Sale Payment acquire onboarding wizard' + + def _get_default_payment_method(self): + return self.env.company.sale_onboarding_payment_method or 'digital_signature' + + payment_method = fields.Selection(selection_add=[ + ('digital_signature', "Electronic signature"), + ('paypal', "PayPal"), + ('stripe', "Credit card (via Stripe)"), + ('other', "Other payment acquirer"), + ('manual', "Custom payment instructions"), + ], default=_get_default_payment_method) + # + + def _set_payment_acquirer_onboarding_step_done(self): + """ Override. """ + self.env.company.sudo().set_onboarding_step_done('sale_onboarding_order_confirmation_state') + + def add_payment_methods(self, *args, **kwargs): + self.env.company.sale_onboarding_payment_method = self.payment_method + if self.payment_method == 'digital_signature': + self.env.company.portal_confirmation_sign = True + if self.payment_method in ('paypal', 'stripe', 'other', 'manual'): + self.env.company.portal_confirmation_pay = True + + return super(PaymentWizard, self).add_payment_methods(*args, **kwargs) diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py new file mode 100644 index 00000000..ce9cd47f --- /dev/null +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class SaleAdvancePaymentInv(models.TransientModel): + _name = "sale.advance.payment.inv" + _description = "Sales Advance Payment Invoice" + + @api.model + def _count(self): + return len(self._context.get('active_ids', [])) + + @api.model + def _default_product_id(self): + product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') + return self.env['product.product'].browse(int(product_id)).exists() + + @api.model + def _default_deposit_account_id(self): + return self._default_product_id()._get_product_accounts()['income'] + + @api.model + def _default_deposit_taxes_id(self): + return self._default_product_id().taxes_id + + @api.model + def _default_has_down_payment(self): + if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): + sale_order = self.env['sale.order'].browse(self._context.get('active_id')) + return sale_order.order_line.filtered( + lambda sale_order_line: sale_order_line.is_downpayment + ) + + return False + + @api.model + def _default_currency_id(self): + if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): + sale_order = self.env['sale.order'].browse(self._context.get('active_id')) + return sale_order.currency_id + + advance_payment_method = fields.Selection([ + ('delivered', 'Regular invoice'), + ('percentage', 'Down payment (percentage)'), + ('fixed', 'Down payment (fixed amount)') + ], string='Create Invoice', default='delivered', required=True, + help="A standard invoice is issued with all the order lines ready for invoicing, \ + according to their invoicing policy (based on ordered or delivered quantity).") + deduct_down_payments = fields.Boolean('Deduct down payments', default=True) + has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True) + product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')], + default=_default_product_id) + count = fields.Integer(default=_count, string='Order Count') + amount = fields.Float('Down Payment Amount', digits='Account', help="The percentage of amount to be invoiced in advance, taxes excluded.") + currency_id = fields.Many2one('res.currency', string='Currency', default=_default_currency_id) + fixed_amount = fields.Monetary('Down Payment Amount (Fixed)', help="The fixed amount to be invoiced in advance, taxes excluded.") + deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], + help="Account used for deposits", default=_default_deposit_account_id) + deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) + + @api.onchange('advance_payment_method') + def onchange_advance_payment_method(self): + if self.advance_payment_method == 'percentage': + amount = self.default_get(['amount']).get('amount') + return {'value': {'amount': amount}} + return {} + + def _prepare_invoice_values(self, order, name, amount, so_line): + invoice_vals = { + 'ref': order.client_order_ref, + 'move_type': 'out_invoice', + 'invoice_origin': order.name, + 'invoice_user_id': order.user_id.id, + 'narration': order.note, + 'partner_id': order.partner_invoice_id.id, + 'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)).id, + 'partner_shipping_id': order.partner_shipping_id.id, + 'currency_id': order.pricelist_id.currency_id.id, + 'payment_reference': order.reference, + 'invoice_payment_term_id': order.payment_term_id.id, + 'partner_bank_id': order.company_id.partner_id.bank_ids[:1].id, + 'team_id': order.team_id.id, + 'campaign_id': order.campaign_id.id, + 'medium_id': order.medium_id.id, + 'source_id': order.source_id.id, + 'invoice_line_ids': [(0, 0, { + 'name': name, + 'price_unit': amount, + 'quantity': 1.0, + 'product_id': self.product_id.id, + 'product_uom_id': so_line.product_uom.id, + 'tax_ids': [(6, 0, so_line.tax_id.ids)], + 'sale_line_ids': [(6, 0, [so_line.id])], + 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)], + 'analytic_account_id': order.analytic_account_id.id or False, + })], + } + + return invoice_vals + + def _get_advance_details(self, order): + context = {'lang': order.partner_id.lang} + if self.advance_payment_method == 'percentage': + if all(self.product_id.taxes_id.mapped('price_include')): + amount = order.amount_total * self.amount / 100 + else: + amount = order.amount_untaxed * self.amount / 100 + name = _("Down payment of %s%%") % (self.amount) + else: + amount = self.fixed_amount + name = _('Down Payment') + del context + + return amount, name + + def _create_invoice(self, order, so_line, amount): + if (self.advance_payment_method == 'percentage' and self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and self.fixed_amount <= 0.00): + raise UserError(_('The value of the down payment amount must be positive.')) + + amount, name = self._get_advance_details(order) + + invoice_vals = self._prepare_invoice_values(order, name, amount, so_line) + + if order.fiscal_position_id: + invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id + invoice = self.env['account.move'].sudo().create(invoice_vals).with_user(self.env.uid) + invoice.message_post_with_view('mail.message_origin_link', + values={'self': invoice, 'origin': order}, + subtype_id=self.env.ref('mail.mt_note').id) + return invoice + + def _prepare_so_line(self, order, analytic_tag_ids, tax_ids, amount): + context = {'lang': order.partner_id.lang} + so_values = { + 'name': _('Down Payment: %s') % (time.strftime('%m %Y'),), + 'price_unit': amount, + 'product_uom_qty': 0.0, + 'order_id': order.id, + 'discount': 0.0, + 'product_uom': self.product_id.uom_id.id, + 'product_id': self.product_id.id, + 'analytic_tag_ids': analytic_tag_ids, + 'tax_id': [(6, 0, tax_ids)], + 'is_downpayment': True, + 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, + } + del context + return so_values + + def create_invoices(self): + sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', [])) + + if self.advance_payment_method == 'delivered': + sale_orders._create_invoices(final=self.deduct_down_payments) + else: + # Create deposit product if necessary + if not self.product_id: + vals = self._prepare_deposit_product() + self.product_id = self.env['product.product'].create(vals) + self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id) + + sale_line_obj = self.env['sale.order.line'] + for order in sale_orders: + amount, name = self._get_advance_details(order) + + if self.product_id.invoice_policy != 'order': + raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) + if self.product_id.type != 'service': + raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) + taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) + tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids + analytic_tag_ids = [] + for line in order.order_line: + analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids] + + so_line_values = self._prepare_so_line(order, analytic_tag_ids, tax_ids, amount) + so_line = sale_line_obj.create(so_line_values) + self._create_invoice(order, so_line, amount) + if self._context.get('open_invoices', False): + return sale_orders.action_view_invoice() + return {'type': 'ir.actions.act_window_close'} + + def _prepare_deposit_product(self): + return { + 'name': 'Down payment', + 'type': 'service', + 'invoice_policy': 'order', + 'property_account_income_id': self.deposit_account_id.id, + 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], + 'company_id': False, + } + diff --git a/addons/sale/wizard/sale_make_invoice_advance_views.xml b/addons/sale/wizard/sale_make_invoice_advance_views.xml new file mode 100644 index 00000000..3dd0a99c --- /dev/null +++ b/addons/sale/wizard/sale_make_invoice_advance_views.xml @@ -0,0 +1,64 @@ + + + + Invoice Orders + sale.advance.payment.inv + +
+

+ Invoices will be created in draft so that you can review + them before validation. +

+ + + + + +
+
+
+
+
+ + + Create invoices + ir.actions.act_window + sale.advance.payment.inv + form + new + + + list + + +
diff --git a/addons/sale/wizard/sale_order_cancel.py b/addons/sale/wizard/sale_order_cancel.py new file mode 100644 index 00000000..b2ac441a --- /dev/null +++ b/addons/sale/wizard/sale_order_cancel.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class SaleOrderCancel(models.TransientModel): + _name = 'sale.order.cancel' + _description = "Sales Order Cancel" + + order_id = fields.Many2one('sale.order', string='Sale Order', required=True, ondelete='cascade') + display_invoice_alert = fields.Boolean('Invoice Alert', compute='_compute_display_invoice_alert') + + @api.depends('order_id') + def _compute_display_invoice_alert(self): + for wizard in self: + wizard.display_invoice_alert = bool(wizard.order_id.invoice_ids.filtered(lambda inv: inv.state == 'draft')) + + def action_cancel(self): + return self.order_id.with_context({'disable_cancel_warning': True}).action_cancel() diff --git a/addons/sale/wizard/sale_order_cancel_views.xml b/addons/sale/wizard/sale_order_cancel_views.xml new file mode 100644 index 00000000..722d2797 --- /dev/null +++ b/addons/sale/wizard/sale_order_cancel_views.xml @@ -0,0 +1,20 @@ + + + + sale.order.cancel.form + sale.order.cancel + +
+ + +
+ Draft invoices for this order will be cancelled. +
+
+
+ +
+
+
diff --git a/addons/sale/wizard/sale_payment_link.py b/addons/sale/wizard/sale_payment_link.py new file mode 100644 index 00000000..823a2e11 --- /dev/null +++ b/addons/sale/wizard/sale_payment_link.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug import urls + +from odoo import api, models + + +class SalePaymentLink(models.TransientModel): + _inherit = "payment.link.wizard" + _description = "Generate Sales Payment Link" + + @api.model + def default_get(self, fields): + res = super(SalePaymentLink, self).default_get(fields) + if res['res_id'] and res['res_model'] == 'sale.order': + record = self.env[res['res_model']].browse(res['res_id']) + res.update({ + 'description': record.name, + 'amount': record.amount_total - sum(record.invoice_ids.filtered(lambda x: x.state != 'cancel').mapped('amount_total')), + 'currency_id': record.currency_id.id, + 'partner_id': record.partner_id.id, + 'amount_max': record.amount_total + }) + return res + + def _generate_link(self): + """ Override of the base method to add the order_id in the link. """ + for payment_link in self: + # only add order_id for SOs, + # otherwise the controller might try to link it with an unrelated record + # NOTE: company_id is not necessary here, we have it in order_id + # however, should parsing of the id fail in the controller, let's include + # it anyway + if payment_link.res_model == 'sale.order': + record = self.env[payment_link.res_model].browse(payment_link.res_id) + payment_link.link = ('%s/website_payment/pay?reference=%s&amount=%s¤cy_id=%s' + '&partner_id=%s&order_id=%s&company_id=%s&access_token=%s') % ( + record.get_base_url(), + urls.url_quote_plus(payment_link.description), + payment_link.amount, + payment_link.currency_id.id, + payment_link.partner_id.id, + payment_link.res_id, + payment_link.company_id.id, + payment_link.access_token + ) + else: + super(SalePaymentLink, payment_link)._generate_link() diff --git a/addons/sale/wizard/sale_payment_link_views.xml b/addons/sale/wizard/sale_payment_link_views.xml new file mode 100644 index 00000000..836d1b90 --- /dev/null +++ b/addons/sale/wizard/sale_payment_link_views.xml @@ -0,0 +1,14 @@ + + + + + Generate a Payment Link + payment.link.wizard + form + + new + + form + + + -- cgit v1.2.3