# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import time from decimal import Decimal from datetime import datetime, date from itertools import chain from simpleeval import simple_eval import math from sql import Table from trytond.model import ModelView, fields from trytond.pool import PoolMeta, Pool from trytond.transaction import Transaction from trytond.pyson import Bool, Eval, Or, Not from trytond.wizard import ( Wizard, StateView, StateAction, StateReport, StateTransition, Button) from trytond.report import Report __all__ = [ 'SaleForceDraft', 'Sale', 'SaleLine', 'SaleReportSummary', 'SaleReportSummaryByParty', 'AddProductForm', 'WizardAddProduct', 'SalePaymentForm', 'SaleUpdateDateStart', 'SaleUpdateDate', 'SaleDetailedStart', 'SaleDetailed', 'SaleDetailedReport', 'WizardSalePayment', 'SaleIncomeDailyStart', 'SaleIncomeDaily', 'SaleIncomeDailyReport','DeleteSalesDraft', ] _ZERO = Decimal('0.00') TODAY = date.today() STATES = { 'readonly': Eval('state').in_(['processing', 'done', 'cancel']), } class Sale(metaclass=PoolMeta): __name__ = 'sale.sale' self_pick_up = fields.Boolean('Self Pick Up', states=STATES, depends=['state'], help='The goods are picked up by the customer before the sale, so no ' 'shipment is created.') pos_create_date = fields.DateTime('Create Date', readonly=True) payments = fields.One2Many('account.statement.line', 'sale', 'Payments') paid_amount = fields.Function(fields.Numeric('Paid Amount', digits=(16,2)), 'get_paid_amount') invoice_number = fields.Char('Invoice Number') invoice_date = fields.Date('Invoice Date') invoice = fields.Many2One('account.invoice', 'Invoice') residual_amount = fields.Function(fields.Numeric('Residual Amount', digits=(16,2), readonly=True), 'get_residual_amount') sale_device = fields.Many2One('sale.device', 'Sale Device', domain=[('shop', '=', Eval('shop'))], depends=['shop'], states=STATES) position = fields.Char('Position', states=STATES) pre_sale = fields.Boolean('Pre-Sale', help="This option for pre-sale, change method of invoice to shipment") shipment_date = fields.Date('Shipment Date', states=STATES) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() sql_table = cls.__table__() super(Sale, cls).__register__(module_name) cursor.execute(*sql_table.update( columns=[sql_table.pos_create_date], values=[sql_table.create_date], where=sql_table.pos_create_date == None)) @classmethod def __setup__(cls): super(Sale, cls).__setup__() cls.party.states['readonly'] = Bool(Eval('invoice_number')) cls.state_string = super(Sale, cls).state.translated('state') for fname in cls.self_pick_up.on_change: if fname not in cls.shop.on_change: cls.shop.on_change.add(fname) if fname not in cls.party.on_change: cls.party.on_change.add(fname) for fname in cls.party.on_change: if fname not in cls.self_pick_up.on_change: cls.self_pick_up.on_change.add(fname) for fname in ( 'invoice_method', 'shipment_method'): fstates = getattr(cls, fname).states if fstates.get('readonly'): fstates['readonly'] = Or(fstates['readonly'], Eval('self_pick_up', False)) else: fstates['readonly'] = Eval('self_pick_up', False) getattr(cls, fname).depends.append('self_pick_up') if hasattr(cls, 'carrier'): if 'invisible' not in cls.carrier.states: cls.carrier.states['invisible'] = Bool(Eval('self_pick_up')) else: invisible = cls.carrier.states['invisible'] cls.carrier.states['invisible'] = Or(invisible, Bool(Eval('self_pick_up'))) cls._error_messages.update({ 'invoice_posted': 'You can not force to draft a invoice paid!', 'not_customer_invoice': ('A customer invoice/refund ' 'from sale device has not been created.'), 'delete_invoice_number': ('The sale "%s" can not be deleted because it has an invoice number "%s"'), 'sale_not_product': ('The sale "%s" dont have product '), }) cls._buttons.update({ 'add_sum': { 'invisible': Eval('state') != 'draft' }, 'wizard_add_product': { 'invisible': Eval('state') != 'draft' }, 'wizard_sale_payment': { 'invisible': Eval('state') == 'done', 'readonly': Not(Bool(Eval('lines'))), }, 'wizard_generate_invoice': { 'invisible': Eval('state').in_(['done', 'processing', 'cancel']), 'readonly': Not(Bool(Eval('lines'))), }, 'wizard_generate_shipment': { 'invisible': Eval('shipment_state').in_(['sent', 'exception']), 'readonly': Not(Bool(Eval('lines'))), }, }) @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' domain = [ bool_op, ('number', operator, value), ('reference', operator, value), ('description', operator, value), ('invoice.number', operator, value), ] return domain @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 @staticmethod def default_invoice_type(): config = Pool().get('sale.configuration')(1) return config.default_invoice_type @classmethod def process(cls, sales): for sale in sales: cls.process_pos(sale) super(Sale, cls).process(sales) @classmethod def process_pos(cls, sale): pool = Pool() Sequence = pool.get('ir.sequence.strict') Date = pool.get('ir.date') Authorization = Pool().get('account.invoice.authorization') # We need to keep that sequence for invoices been taked from shop # configuration instead fiscalyear, because some countries sequences # are setted by shop number = None sequence_id = None if not sale: return invoice = sale.create_invoice() sale.set_invoice_state() sequence_id = sale.get_sequence(sale) if not sale.invoice_number and sale.invoice_method == 'order': if sequence_id: number = Sequence.get_id(sequence_id) sale_date = Date.today() cls.write([sale], { 'invoice_number': number, 'invoice_date': sale_date, }) else: number = sale.invoice_number position = sale.position if sale.position else None sale.state = 'processing' if invoice: if sale.invoice_date: inv_date = sale.invoice_date else: inv_date = Date.today() sale.invoice = invoice.id sale.save() invoice.write([invoice], { 'shop': sale.shop.id, 'invoice_date': inv_date, 'number': number, 'reference': sale.reference or sale.number, 'position': position, }) if sale.invoice_type: invoice.write([invoice], { 'invoice_type': sale.invoice_type, }) authorization_id = None if sale.untaxed_amount_cache > 0: if sale.invoice_type == 'C' and sale.shop.computer_authorization: authorization_id = sale.shop.computer_authorization.id elif sale.invoice_type == 'P' and sale.shop.pos_authorization: authorization_id = sale.shop.pos_authorization.id elif sale.invoice_type == 'M' and sale.shop.manual_authorization: authorization_id = sale.shop.manual_authorization.id elif sale.invoice_type in ['1','2','3'] and sale.shop.electronic_authorization: authorization_id = sale.shop.electronic_authorization.id else: if sale.shop.credit_note_electronic_authorization and sale.invoice_type in ['91', 'N']: authorization_id = sale.shop.credit_note_electronic_authorization.id elif sale.shop.debit_note_electronic_authorization and sale.invoice_type == '92': authorization_id = sale.shop.debit_note_electronic_authorization.id # auths = Authorization.search([ # ('state', '=', 'active'), # ('company', '=', sale.company.id), # ('kind', '=', sale.invoice_type) # ]) if authorization_id: invoice.write([invoice], { 'authorization': authorization_id, }) sale.save() return sale @classmethod def cancel(cls, sales): pool = Pool() Invoice = pool.get('account.invoice') Journal = pool.get('account.journal') super(Sale, cls).cancel(sales) journals = Journal.search([ ('type', '=', 'revenue'), ]) if journals: journal = journals[0] else: return for sale in sales: if not sale.number or not sale.invoice_number: continue sale_invoices = list(sale.invoices) if not sale.invoices: invoice, = Invoice.create([{ 'party': sale.party.id, 'account': sale.party.account_receivable.id, 'invoice_date': sale.invoice_date or sale.sale_date, 'payment_term': sale.payment_term.id, 'salesman': sale.salesman and sale.salesman.id, 'journal': journal.id, 'invoice_address': sale.invoice_address.id, 'number': sale.invoice_number, 'reference': sale.number }]) sale_invoices.append(invoice) Invoice.cancel(sale_invoices) @staticmethod def default_party(): User = Pool().get('res.user') user = User(Transaction().user) return user.shop.party.id if user.shop and user.shop.party else None @fields.depends() def on_change_shop(self): super(Sale, self).on_change_shop() if self.shop: self.self_pick_up = self.shop.self_pick_up self.on_change_self_pick_up() def on_change_party(self): super(Sale, self).on_change_party() if hasattr(self, 'self_pick_up') and self.self_pick_up: self.on_change_self_pick_up() @fields.depends('self_pick_up', 'shop') def on_change_self_pick_up(self): if self.self_pick_up: self.invoice_method = 'order' self.shipment_method = 'order' if self.shop and self.shop.address: self.shipment_address = self.shop.address else: self.on_change_party() self.invoice_method = self.default_invoice_method() self.shipment_method = self.default_shipment_method() @fields.depends('self_pick_up', 'invoice_method', 'pre_sale') def on_change_pre_sale(self): if self.pre_sale == True: self.self_pick_up = False self.invoice_method = 'shipment' else: self.self_pick_up = True self.invoice_method = 'order' @classmethod def view_attributes(cls): return super(Sale, cls).view_attributes() + [ ('//group[@id="full_workflow_buttons"]', 'states', { 'invisible': Eval('self_pick_up', False), })] @classmethod def create(cls, vlist): now = datetime.now() vlist = [x.copy() for x in vlist] for vals in vlist: vals['pos_create_date'] = now return super(Sale, cls).create(vlist) @classmethod def delete(cls, sales): for sale in sales: if sale.invoice_number: cls.raise_user_error('delete_invoice_number', ( sale.rec_name, sale.invoice_number) ) super(Sale, cls).delete(sales) @classmethod @ModelView.button_action('sale_pos.wizard_add_product') def wizard_add_product(cls, sales): # cls.create([{'party': 113, 'price_list': 1, 'agent': 3, 'invoice_address': 108, 'shipment_address': 108, 'lines': [('create', [{'product': 13, 'quantity': '1', 'type': 'line', 'amount_w_tax': 29000, 'unit': 1, 'unit_price': 24369.7479, 'unit_price_w_tax': 29000}])], 'company': 1, 'currency': 31, 'create_uid': 1}]) pass @classmethod @ModelView.button def add_sum(cls, sales): Line = Pool().get('sale.line') lines = [] for sale in sales: line = Line( sale=sale.id, type='subtotal', description='Subtotal', sequence=10000, ) lines.append(line) Line.save(lines) @classmethod def process_sale_pos(cls, sale): # We must set directly state and amounts cache because is faster cls.write([sale], { 'state': 'confirmed', 'untaxed_amount_cache': sale.untaxed_amount, 'tax_amount_cache': sale.tax_amount, 'total_amount_cache': sale.total_amount }) sale = cls.process_pos(sale) return sale @classmethod def update_state(cls, sales): for sale in sales: if not sale.is_done(): continue cls.do([sale]) if sale.invoices: invoice = sale.invoices[0] if not sale.invoice: cls.write([sale], { 'invoice': invoice.id, 'invoice_number': invoice.number, 'invoice_date': invoice.invoice_date, }) @classmethod def do_stock_moves(cls, sales): for sale in sales: sale.create_shipment('out') sale.set_shipment_state() @classmethod def post_invoices(cls, sale): pool = Pool() Date = pool.get('ir.date') Invoice = pool.get('account.invoice') Sequence = pool.get('ir.sequence.strict') invoice = None if sale.invoice: invoice = sale.invoice else: if sale.invoices: invoice = sale.invoices[0] if invoice: if invoice.payment_term.id != sale.payment_term.id: invoice.payment_term = sale.payment_term _invoice_date = sale.invoice_date or Date.today() if invoice.state == 'draft': if not getattr(invoice, 'invoice_date', False): invoice.invoice_date = _invoice_date if not getattr(invoice, 'accounting_date', False): invoice.accounting_date = _invoice_date or date.today() if invoice.invoice_type not in ('C', 'P', 'M'): Invoice.validate_invoice([invoice]) if hasattr(Invoice, 'submit'): Invoice.submit([invoice]) if invoice.invoice_type in ('C', 'P', 'M') or ( hasattr(invoice, 'cufe') and invoice.cufe): if sale.shop.workflow_invoice == 'validated': Invoice.write([invoice], {'state': 'validated'}) else: sale = cls.process_pos(sale) if sale.invoice_number: invoice.number = sale.invoice_number else: sequence_id = sale.get_sequence(sale) if sequence_id: number = Sequence.get_id(sequence_id) sale.invoice_number = number sale.save() invoice.number = number invoice.save() Invoice.post([invoice]) @classmethod def do_reconcile(cls, sales): Reconciliation = Pool().get('account.move.reconciliation') for sale in sales: reconcile_lines = [] account_reconcile_id = None invoice_id = None if not sale.payments or not sale.number: continue try: for invoice in sale.invoices: if invoice.state == 'paid' or not invoice.move: continue invoice_id = invoice.id account_reconcile_id = invoice.account.id for line in invoice.payment_lines: if not line.reconciliation and line.account.id == account_reconcile_id: reconcile_lines.append(line.id) for l in invoice.move.lines: if l.account.id == account_reconcile_id: reconcile_lines.append(l.id) break for st_line in sale.payments: st_line.invoice = invoice_id st_line.save() if not st_line.move: st_line.create_move() for ml in st_line.move.lines: if not ml.reconciliation and ml.account.id == account_reconcile_id: reconcile_lines.append(ml.id) if reconcile_lines: Reconciliation.create([{ 'lines': [('add', reconcile_lines)], 'date': TODAY, }]) if sale.invoice or sale.invoices: sale.state = 'done' sale.save() except Exception: print('Warning: Sale number not processed %s' % sale.number) @classmethod def workflow_to_end(cls, sales): _sales = cls.process_sale_pos(sales[0]) cls.post_invoices(_sales) cls.do_stock_moves([_sales]) cls.do_reconcile([_sales]) cls.update_state([_sales]) @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 result[name][sale.id] += sale.get_total_vouchers_amount() return result @classmethod def get_residual_amount(cls, sales, names): return { n: {s.id: s.total_amount - s.paid_amount for s in sales} for n in names } @classmethod @ModelView.button_action('sale_pos.wizard_sale_payment') def wizard_sale_payment(cls, sales): pass @classmethod @ModelView.button def wizard_generate_invoice(cls, sales): cls.quote(sales) cls.workflow_to_end(sales) @classmethod @ModelView.button def wizard_generate_shipment(cls, sales): cls.do_stock_moves(sales) @classmethod def copy(cls, sales, default=None): if default is None: default = {} default['payments'] = None default['invoice'] = None default['invoice_number'] = None default['invoice_date'] = None if sales: sale = sales[0] if sale.invoice and sale.invoice.number: default['reference'] = sale.invoice.number default['invoice_number'] = None return super(Sale, cls).copy(sales, default) def get_sequence(self, sale): sequence_id = None if sale.untaxed_amount_cache > 0: if sale.invoice_type == 'C' and sale.shop.computer_authorization: sequence_id = sale.shop.computer_authorization.sequence.id elif sale.invoice_type == 'P' and sale.shop.pos_authorization: sequence_id = sale.shop.pos_authorization.sequence.id elif sale.invoice_type == 'M' and sale.shop.manual_authorization: sequence_id = sale.shop.manual_authorization.sequence.id elif sale.invoice_type in ['1','2','3'] and sale.shop.electronic_authorization: sequence_id = sale.shop.electronic_authorization.sequence.id elif sale.shop.invoice_sequence: sequence_id = sale.shop.invoice_sequence.id else: if sale.shop.credit_note_electronic_authorization and sale.invoice_type in ['91', 'N']: sequence_id = sale.shop.credit_note_electronic_authorization.sequence.id elif sale.shop.debit_note_electronic_authorization and sale.invoice_type == '92': sequence_id = sale.shop.debit_note_electronic_authorization.sequence.id else: if sale.shop.credit_note_sequence: sequence_id = sale.shop.credit_note_sequence.id return sequence_id def create_shipment(self, shipment_type): if self.self_pick_up: return self.create_moves_without_shipment(shipment_type) return super(Sale, self).create_shipment(shipment_type) def create_moves_without_shipment(self, shipment_type): pool = Pool() Move = pool.get('stock.move') if not self.self_pick_up: return if self.shipment_method != 'order': return moves = {} for line in self.lines: move = line.get_move(shipment_type) if move: moves[line.id] = move to_create = [] for m in moves: if self.self_pick_up: moves[m].state = 'draft' to_create.append(moves[m]._save_values) Move.create(to_create) Move.do(self.moves) self.set_shipment_state() @fields.depends('lines', 'currency', 'party', 'self_pick_up') def on_change_lines(self): ''' Overrides this method completely if the sale is self pick up to improve performance: Computes untaxed, total and tax amounts from the already computed values in sale lines. ''' self.untaxed_amount = Decimal('0.0') self.tax_amount = Decimal('0.0') self.total_amount = Decimal('0.0') if self.lines: self.untaxed_amount = reduce(lambda x, y: x + y, [(getattr(l, 'amount', None) or Decimal(0)) for l in self.lines if l.type == 'line'], Decimal(0) ) self.total_amount = reduce(lambda x, y: x + y, [(getattr(l, 'amount_w_tax', None) or Decimal(0)) for l in self.lines if l.type == 'line'], Decimal(0) ) if self.currency: self.untaxed_amount = self.currency.round(self.untaxed_amount) self.total_amount = self.currency.round(self.total_amount) self.tax_amount = self.total_amount - self.untaxed_amount if self.currency: self.tax_amount = self.currency.round(self.tax_amount) def get_invoice_number(self, name=None): if self.invoices: return self.invoices[0].number @classmethod def dash_on_change_party(cls, args, ctx={}): if args.get('party'): Party = Pool().get('party.party') party = Party(args.get('party')['id']) address = party.address_get(type='invoice') return { 'invoice_address': {'id': address.id, 'name': address.name}, 'shipment_address': None } class SaleLine(metaclass=PoolMeta): __name__ = 'sale.line' unit_price_w_tax = fields.Function(fields.Numeric('Unit Price with Tax', digits=(16, Eval('_parent_sale', {}).get('currency_digits', Eval('currency_digits', 2))), states={ 'invisible': Eval('type') != 'line', }, depends=['type']), 'get_price_with_tax') amount_w_tax = fields.Function(fields.Numeric('Amount with Tax', digits=(16, Eval('_parent_sale', {}).get('currency_digits', Eval('currency_digits', 2))), states={ 'invisible': ~Eval('type').in_(['line', 'subtotal']), }, depends=['type']), 'get_price_with_tax') unit_price_full = fields.Numeric('Unit Price Full', digits=(16, Eval('_parent_sale', {}).get('currency_digits', Eval('currency_digits', 2))), states={ 'invisible': Eval('type') != 'line', }, depends=['type', 'currency_digits']) @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @staticmethod def default_currency(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.id @classmethod def get_price_with_tax(cls, lines, names): pool = Pool() Tax = pool.get('account.tax') Company = pool.get('company.company') amount_w_tax = {} unit_price_w_tax = {} def compute_amount_with_tax(line): tax_amount = _ZERO amount = _ZERO if line.taxes: tax_list = Tax.compute(line.taxes, line.unit_price or _ZERO, line.quantity or 0.0) tax_amount = sum([t['amount'] for t in tax_list], _ZERO) if line.unit_price: amount = line.unit_price * Decimal(line.quantity) return amount + tax_amount for line in lines: amount = _ZERO unit_price = _ZERO if line.sale: currency = line.sale.currency else: # company = Company(Transaction().context.get('company')) company = Company(1) currency = company.currency if line.type == 'line' and line.quantity: amount = compute_amount_with_tax(line) unit_price = amount / Decimal(str(line.quantity)) # Only compute subtotals if the two fields are provided to speed up elif line.type == 'subtotal' and len(names) == 2: for line2 in line.sale.lines: if line2.type == 'line': amount2 = compute_amount_with_tax(line2) if currency: amount2 = currency.round(amount2) amount += amount2 elif line2.type == 'subtotal': if line == line2: break amount = _ZERO if currency: amount = currency.round(amount) amount_w_tax[line.id] = amount unit_price_w_tax[line.id] = unit_price result = { 'amount_w_tax': amount_w_tax, 'unit_price_w_tax': unit_price_w_tax, } for key in result.keys(): if key not in names: del result[key] return result @fields.depends('type', 'unit_price', 'quantity', 'taxes', 'sale', '_parent_sale.currency', 'product', 'amount') def on_change_with_unit_price_w_tax(self, name=None): if not self.sale: self.sale = Transaction().context.get('sale') return SaleLine.get_price_with_tax([self], ['unit_price_w_tax'])['unit_price_w_tax'][self.id] @fields.depends('type', 'unit_price', 'quantity', 'taxes', 'sale', '_parent_sale.currency', 'product', 'amount') def on_change_with_amount_w_tax(self, name=None): if not self.sale: self.sale = Transaction().context.get('sale') return SaleLine.get_price_with_tax([self], ['amount_w_tax'])['amount_w_tax'][self.id] @fields.depends('unit_price', 'unit_price_full', 'product', 'quantity') def on_change_unit_price_full(self, name=None): pool = Pool() Tax = pool.get('account.tax') if self.product and self.unit_price_full and self.product.template.customer_taxes_used: res = Tax.reverse_compute(self.unit_price_full, self.product.template.customer_taxes_used) self.unit_price = res @fields.depends('product', 'quantity', 'unit', 'taxes', '_parent_sale.currency', '_parent_sale.party', '_parent_sale.sale_date', 'unit_price_full') def on_change_quantity(self): super(SaleLine, self).on_change_quantity() if self.unit_price_full: self.on_change_unit_price_full() @classmethod def dash_on_change_line(cls, args, ctx): if not args.get('quantity') or not args.get('product'): return {} Line = Pool().get('sale.line') Sale = Pool().get('sale.sale') Product = Pool().get('product.product') PriceListLine = Pool().get('product.price_list.line') product_id = args['product']['id'] product = Product(product_id) company_id = ctx.get('company') with Transaction().set_context(ctx): unit_price = product.list_price if args.get('quantity'): quantity = int(args.get('quantity')) else: quantity = 1 if ctx.get('price_list'): price_list_id = ctx.get('price_list')['id'] else: price_list_id = None percent_commission = 0 if price_list_id: price_lines = PriceListLine.search([ ('price_list', '=', price_list_id), ('product', '=', product_id), ]) if price_lines: price_line = price_lines[0] unit_price = float(unit_price) unit_price = Decimal(eval(price_line.formula)) percent_commission = price_line.price_list.percent_commission sale = Sale(company=company_id) taxes_ids = [t.id for t in product.customer_taxes_used] line = Line( sale=sale.id, type='line', taxes=taxes_ids, unit_price=unit_price, quantity=quantity, product=product.id, ) res = cls.get_price_with_tax([line], ['amount_w_tax', 'unit_price_w_tax']) res = { 'unit_price_w_tax': math.ceil(res['unit_price_w_tax'][None]), 'amount_w_tax': math.ceil(res['amount_w_tax'][None]), 'unit_price': math.ceil(unit_price), 'taxes': [[('add'), taxes_ids]], 'unit': product.template.default_uom.id, 'type': 'line', } if percent_commission: res['commission_amount'] = round((((unit_price) * quantity) * percent_commission), 0) return res @classmethod def __setup__(cls): super(SaleLine, cls).__setup__() # Allow edit product, quantity and unit in lines without parent sale for fname in ('product', 'quantity', 'unit'): field = getattr(cls, fname) if field.states.get('readonly'): del field.states['readonly'] @staticmethod def default_sale(): if Transaction().context.get('sale'): return Transaction().context.get('sale') return None @fields.depends('sale') def on_change_product(self): Sale = Pool().get('sale.sale') if not self.sale: sale_id = Transaction().context.get('sale') if sale_id: self.sale = Sale(sale_id) super(SaleLine, self).on_change_product() @fields.depends('sale') def on_change_with_amount(self): if not self.sale: self.sale = Transaction().context.get('sale') return super(SaleLine, self).on_change_with_amount() def get_from_location(self, name): res = super(SaleLine, self).get_from_location(name) if self.sale.self_pick_up: if self.warehouse and self.quantity >= 0: return self.warehouse.storage_location.id return res def get_to_location(self, name): res = super(SaleLine, self).get_to_location(name) if self.sale.self_pick_up: if self.warehouse and self.quantity < 0: return self.warehouse.storage_location.id return res class StatementLine(metaclass=PoolMeta): __name__ = 'account.statement.line' sale = fields.Many2One('sale.sale', 'Sale', ondelete='RESTRICT') class SaleReportSummary(metaclass=PoolMeta): __name__ = 'sale_pos.sales_summary' @classmethod def get_context(cls, records, data): report_context = super(SaleReportSummary, cls).get_context(records, data) sum_untaxed_amount = Decimal(0) sum_tax_amount = Decimal(0) sum_total_amount = Decimal(0) for sale in records: sum_untaxed_amount += sale.untaxed_amount sum_tax_amount += sale.tax_amount sum_total_amount += sale.total_amount report_context['sum_untaxed_amount'] = sum_untaxed_amount report_context['sum_tax_amount'] = sum_tax_amount report_context['sum_total_amount'] = sum_total_amount report_context['company'] = report_context['user'].company return report_context class SaleReportSummaryByParty(metaclass=PoolMeta): __name__ = 'sale_pos.sales_summary_by_party' @classmethod def get_context(cls, records, data): parties = {} report_context = super(SaleReportSummaryByParty, cls).get_context(records, data) report_context['start_date'] = report_context['end_date'] = \ records[0].sale_date if records else None sum_untaxed_amount = Decimal(0) sum_tax_amount = Decimal(0) sum_total_amount = Decimal(0) for sale in records: sum_untaxed_amount += sale.untaxed_amount sum_tax_amount += sale.tax_amount sum_total_amount += sale.total_amount if sale.party.id not in parties.keys(): party = sale.party party.name = sale.party.full_name party.untaxed_amount = sale.untaxed_amount party.tax_amount = sale.tax_amount party.total_amount = sale.total_amount party.currency = sale.currency else: party = parties.get(sale.party.id) party.untaxed_amount += sale.untaxed_amount party.tax_amount += sale.tax_amount party.total_amount += sale.total_amount parties[sale.party.id] = party if sale.sale_date: if not report_context['start_date'] or report_context['start_date'] > sale.sale_date: report_context['start_date'] = sale.sale_date if not report_context['end_date'] or report_context['end_date'] < sale.sale_date: report_context['end_date'] = sale.sale_date report_context['parties'] = parties.values() report_context['sum_untaxed_amount'] = sum_untaxed_amount report_context['sum_tax_amount'] = sum_tax_amount report_context['sum_total_amount'] = sum_total_amount report_context['company'] = report_context['user'].company return report_context class AddProductForm(ModelView): 'Add Product Form' __name__ = 'sale_pos.add_product_form' sale = fields.Many2One('sale.sale', 'Sale') lines = fields.One2Many('sale.line', None, 'Lines', context={'sale': Eval('sale')}, depends=['sale'],) class WizardAddProduct(Wizard): 'Wizard Add Product' __name__ = 'sale_pos.add_product' start = StateView('sale_pos.add_product_form', 'sale_pos.add_product_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Add and New', 'add_new_', 'tryton-go-jump', default=True), Button('Add', 'add_', 'tryton-ok'), ]) add_new_ = StateTransition() add_ = StateTransition() def default_start(self, fields): return { 'sale': Transaction().context.get('active_id'), } def add_lines(self): for line in self.start.lines: line.sale = Transaction().context.get('active_id', False) line.save() def transition_add_new_(self): self.add_lines() return 'start' def transition_add_(self): self.add_lines() return 'end' class SaleForceDraft(Wizard): 'Sale Force Draft' __name__ = 'sale_pos.force_draft' start_state = 'force_draft' force_draft = StateTransition() @classmethod def __setup__(cls): super(SaleForceDraft, cls).__setup__() cls._error_messages.update({ 'with_electronic_invoice': 'The electronic invoice already exist!', }) def transition_force_draft(self): sale_table = Table('sale_sale') invoice_table = Table('account_invoice') move_table = Table('account_move') stock_move_table = Table('stock_move') Sale = Pool().get('sale.sale') Date = Pool().get('ir.date') cursor = Transaction().connection.cursor() ids = Transaction().context['active_ids'] if not ids: return 'end' sales = Sale.browse(ids) for sale in sales: # if sale.sale_date != Date.today(): # return 'end' # The payments must be delete for pay in sale.payments: if pay.statement.state != 'draft': return 'end' if pay.move: self.unreconcile_move(pay.move) pay.move.delete([pay.move]) pay.delete([pay]) # The invoices must be delete for invoice in sale.invoices: if (hasattr(invoice, 'cufe') and invoice.cufe) or \ hasattr(invoice, 'electronic_state') and \ invoice.electronic_state == 'submitted': self.raise_user_error('with_electronic_invoice') if invoice.state == 'paid': self.unreconcile_move(invoice.move) if invoice.move: cursor.execute(*move_table.update( columns=[move_table.state], values=['draft'], where=move_table.id == invoice.move.id) ) cursor.execute(*move_table.delete( where=move_table.id == invoice.move.id) ) cursor.execute(*invoice_table.update( columns=[invoice_table.state, invoice_table.number], values=['validate', None], where=invoice_table.id == invoice.id) ) cursor.execute(*invoice_table.delete( where=invoice_table.id == invoice.id) ) if sale.id: cursor.execute(*sale_table.update( columns=[sale_table.state], values=['draft'], where=sale_table.id == sale.id) ) # The stock moves must be delete stock_moves = [m.id for line in sale.lines for m in line.moves] if stock_moves: cursor.execute(*stock_move_table.update( columns=[stock_move_table.state], values=['draft'], where=stock_move_table.id.in_(stock_moves) )) cursor.execute(*stock_move_table.delete( where=stock_move_table.id.in_(stock_moves)) ) return 'end' def unreconcile_move(self, move): Reconciliation = Pool().get('account.move.reconciliation') reconciliations = [l.reconciliation for l in move.lines if l.reconciliation] if reconciliations: Reconciliation.delete(reconciliations) class SaleUpdateDateStart(ModelView): 'Sale Update Date Start' __name__ = 'sale_pos.sale_update_date.start' new_date = fields.Date('New Date', required=True) class SaleUpdateDate(Wizard): 'Sale Update Date' __name__ = 'sale_pos.sale_update_date' start = StateView('sale_pos.sale_update_date.start', 'sale_pos.sale_update_date_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Ok', 'accept', 'tryton-ok', default=True), ]) accept = StateTransition() def transition_accept(self): sale = Table('sale_sale') id_ = Transaction().context['active_id'] cursor = Transaction().connection.cursor() if id_: cursor.execute(*sale.update( columns=[sale.sale_date], values=[self.start.new_date], where=sale.id == id_) ) return 'end' class SaleDetailedStart(ModelView): 'Sale Detailed Start' __name__ = 'sale_pos.sale_detailed.start' start_date = fields.Date('Start Date', required=True) end_date = fields.Date('End Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) salesman = fields.Many2One('company.employee', 'Salesman') party = fields.Many2One('party.party', 'Party') product = fields.Many2One('product.product', 'Product') shop = fields.Many2One('sale.shop', 'Shop') @staticmethod def default_company(): return Transaction().context.get('company') class SaleDetailed(Wizard): 'Sale Detailed' __name__ = 'sale_pos.sale_detailed' start = StateView('sale_pos.sale_detailed.start', 'sale_pos.sale_detailed_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Print', 'print_', 'tryton-ok', default=True), ]) print_ = StateAction('sale_pos.report_sale_detailed') @classmethod def __setup__(cls): super(SaleDetailed, cls).__setup__() # cls._error_messages.update({ # 'sale_not_product': ('The sale "%s" dont have product '), # }) def do_print_(self, action): salesman_id = None party_id = None product_id = None shop_id = None if self.start.salesman: salesman_id = self.start.salesman.id if self.start.shop: shop_id = self.start.shop.id if self.start.party: party_id = self.start.party.id if self.start.product: product_id = self.start.product.id data = { 'company': self.start.company.id, 'start_date': self.start.start_date, 'end_date': self.start.end_date, 'salesman': salesman_id, 'party': party_id, 'product': product_id, 'shop': shop_id, } return action, data def transition_print_(self): return 'end' class SaleDetailedReport(Report): __name__ = 'sale_pos.report_sale_detailed' @classmethod def get_context(cls, records, data): report_context = super(SaleDetailedReport, cls).get_context(records, data) pool = Pool() Sale = pool.get('sale.sale') SaleLine = pool.get('sale.line') Company = pool.get('company.company') sales_line_dom = [ ('sale.sale_date', '>=', data['start_date']), ('sale.sale_date', '<=', data['end_date']), ('sale.company', '=', data['company']), ('sale.state', 'in', ['processing', 'done']), ] if data['salesman']: sales_line_dom.append( ('sale.salesman', '=', data['salesman']) ) if data['shop']: sales_line_dom.append( ('sale.shop', '=', data['shop']) ) if data['party']: sales_line_dom.append( ('sale.party', '=', data['party']) ) if data['product']: # lines = SaleLine.search([ # ('product', '=', data['product']) # ]) # line_ids = [l.id for l in lines] sales_line_dom.append( ('product', '=', data['product']) ) start_lines = SaleLine.search(sales_line_dom, order=[('sale.sale_date', 'ASC')]) lines = [] amount = [] amount_w_tax = [] for line in start_lines: if line.type == 'line' and not line.product: Sale.raise_user_error('sale_not_product', line.sale.number) is_invoiced = False if line.sale.invoices: for inv in line.sale.invoices: if inv.state not in ['draft', 'cancel']: is_invoiced = True if is_invoiced: lines.append(line) amount.append(line.amount) amount_w_tax.append(line.amount_w_tax) else: lines.append(line) amount.append(line.amount) amount_w_tax.append(line.amount_w_tax) report_context['amount'] = sum(amount) report_context['amount_w_tax'] = sum(amount_w_tax) report_context['records'] = lines report_context['company'] = Company(data['company']) return report_context 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 = fields.Numeric('Payment amount', required=True, digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']) currency_digits = fields.Integer('Currency Digits') party = fields.Many2One('party.party', 'Party', readonly=True) require_voucher = fields.Boolean('Require Voucher', depends=['journal']) voucher = fields.Char('Voucher Number', states={ 'required': Eval('require_voucher', False), 'invisible': Not(Eval('require_voucher', False)), }, depends=['require_voucher']) self_pick_up = fields.Boolean('Self Pick Up', readonly=True) do_invoice = fields.Boolean('Do Invoice') @fields.depends('journal', 'voucher') def on_change_with_require_voucher(self): if self.journal: return self.journal.require_voucher else: return False @classmethod def default_require_voucher(cls): return False @classmethod def default_do_invoice(cls): return True class WizardSalePayment(Wizard): 'Wizard Sale Payment' __name__ = 'sale.payment' start = StateView('sale.payment.form', 'sale_pos.sale_payment_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Pay', 'pay_', 'tryton-ok', default=True), ]) pay_ = StateTransition() @classmethod def __setup__(cls): super(WizardSalePayment, cls).__setup__() cls._error_messages.update({ 'not_sale_device': ('You have not defined a sale device for ' 'your user.'), 'not_draft_statement': ('A draft statement for "%s" payments ' 'has not been created.'), 'party_without_account_receivable': 'Party %s has no any ' 'account receivable defined. Please, assign one.', }) 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: self.raise_user_error('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_digits': sale.currency_digits, 'party': sale.party.id, } def transition_pay_(self): pool = Pool() User = pool.get('res.user') user = User(Transaction().user) Sale = pool.get('sale.sale') Statement = pool.get('account.statement') StatementLine = pool.get('account.statement.line') active_id = Transaction().context.get('active_id', False) sale = Sale(active_id) form = self.start device_id = user.sale_device.id if user.sale_device else sale.sale_device.id statements = Statement.search([ ('journal', '=', form.journal), ('state', '=', 'draft'), ('sale_device', '=', device_id), ], order=[('date', 'DESC')]) if not statements: self.raise_user_error('not_draft_statement', (form.journal.name,)) if not sale.number: Sale.set_number([sale]) account = (sale.party.account_receivable and sale.party.account_receivable.id or self.raise_user_error('party_without_account_receivable', error_args=(sale.party.name,))) if form.payment_amount: payment = StatementLine( statement=statements[0].id, date=date.today(), amount=form.payment_amount, party=sale.party.id, account=account, description=self.start.voucher, sale=active_id, # number=self.start.voucher, # voucher=self.start.voucher, ) payment.save() if sale.total_amount != sale.paid_amount: return 'start' sale.save() if self.start.do_invoice: # for inv in sale.invoices: # if inv.state == 'posted': # inv.write([inv], {'state': 'draft'}) Sale.workflow_to_end([sale]) return 'end' class SaleIncomeDailyStart(ModelView): 'Sale Income Daily Start' __name__ = 'sale_pos.sale_income_daily.start' company = fields.Many2One('company.company', 'Company', required=True) date = fields.Date('Date', required=True) shop = fields.Many2One('sale.shop', 'Shop') user = fields.Many2One('res.user', 'User') # journal = fields.Many2One('account.statement.journal', 'Journal') @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_shop(): return Transaction().context.get('shop') @staticmethod def default_user(): return Transaction().user @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() class SaleIncomeDaily(Wizard): 'Sale Income Daily' __name__ = 'sale_pos.sale_income_daily' start = StateView('sale_pos.sale_income_daily.start', 'sale_pos.sale_income_daily_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Print', 'print_', 'tryton-ok', default=True), ]) print_ = StateReport('sale_pos.sale_income_daily_report') def do_print_(self, action): shop_id, user_id = None, None if self.start.user: user_id = self.start.user.id if self.start.shop: shop_id = self.start.shop.id report_context = { 'company': self.start.company.id, 'date': self.start.date, 'user': user_id, 'shop': shop_id, } return action, report_context def transition_print_(self): return 'end' class SaleIncomeDailyReport(Report): 'Income Daily Report' __name__ = 'sale_pos.sale_income_daily_report' @classmethod def get_context(cls, records, data): report_context = super(SaleIncomeDailyReport, cls).get_context(records, data) pool = Pool() Invoice = pool.get('account.invoice') Voucher = pool.get('account.voucher') Sale = pool.get('sale.sale') Company = pool.get('company.company') company = Company(data['company']) Statement = pool.get('account.statement') Shop = pool.get('sale.shop') User = pool.get('res.user') dom_statement = [ ('date', '=', data['date']), ] if data['shop']: dom_statement.append(('sale_device.shop.id', '=', data['shop'])) statements = Statement.search(dom_statement) user_id = Transaction().user user = User(user_id) records = [] advances = [] statements_ = [] total_advances = [] advances_cash = [] advances_electronic = [] total_statements = [] statement_cash = [] statement_electronic = [] for statement in statements: st_amount = sum(l.amount for l in statement.lines) statements_.append({ 'id': statement.id, 'sale_device': statement.sale_device.name, 'journal': statement.journal.name, 'turn': statement.turn, 'total_amount': st_amount, 'state': statement.state, }) total_statements.append(st_amount) for l in statement.lines: if l.statement.journal.kind == 'cash': statement_cash.append(l.amount) else: statement_electronic.append(l.amount) advances_voucher = [] # dom_sales = [ # ('sale_date', '=', data['date']), # ('vouchers', '!=', None) # ] # if data['shop']: # dom_sales.append( # ('shop', '=', data['shop']), # ) # sales = Sale.search(dom_sales) # voucher_ids = [] # for sale in sales: # for v in sale.vouchers: # cash, electronic = 0,0 # if v.payment_mode.payment_type == 'cash': # advances_cash.append(v.amount_to_pay) # cash = v.amount_to_pay # else: # electronic = v.amount_to_pay # advances_electronic.append(v.amount_to_pay) # voucher_ids.append(v.id) # advances_voucher.append(v.amount_to_pay) # advances.append({ # 'number': v.number, # 'reference': sale.number, # 'party': v.party.name, # 'total_amount': v.amount_to_pay, # 'payment_mode': v.payment_mode.name, # 'cash': cash, # 'electronic': electronic, # }) # total_advances.append(v.amount_to_pay) dom_vouchers = [ ('move', '!=', None), ('move.post_date', '=', data['date']), ] if data['user']: dom_vouchers.append( ('create_uid', '=', data['user']), ) # if voucher_ids: # dom_vouchers.append( # ('id', 'not in', voucher_ids) # ) vouchers = Voucher.search(dom_vouchers) for v in vouchers: cash = 0 electronic = 0 for l in v.lines: if v.payment_mode.payment_type == 'cash': advances_cash.append(l.amount) cash = l.amount else: advances_electronic.append(l.amount) electronic = l.amount advances_voucher.append(l.amount) advances.append({ 'number': l.voucher.number, 'reference': l.detail, 'party': l.party.name if l.party else l.voucher.party.name, 'total_amount': l.amount, 'payment_mode': l.voucher.payment_mode.name, 'cash': cash, 'electronic': electronic, }) total_advances.append(l.amount) dom_invoices = [ ('company', '=', data['company']), ('invoice_date', '=', data['date']), ('number', '!=', None), ('state', 'in', ['posted', 'paid', 'canceled']), ] shop_names = '' if data['shop']: shop_names = Shop(data['shop']).name dom_invoices.append( ('shop', '=', data['shop']) ) else: shops = Shop.search([]) for s in shops: shop_names += s.name + ', ' invoices = Invoice.search(dom_invoices, order=[('number', 'ASC')]) invoices_number = [] total_invoices_cash = [] total_invoices_electronic = [] total_invoices_credit = [] total_invoices_paid = [] total_invoices_amount = [] for invoice in invoices: invoices_number.append(invoice.number) cash = 0 electronic = 0 credit = 0 total_invoices_amount.append(invoice.total_amount) sale = invoice.sales[0] if not sale.payments: total_invoices_credit.append(invoice.amount_to_pay) credit = invoice.amount_to_pay else: for p in sale.payments: if p.statement.date == data['date']: if p.statement.journal.kind == 'cash': cash += p.amount total_invoices_cash.append(p.amount) else: electronic += p.amount total_invoices_electronic.append(p.amount) total_invoices_paid.append(p.amount) inv = { 'number': invoice.number, 'reference': invoice.reference, 'party': invoice.party.name, 'total_amount': invoice.total_amount, 'credit': credit, 'cash': cash, 'electronic': electronic, 'paid': cash + electronic, 'state': invoice.state_string, } records.append(inv) advances_cash_ = sum(advances_cash) advances_electronic_ = sum(advances_electronic) statement_cash_ = sum(statement_cash) statement_electronic_ = sum(statement_electronic) report_context['records'] = records report_context['advances'] = advances report_context['statements'] = statements_ report_context['date'] = data['date'] report_context['company'] = company.party.name report_context['shop'] = shop_names report_context['user'] = user.name report_context['print_date'] = datetime.now() report_context['statement_cash'] = statement_cash_ report_context['statement_electronic'] = statement_electronic_ report_context['total_invoices_amount'] = sum(total_invoices_amount) report_context['total_invoices_cash'] = sum(total_invoices_cash) report_context['total_invoices_electronic'] = sum(total_invoices_electronic) report_context['total_invoices_credit'] = sum(total_invoices_credit) report_context['total_invoices_paid'] = sum(total_invoices_paid) report_context['total_advances'] = sum(total_advances) report_context['advances_cash'] = advances_cash_ report_context['advances_electronic'] = advances_electronic_ report_context['advances_voucher'] = sum(advances_voucher) report_context['total_statements'] = sum(total_statements) report_context['start_invoice'] = min(invoices_number) if invoices_number else '' report_context['end_invoice'] = max(invoices_number) if invoices_number else '' report_context['total_cash'] = advances_cash_ + statement_cash_ report_context['total_electronic'] = advances_electronic_ + statement_electronic_ return report_context class DeleteSalesDraft(Wizard): 'Delete Sales Draft' __name__ = 'sale_pos.delete_sales_draft' start_state = 'delete_sales_draft' delete_sales_draft = StateTransition() @classmethod def __setup__(cls): super(DeleteSalesDraft, cls).__setup__() def transition_delete_sales_draft(self): Sale = Pool().get('sale.sale') sales = Sale.search(['AND', ['OR', [ ('state', '=', 'draft'), ('invoice_number', '=', ''), ],[ ('state', '=', 'draft'), ('invoice_number', '=', None), ], ]]) sales_delete = [] for sale in sales: if sale.lines: continue if sale.payments: continue sales_delete.append(sale) Sale.delete(sales_delete) return 'end'