# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from trytond.i18n import gettext from trytond.model import Index, ModelSQL, ModelView, Workflow, fields from trytond.modules.product import price_digits from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, If from trytond.transaction import Transaction from .exceptions import AmendmentValidateError class Sale(metaclass=PoolMeta): __name__ = 'sale.sale' amendments = fields.One2Many( 'sale.amendment', 'sale', "Amendments", states={ 'invisible': ( ~Eval('state').in_(['processing', 'done']) | ((Eval('state') == 'done') & ~Eval('amendments'))), 'readonly': Eval('state') != 'processing', }) @classmethod def copy(cls, sales, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('amendments') return super().copy(sales, default=default) class Amendment(Workflow, ModelSQL, ModelView): __name__ = 'sale.amendment' sale = fields.Many2One( 'sale.sale', "Sale", required=True, domain=[ ('state', 'in', ['processing', 'done']), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }) date = fields.Date( "Date", required=True, states={ 'readonly': Eval('state') != 'draft', }) description = fields.Char( "Description", states={ 'readonly': Eval('state') != 'draft', }) state = fields.Selection([ ('draft', "Draft"), ('validated', "Validated"), ], "State", readonly=True, required=True, sort=False) lines = fields.One2Many( 'sale.amendment.line', 'amendment', "Lines", states={ 'readonly': ( (Eval('state') != 'draft') | ~Eval('sale')), }) @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_indexes.add( Index( t, (t.state, Index.Equality(cardinality='low')), where=t.state == 'draft')) cls._order = [ ('date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= { ('draft', 'validated'), } cls._buttons.update({ 'validate_amendment': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, }) @classmethod def default_date(cls): return Pool().get('ir.date').today() @classmethod def default_state(cls): return 'draft' @classmethod @ModelView.button @Workflow.transition('validated') def validate_amendment(cls, amendments): pool = Pool() Sale = pool.get('sale.sale') transaction = Transaction() context = transaction.context sales = set() for amendment in amendments: sale = amendment.sale if sale in sales: raise AmendmentValidateError( gettext('sale_amendment.msg_one_sale_at_time', sale=sale.rec_name)) sales.add(sale) sale.revision += 1 for line in amendment.lines: line.apply(sale) # Force saved lines sale.lines = sale.lines Sale.save(sales) Sale.store_cache(sales) cls._clear_sale(sales) with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) @classmethod def _clear_sale(cls, sales): pool = Pool() Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') Shipment = pool.get('stock.shipment.out') ShipmentReturn = pool.get('stock.shipment.out.return') Move = pool.get('stock.move') invoices = set() shipments = set() shipment_returns = set() invoice_lines = set() moves = set() for sale in sales: for invoice in sale.invoices: if invoice.state == 'draft': invoices.add(invoice) for shipment in sale.shipments: if shipment.state in ['draft', 'waiting']: shipments.add(shipment) for shipment_return in sale.shipment_returns: if shipment_return.state == 'draft': shipment_returns.add(shipment_return) for line in sale.lines: for invoice_line in line.invoice_lines: if invoice_line.invoice_state == 'draft': invoice_lines.add(invoice_line) moves.update(cls._stock_moves(line)) shipments_waiting = [s for s in shipments if s.state == 'waiting'] Shipment.draft(shipments) # Clear inventory moves InvoiceLine.delete(invoice_lines) Move.delete(moves) Invoice.update_taxes([i for i in invoices if i.lines]) Invoice.delete([i for i in invoices if not i.lines]) Shipment.wait([s for s in shipments_waiting if s.outgoing_moves]) Shipment.delete([s for s in shipments if not s.outgoing_moves]) ShipmentReturn.delete( [s for s in shipment_returns if not s.incoming_moves]) @classmethod def _stock_moves(cls, line): for move in line.moves: if move.state in {'staging', 'draft'}: yield move class AmendmentLine(ModelSQL, ModelView): __name__ = 'sale.amendment.line' amendment = fields.Many2One( 'sale.amendment', "Amendment", required=True, ondelete='CASCADE') state = fields.Function(fields.Selection( 'get_states', "State"), 'on_change_with_state') action = fields.Selection([ ('taxes', "Recompute Taxes"), ('payment_term', "Change Payment Term"), ('party', "Change Parties and Addresses"), ('warehouse', "Change Warehouse"), ('line', "Change Line"), ], "Action", required=True, states={ 'readonly': Eval('state') != 'draft', }) sale = fields.Function(fields.Many2One( 'sale.sale', "Sale"), 'on_change_with_sale') line = fields.Many2One( 'sale.line', "Line", domain=[ ('type', '=', 'line'), ('sale', '=', Eval('sale', -1)), ], states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'line', 'required': Eval('action') == 'line', }) payment_term = fields.Many2One( 'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT', states={ 'invisible': Eval('action') != 'payment_term', }) party = fields.Many2One( 'party.party', "Party", states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'party', 'required': Eval('action') == 'party', }) invoice_party = fields.Many2One( 'party.party', "Invoice Party", states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'party', }, search_context={ 'related_party': Eval('party'), }) invoice_address = fields.Many2One( 'party.address', "Invoice Address", domain=[ ('party', '=', If(Eval('invoice_party'), Eval('invoice_party', -1), Eval('party', -1))), ], states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'party', 'required': Eval('action') == 'party', }) shipment_party = fields.Many2One( 'party.party', "Shipment Party", states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'party', }, search_context={ 'related_party': Eval('party'), }) shipment_address = fields.Many2One( 'party.address', "Shipment Address", domain=['OR', ('party', '=', If(Eval('shipment_party'), Eval('shipment_party', -1), Eval('party', -1))), ('warehouses', '=', Eval('warehouse', -1)), ], states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'party', 'required': Eval('action') == 'party', }) warehouse = fields.Many2One( 'stock.location', "Warehouse", domain=[ ('type', '=', 'warehouse'), ], states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'warehouse', 'required': Eval('action') == 'warehouse', }) product = fields.Many2One( 'product.product', "Product", domain=[ If((Eval('state') == 'draft') & ~(Eval('quantity', 0) < 0), ('salable', '=', True), ()), ('default_uom_category', '=', Eval('product_uom_category', -1)), ], states={ 'readonly': ( (Eval('state') != 'draft') | ~Eval('product_uom_category', None)), 'invisible': Eval('action') != 'line', }) quantity = fields.Float( "Quantity", digits='unit', states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'line', 'required': Eval('action') == 'line', }) unit = fields.Many2One( 'product.uom', "Unit", ondelete='RESTRICT', states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'line', 'required': Bool(Eval('product')), }) unit_price = fields.Numeric( "Unit Price", digits=price_digits, states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'line', 'required': Eval('action') == 'line', }) description = fields.Text( "Description", states={ 'readonly': Eval('state') != 'draft', 'invisible': Eval('action') != 'line', }) product_uom_category = fields.Function( fields.Many2One('product.uom.category', "Product UoM Category"), 'on_change_with_product_uom_category') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('amendment') unit_categories = cls._unit_categories() cls.unit.domain = [ ('category', 'in', [Eval(c, -1) for c in unit_categories]), ] @classmethod def _unit_categories(cls): return ['product_uom_category'] @fields.depends( 'amendment', '_parent_amendment.sale', '_parent_amendment._parent_sale.payment_term', '_parent_amendment._parent_sale.party', '_parent_amendment._parent_sale.invoice_party', '_parent_amendment._parent_sale.invoice_address', '_parent_amendment._parent_sale.shipment_party', '_parent_amendment._parent_sale.shipment_address', '_parent_amendment._parent_sale.warehouse') def on_change_amendment(self): if self.amendment and self.amendment.sale: self.payment_term = self.amendment.sale.payment_term self.party = self.amendment.sale.party self.invoice_party = self.amendment.sale.invoice_party self.invoice_address = self.amendment.sale.invoice_address self.shipment_party = self.amendment.sale.shipment_party self.shipment_address = self.amendment.sale.shipment_address self.warehouse = self.amendment.sale.warehouse @fields.depends(methods=['on_change_amendment']) def on_change_state(self): self.on_change_amendment() @classmethod def get_states(cls): pool = Pool() Amendment = pool.get('sale.amendment') return Amendment.fields_get(['state'])['state']['selection'] @fields.depends( 'amendment', '_parent_amendment.state') def on_change_with_state(self, name=None): if self.amendment: return self.amendment.state @fields.depends(methods=['on_change_line', 'on_change_amendment']) def on_change_action(self): self.line = None self.on_change_line() self.on_change_amendment() @fields.depends( 'amendment', '_parent_amendment.sale') def on_change_with_sale(self, name=None): return self.amendment.sale if self.amendment else None @fields.depends('line') def on_change_line(self): if self.line: self.product = self.line.product self.quantity = self.line.quantity self.unit = self.line.unit self.unit_price = self.line.unit_price self.description = self.line.description else: self.product = self.quantity = self.unit = self.unit_price = None self.description = None @fields.depends('party', 'invoice_party', 'shipment_party', 'warehouse') def on_change_party(self): if not self.invoice_party: self.invoice_address = None if not self.shipment_party: self.shipment_address = None if self.party: if not self.invoice_address: self.invoice_address = self.party.address_get(type='invoice') if not self.shipment_party: with Transaction().set_context( warehouse=( self.warehouse.id if self.warehouse else None)): self.shipment_address = self.party.address_get( type='delivery') @fields.depends('party', 'invoice_party') def on_change_invoice_party(self): if self.invoice_party: self.invoice_address = self.invoice_party.address_get( type='invoice') elif self.party: self.invoice_address = self.party.address_get(type='invoice') @fields.depends('party', 'shipment_party', 'warehouse') def on_change_shipment_party(self): with Transaction().set_context( warehouse=self.warehouse.id if self.warehouse else None): if self.shipment_party: self.shipment_address = self.shipment_party.address_get( type='delivery') elif self.party: self.shipment_address = self.party.address_get(type='delivery') @fields.depends('line') def on_change_with_product_uom_category(self, name=None): if self.line: if self.line.product_uom_category: return self.line.product_uom_category elif self.line.unit: return self.line.unit.category def apply(self, sale): assert self.sale == sale sale_line = None if self.line: for line in sale.lines: if self.line == line: sale_line = line break getattr(self, '_apply_%s' % self.action)(sale, sale_line) def _apply_taxes(self, sale, sale_line): for line in sale.lines: if line.product: line.taxes = line.compute_taxes(sale.party) def _apply_payment_term(self, sale, sale_line): sale.payment_term = self.payment_term def _apply_party(self, sale, sale_line): sale.party = self.party sale.invoice_party = self.invoice_party sale.invoice_address = self.invoice_address sale.shipment_party = self.shipment_party sale.shipment_address = self.shipment_address def _apply_warehouse(self, sale, sale_line): sale.warehouse = self.warehouse def _apply_line(self, sale, sale_line): sale_line.product = self.product sale_line.quantity = self.quantity sale_line.unit = self.unit sale_line.unit_price = self.unit_price sale_line.description = self.description