421 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # This file is part of the sale_payment module for Tryton.
 | |
| # The COPYRIGHT file at the top level of this repository contains the full
 | |
| # copyright notices and license terms.
 | |
| from decimal import Decimal
 | |
| from sql import For, Literal
 | |
| from sql.operators import And
 | |
| from sql.aggregate import Sum
 | |
| from sql.conditionals import Coalesce
 | |
| 
 | |
| from trytond.model import ModelView, fields
 | |
| from trytond.pool import PoolMeta, Pool
 | |
| from trytond.pyson import Bool, Eval, Not
 | |
| from trytond.transaction import Transaction
 | |
| from trytond.wizard import Wizard, StateView, StateTransition, Button
 | |
| from trytond.i18n import gettext
 | |
| from trytond.exceptions import UserError
 | |
| from trytond.modules.currency.fields import Monetary
 | |
| 
 | |
| 
 | |
| __all__ = ['Sale', 'SalePaymentForm', 'WizardSalePayment',
 | |
|            'WizardSaleReconcile']
 | |
| 
 | |
| 
 | |
| class Sale(metaclass=PoolMeta):
 | |
|     __name__ = 'sale.sale'
 | |
|     payments = fields.One2Many('account.statement.line', 'sale', 'Payments')
 | |
|     paid_amount = fields.Function(fields.Numeric('Paid Amount', readonly=True),
 | |
|                                   'get_paid_amount')
 | |
|     residual_amount = fields.Function(fields.Numeric('Residual Amount'),
 | |
|                                       'get_residual_amount',
 | |
|                                       searcher='search_residual_amount')
 | |
|     sale_device = fields.Many2One('sale.device', 'Sale Device',
 | |
|                                   domain=[('shop', '=', Eval('shop'))],
 | |
|                                   depends=['shop'], states={
 | |
|                                       'readonly': Eval('state') != 'draft',
 | |
|                                   })
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super(Sale, cls).__setup__()
 | |
|         cls._buttons.update({
 | |
|             'wizard_sale_payment': {
 | |
|                 'invisible': Eval('state') == 'done',
 | |
|                 'readonly': Not(Bool(Eval('lines'))),
 | |
|                     },
 | |
|             'wizard_change_payment_method': {},
 | |
|                 })
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_sale_device():
 | |
|         User = Pool().get('res.user')
 | |
|         user = User(Transaction().user)
 | |
|         return user.sale_device and user.sale_device.id or None
 | |
| 
 | |
|     def set_basic_values_to_invoice(self, invoice):
 | |
|         pool = Pool()
 | |
|         Date = pool.get('ir.date')
 | |
|         today = Date.today()
 | |
|         if not getattr(invoice, 'invoice_date', False):
 | |
|             invoice.invoice_date = today
 | |
|         if not getattr(invoice, 'accounting_date', False):
 | |
|             invoice.accounting_date = today
 | |
|         invoice.description = self.reference
 | |
| 
 | |
|     @classmethod
 | |
|     def set_invoices_to_be_posted(cls, sales):
 | |
|         pool = Pool()
 | |
|         Invoice = pool.get('account.invoice')
 | |
|         invoices = []
 | |
|         to_post = set()
 | |
|         for sale in sales:
 | |
|             grouping = getattr(sale.party, 'sale_invoice_grouping_method',
 | |
|                 False)
 | |
|             if Transaction().context.get('skip_grouping', False):
 | |
|                 grouping = None
 | |
|             if getattr(sale, 'invoices', None) and not grouping:
 | |
|                 for invoice in sale.invoices:
 | |
|                     if not invoice.state == 'draft':
 | |
|                         continue
 | |
|                     sale.set_basic_values_to_invoice(invoice)
 | |
|                     invoices.extend(([invoice], invoice._save_values()))
 | |
|                     to_post.add(invoice)
 | |
| 
 | |
|         if to_post:
 | |
|             Invoice.write(*invoices)
 | |
|             return list(to_post)
 | |
| 
 | |
|     @classmethod
 | |
|     @ModelView.button
 | |
|     def process(cls, sales):
 | |
|         states = {'confirmed', 'processing', 'done'}
 | |
|         sales = [s for s in sales if s.state in states]
 | |
|         cls._process_invoice(sales)
 | |
|         cls._process_shipment(sales)
 | |
|         cls._process_invoice_shipment_states(sales)
 | |
|         cls._process_state(sales)
 | |
| 
 | |
|     @classmethod
 | |
|     def workflow_to_end(cls, sales):
 | |
|         pool = Pool()
 | |
|         StatementLine = pool.get('account.statement.line')
 | |
|         Invoice = pool.get('account.invoice')
 | |
| 
 | |
|         for sale in sales:
 | |
|             if sale.state == 'draft':
 | |
|                 cls.quote([sale])
 | |
|             if sale.state == 'quotation':
 | |
|                 cls.confirm([sale])
 | |
|             if sale.state == 'confirmed':
 | |
|                 cls.process([sale])
 | |
| 
 | |
|             if not sale.invoices and sale.invoice_method == 'order':
 | |
|                 raise UserError(
 | |
|                     gettext(
 | |
|                         'sale_payment.not_customer_invoice',
 | |
|                         reference=sale.reference
 | |
|                     ))
 | |
| 
 | |
|         to_post = cls.set_invoices_to_be_posted(sales)
 | |
|         if to_post:
 | |
|             with Transaction().set_context(_skip_warnings=True):
 | |
|                 Invoice.post(to_post)
 | |
| 
 | |
|         to_save = []
 | |
|         to_do = []
 | |
|         for sale in sales:
 | |
|             posted_invoice = None
 | |
|             for invoice in sale.invoices:
 | |
|                 if invoice.state == 'posted':
 | |
|                     posted_invoice = invoice
 | |
|                     break
 | |
|             if posted_invoice:
 | |
|                 for payment in sale.payments:
 | |
|                     # Because of account_invoice_party_without_vat module
 | |
|                     # could be installed, invoice party may be different of
 | |
|                     # payment party if payment party has not any vat
 | |
|                     # and both parties must be the same
 | |
|                     if payment.party != invoice.party:
 | |
|                         payment.party = invoice.party
 | |
|                     payment.invoice = posted_invoice
 | |
|                     to_save.append(payment)
 | |
| 
 | |
|             if sale.is_done():
 | |
|                 to_do.append(sale)
 | |
| 
 | |
|         StatementLine.save(to_save)
 | |
| 
 | |
|         if to_do:
 | |
|             cls.do(to_do)
 | |
| 
 | |
|     @classmethod
 | |
|     def get_paid_amount(cls, sales, names):
 | |
|         result = {n: {s.id: Decimal(0) for s in sales} for n in names}
 | |
|         for name in names:
 | |
|             for sale in sales:
 | |
|                 for payment in sale.payments:
 | |
|                     result[name][sale.id] += payment.amount
 | |
|         return result
 | |
| 
 | |
|     @classmethod
 | |
|     def get_residual_amount(cls, sales, name):
 | |
|         return {s.id: s.total_amount - s.paid_amount if s.state != 'cancelled'
 | |
|                 else Decimal(0) for s in sales}
 | |
| 
 | |
|     @classmethod
 | |
|     def search_residual_amount(cls, name, clause):
 | |
|         pool = Pool()
 | |
|         Sale = pool.get('sale.sale')
 | |
|         StatementLine = pool.get('account.statement.line')
 | |
| 
 | |
|         sale = Sale.__table__()
 | |
|         payline = StatementLine.__table__()
 | |
|         Operator = fields.SQL_OPERATORS[clause[1]]
 | |
|         value = clause[2]
 | |
| 
 | |
|         query = sale.join(
 | |
|             payline,
 | |
|             type_='LEFT',
 | |
|             condition=(sale.id == payline.sale)
 | |
|             ).select(
 | |
|                 sale.id,
 | |
|                 where=(And([
 | |
|                     sale.total_amount_cache is not None,
 | |
|                     sale.state.in_([
 | |
|                         'draft',
 | |
|                         'quotation',
 | |
|                         'confirmed',
 | |
|                         'processing',
 | |
|                         'done'])])),
 | |
|                 group_by=(sale.id),
 | |
|                 having=(Operator(
 | |
|                     sale.total_amount_cache - Sum(
 | |
|                         Coalesce(payline.amount, 0)), value)
 | |
|                         ))
 | |
|         return [('id', 'in', query)]
 | |
| 
 | |
|     @classmethod
 | |
|     @ModelView.button_action('sale_payment.wizard_sale_payment')
 | |
|     def wizard_sale_payment(cls, sales):
 | |
|         pass
 | |
| 
 | |
|     @classmethod
 | |
|     @ModelView.button_action('sale_payment.wizard_sale_change_payment_method')
 | |
|     def wizard_change_payment_method(cls, sales):
 | |
|         pass
 | |
| 
 | |
|     @classmethod
 | |
|     def copy(cls, sales, default=None):
 | |
|         if default is None:
 | |
|             default = {}
 | |
|         default['payments'] = None
 | |
|         return super(Sale, cls).copy(sales, default)
 | |
| 
 | |
| 
 | |
| class ChangePaymentMethodForm(ModelView):
 | |
|     'Change Payments Method form'
 | |
|     __name__ = 'sale.change_payment_method.form'
 | |
| 
 | |
|     sale = fields.Many2One('sale.sale', "Sale", required=True)
 | |
|     statement = fields.Many2One('account.statement', 'Statement Journal',
 | |
|                                 domain=[
 | |
|                                     ('id', 'in', Eval('statements', [])),
 | |
|                                 ], required=True)
 | |
|     statements = fields.One2Many('account.statement', None,
 | |
|                                  'Allowed Statement', readonly=True)
 | |
|     payment = fields.Many2One(
 | |
|         'account.statement.line', "Payment", required=True,
 | |
|         domain=[('sale', '=', Eval('sale'))])
 | |
| 
 | |
| 
 | |
| class ChangePaymentMethod(Wizard):
 | |
|     'Change Payments Method for the sale'
 | |
|     __name__ = 'sale.change_payment_method'
 | |
|     start = StateView('sale.change_payment_method.form',
 | |
|                       'sale_payment.sale_change_payment_method_view_form', [
 | |
|                           Button('Cancel', 'end', 'tryton-cancel'),
 | |
|                           Button('Modify', 'modify_payments_', 'tryton-ok',
 | |
|                                  default=True),])
 | |
| 
 | |
|     modify_payments_ = StateTransition()
 | |
| 
 | |
|     def default_start(self, fields):
 | |
|         pool = Pool()
 | |
|         Sale = pool.get('sale.sale')
 | |
|         User = pool.get('res.user')
 | |
|         Statement = pool.get('account.statement')
 | |
|         sale = Sale(Transaction().context['active_id'])
 | |
|         user = User(Transaction().user)
 | |
|         device = user.sale_device
 | |
| 
 | |
|         if device:
 | |
|             journals = [j.id for j in device.journals]
 | |
|             draft_statements = Statement.search([
 | |
|                 ('journal', 'in', journals),
 | |
|                 ('state', '=', 'draft'),
 | |
|             ], order=[
 | |
|                 ('create_date', 'ASC'),
 | |
|             ])
 | |
| 
 | |
|         return {
 | |
|             'sale': sale.id,
 | |
|             'statements': [statement.id for statement in draft_statements],
 | |
|             }
 | |
| 
 | |
|     def transition_modify_payments_(self):
 | |
|         self.start.payment.statement = self.start.statement.id
 | |
|         self.start.payment.save()
 | |
| 
 | |
|         return 'end'
 | |
| 
 | |
| 
 | |
| class SalePaymentForm(ModelView):
 | |
|     'Sale Payment Form'
 | |
|     __name__ = 'sale.payment.form'
 | |
|     journal = fields.Many2One('account.statement.journal', 'Statement Journal',
 | |
|                               domain=[
 | |
|                                   ('id', 'in', Eval('journals', [])),
 | |
|                               ],
 | |
|                               depends=['journals'], required=True)
 | |
|     journals = fields.One2Many('account.statement.journal', None,
 | |
|                                'Allowed Statement Journals')
 | |
|     payment_amount = Monetary('Payment amount', required=True,
 | |
|                               currency='currency', digits='currency')
 | |
|     party = fields.Many2One('party.party', 'Party', readonly=True)
 | |
|     currency = fields.Many2One('currency.currency', 'Currency', readonly=True)
 | |
| 
 | |
| 
 | |
| class WizardSalePayment(Wizard):
 | |
|     'Wizard Sale Payment'
 | |
|     __name__ = 'sale.payment'
 | |
|     start = StateView('sale.payment.form',
 | |
|                       'sale_payment.sale_payment_view_form', [
 | |
|                           Button('Cancel', 'end', 'tryton-cancel'),
 | |
|                           Button('Pay', 'pay_', 'tryton-ok', default=True),
 | |
|                       ])
 | |
|     pay_ = StateTransition()
 | |
| 
 | |
|     def default_start(self, fields):
 | |
|         pool = Pool()
 | |
|         Sale = pool.get('sale.sale')
 | |
|         User = pool.get('res.user')
 | |
|         sale = Sale(Transaction().context['active_id'])
 | |
|         user = User(Transaction().user)
 | |
|         sale_device = sale.sale_device or user.sale_device or False
 | |
|         if user.id != 0 and not sale_device:
 | |
|             raise UserError(gettext('sale_payment.not_sale_device'))
 | |
|         return {
 | |
|             'journal': sale_device.journal.id
 | |
|             if sale_device.journal else None,
 | |
|             'journals': [j.id for j in sale_device.journals],
 | |
|             'payment_amount': sale.total_amount - sale.paid_amount
 | |
|             if sale.paid_amount else sale.total_amount,
 | |
|             'currency': sale.currency and sale.currency.id,
 | |
|             'party': sale.party.id,
 | |
|             }
 | |
| 
 | |
|     def get_statement_line(self, sale):
 | |
|         pool = Pool()
 | |
|         Date = pool.get('ir.date')
 | |
|         Sale = pool.get('sale.sale')
 | |
|         Statement = pool.get('account.statement')
 | |
|         StatementLine = pool.get('account.statement.line')
 | |
| 
 | |
|         form = self.start
 | |
|         statements = Statement.search([
 | |
|                 ('journal', '=', form.journal),
 | |
|                 ('state', '=', 'draft'),
 | |
|                 ], order=[('date', 'DESC')])
 | |
|         if not statements:
 | |
|             raise UserError(gettext('sale_payment.not_draft_statement',
 | |
|                                     journal=form.journal.name))
 | |
| 
 | |
|         if not sale.number:
 | |
|             Sale.set_number([sale])
 | |
| 
 | |
|         with Transaction().set_context(date=Date.today()):
 | |
|             account = sale.party.account_receivable_used
 | |
| 
 | |
|         if not account:
 | |
|             raise UserError(gettext(
 | |
|                 'sale_payment.party_without_account_receivable',
 | |
|                 party=sale.party.name))
 | |
| 
 | |
|         if form.payment_amount:
 | |
|             return StatementLine(
 | |
|                 statement=statements[0],
 | |
|                 date=Date.today(),
 | |
|                 amount=form.payment_amount,
 | |
|                 party=sale.party,
 | |
|                 account=account,
 | |
|                 description=sale.number,
 | |
|                 sale=sale,
 | |
|                 )
 | |
| 
 | |
|     def transition_pay_(self):
 | |
|         pool = Pool()
 | |
|         Sale = pool.get('sale.sale')
 | |
|         sale = Sale(Transaction().context['active_id'])
 | |
|         transaction = Transaction()
 | |
|         database = transaction.database
 | |
|         connection = transaction.connection
 | |
| 
 | |
|         if database.has_select_for():
 | |
|             table = Sale.__table__()
 | |
|             query = table.select(
 | |
|                 Literal(1),
 | |
|                 where=(table.id == sale.id),
 | |
|                 for_=For('UPDATE', nowait=True))
 | |
|             with connection.cursor() as cursor:
 | |
|                 cursor.execute(*query)
 | |
|         else:
 | |
|             Sale.lock()
 | |
| 
 | |
|         line = self.get_statement_line(sale)
 | |
|         if line:
 | |
|             line.save()
 | |
| 
 | |
|         if sale.total_amount != sale.paid_amount:
 | |
|             return 'start'
 | |
|         if sale.state not in ('draft', 'quotation', 'confirmed'):
 | |
|             return 'end'
 | |
| 
 | |
|         sale.description = sale.reference
 | |
|         sale.save()
 | |
| 
 | |
|         Sale.workflow_to_end([sale])
 | |
| 
 | |
|         return 'end'
 | |
| 
 | |
| 
 | |
| class WizardSaleReconcile(Wizard):
 | |
|     'Reconcile Sales'
 | |
|     __name__ = 'sale.reconcile'
 | |
|     start = StateTransition()
 | |
|     reconcile = StateTransition()
 | |
| 
 | |
|     def transition_start(self):
 | |
|         pool = Pool()
 | |
|         Sale = pool.get('sale.sale')
 | |
|         Line = pool.get('account.move.line')
 | |
|         for sale in Sale.browse(Transaction().context['active_ids']):
 | |
|             account = sale.party.account_receivable
 | |
|             lines = []
 | |
|             amount = Decimal('0.0')
 | |
|             for invoice in sale.invoices:
 | |
|                 for line in invoice.lines_to_pay:
 | |
|                     if not line.reconciliation:
 | |
|                         lines.append(line)
 | |
|                         amount += line.debit - line.credit
 | |
|             for payment in sale.payments:
 | |
|                 if not payment.move:
 | |
|                     continue
 | |
|                 for line in payment.move.lines:
 | |
|                     if (not line.reconciliation and (
 | |
|                             line.account == account)):
 | |
|                         lines.append(line)
 | |
|                         amount += line.debit - line.credit
 | |
|             if lines and amount == Decimal('0.0'):
 | |
|                 Line.reconcile(lines)
 | |
|         return 'end'
 |