diff --git a/CHANGELOG b/CHANGELOG index 5c04f2c566..948d778f9b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Manage negative amounts in sale * Add cache on amount fields in readonly states * Confirmed state split into Confirmed and Processing * Add a Function field delivery_date to sale line diff --git a/doc/index.rst b/doc/index.rst index a9074650e2..0435a0699d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -76,6 +76,7 @@ optional or completed with sensible default values: - Invoices: The list of related invoices. - Moves: The list of related stock moves. - Shipments: The list of related shipments. +- Return Shipments: The list of related shipments return. The *Sale* report allow to print the sale orders or to send them by mail. diff --git a/sale.py b/sale.py index e1b5ca7f80..98dacadeca 100644 --- a/sale.py +++ b/sale.py @@ -148,7 +148,10 @@ class Sale(Workflow, ModelSQL, ModelView): ('exception', 'Exception'), ], 'Shipment State', readonly=True, required=True) shipments = fields.Function(fields.One2Many('stock.shipment.out', None, - 'Shipments'), 'get_function_fields') + 'Shipments'), 'get_shipments') + shipment_returns = fields.Function( + fields.One2Many('stock.shipment.out.return', None, 'Shipment Returns'), + 'get_shipment_returns') moves = fields.Function(fields.One2Many('stock.move', None, 'Moves'), 'get_function_fields') @@ -444,8 +447,6 @@ class Sale(Workflow, ModelSQL, ModelView): res['currency_digits'] = self.get_currency_digits(sales) if 'party_lang' in names: res['party_lang'] = self.get_party_lang(sales) - if 'shipments' in names: - res['shipments'] = self.get_shipments(sales) if 'moves' in names: res['moves'] = self.get_moves(sales) return res @@ -543,23 +544,23 @@ class Sale(Workflow, ModelSQL, ModelView): 'invoice_state': state, }) - def get_shipments(self, sales): - ''' - Return shipment_out ids for each sales + def get_shipments_returns(attribute): + "Computes the returns or shipments" + def method(self, ids, name): + shipments = {} + for sale in self.browse(ids): + shipments[sale.id] = [] + for line in sale.lines: + for move in line.moves: + ship_or_return = getattr(move, attribute) + if bool(ship_or_return): + if ship_or_return.id not in shipments[sale.id]: + shipments[sale.id].append(ship_or_return.id) + return shipments + return method - :param sales: a BrowseRecordList of sales - :return: a dictionary with sale id as key and - a list of shipment_out id as value - ''' - res = {} - for sale in sales: - res[sale.id] = [] - for line in sale.lines: - for move in line.moves: - if move.shipment_out: - if move.shipment_out.id not in res[sale.id]: - res[sale.id].append(move.shipment_out.id) - return res + get_shipments = get_shipments_returns('shipment_out') + get_shipment_returns = get_shipments_returns('shipment_out_return') def get_moves(self, sales): ''' @@ -647,7 +648,11 @@ class Sale(Workflow, ModelSQL, ModelView): if not sale.invoice_address or not sale.shipment_address: self.raise_user_error('addresses_required') for line in sale.lines: - if (not line.from_location + if line.quantity >= 0: + location = line.from_location + else: + location = line.to_location + if (not location and line.product and line.product.type in ('goods', 'assets')): self.raise_user_error('warehouse_required') @@ -685,31 +690,24 @@ class Sale(Workflow, ModelSQL, ModelView): 'total_amount_cache': sale.total_amount, }) - def _get_invoice_line_sale_line(self, sale): + def _get_invoice_line_sale_line(self, sale, invoice_type): ''' - Return invoice line values for each sale lines - - :param sale: the BrowseRecord of the sale - - :return: a dictionary with invoiced sale line id as key - and a list of invoice lines values as value + Return invoice line values for each sale lines according to + invoice_type ''' line_obj = Pool().get('sale.line') res = {} for line in sale.lines: + if (invoice_type == 'out_invoice') != (line.quantity >= 0): + continue val = line_obj.get_invoice_line(line) if val: res[line.id] = val return res - def _get_invoice_sale(self, sale): + def _get_invoice_sale(self, sale, invoice_type): ''' - Return invoice values for sale - - :param sale: the BrowseRecord of the sale - - :return: a dictionary with invoice fields as key and - invoice values as value + Return invoice values of type invoice_type for sale ''' journal_obj = Pool().get('account.journal') @@ -721,7 +719,7 @@ class Sale(Workflow, ModelSQL, ModelView): res = { 'company': sale.company.id, - 'type': 'out_invoice', + 'type': invoice_type, 'reference': sale.reference, 'journal': journal_id, 'party': sale.party.id, @@ -732,9 +730,9 @@ class Sale(Workflow, ModelSQL, ModelView): } return res - def create_invoice(self, sale): + def create_invoice(self, sale, invoice_type): ''' - Create an invoice for the sale and return the id + Create an invoice of type invoice_type for the sale and return the id ''' pool = Pool() invoice_obj = pool.get('account.invoice') @@ -745,12 +743,12 @@ class Sale(Workflow, ModelSQL, ModelView): self.raise_user_error('missing_account_receivable', error_args=(sale.party.rec_name,)) - invoice_lines = self._get_invoice_line_sale_line(sale) + invoice_lines = self._get_invoice_line_sale_line(sale, invoice_type) if not invoice_lines: return - vals = self._get_invoice_sale(sale) + vals = self._get_invoice_sale(sale, invoice_type) with Transaction().set_user(0, set_context=True): invoice_id = invoice_obj.create(vals) @@ -773,19 +771,21 @@ class Sale(Workflow, ModelSQL, ModelView): }) return invoice_id - def _get_move_sale_line(self, sale): + def _get_move_sale_line(self, sale, shipment_type): ''' - Return a dictionary of move values for each sale lines - - :param sale: the BrowseRecord of the sale - - :return: a dictionary with move as key and move values as value + Return a dictionary of move values for each sale lines of the right + shipment_type ''' line_obj = Pool().get('sale.line') res = {} for line in sale.lines: val = line_obj.get_move(line) - if val: + if not val: + continue + out_move = (val['to_location'] + == line.sale.party.customer_location.id) + out_shipment = shipment_type == 'out' + if out_move == out_shipment: res[line.id] = val return res @@ -808,20 +808,28 @@ class Sale(Workflow, ModelSQL, ModelView): ('warehouse', line.warehouse.id), ) - def create_shipment(self, sale): + _group_return_key = _group_shipment_key + + def create_shipment(self, sale, shipment_type): ''' - Create a shipment for the sale + Create a shipment of type shipment_type for the sale and return the list of created shipment ids ''' pool = Pool() - shipment_obj = pool.get('stock.shipment.out') move_obj = pool.get('stock.move') sale_line_obj = pool.get('sale.line') - moves = self._get_move_sale_line(sale) + moves = self._get_move_sale_line(sale, shipment_type) if not moves: return - keyfunc = partial(self._group_shipment_key, moves.values()) + if shipment_type == 'out': + keyfunc = partial(self._group_shipment_key, moves.values()) + move_shipment_key = 'shipment_out' + shipment_obj = pool.get('stock.shipment.out') + elif shipment_type == 'return': + keyfunc = partial(self._group_return_key, moves.values()) + move_shipment_key = 'shipment_out_return' + shipment_obj = pool.get('stock.shipment.out.return') moves = moves.items() moves = sorted(moves, key=keyfunc) @@ -838,12 +846,13 @@ class Sale(Workflow, ModelSQL, ModelView): shipment_id = shipment_obj.create(values) shipments.append(shipment_id) for line_id, values in grouped_moves: - values['shipment_out'] = shipment_id + values[move_shipment_key] = shipment_id move_id = move_obj.create(values) sale_line_obj.write(line_id, { 'moves': [('add', move_id)], }) - shipment_obj.wait(shipments) + if shipment_type == 'out': + shipment_obj.wait(shipments) return shipments def is_done(self, sale): @@ -878,9 +887,11 @@ class Sale(Workflow, ModelSQL, ModelView): for sale in self.browse(ids): if sale.state in ('done', 'cancel'): continue - self.create_invoice(sale) + self.create_invoice(sale, 'out_invoice') + self.create_invoice(sale, 'out_credit_note') self.set_invoice_state(sale) - self.create_shipment(sale) + self.create_shipment(sale, 'out') + self.create_shipment(sale, 'return') self.set_shipment_state(sale) if self.is_done(sale): done.append(sale.id) @@ -1283,16 +1294,22 @@ class SaleLine(ModelSQL, ModelView): def get_from_location(self, ids, name): result = {} for line in self.browse(ids): - if line.warehouse: + if line.quantity >= 0 and line.warehouse: result[line.id] = line.warehouse.output_location.id else: - result[line.id] = None + result[line.id] = line.sale.party.customer_location.id return result def get_to_location(self, ids, name): result = {} for line in self.browse(ids): - result[line.id] = line.sale.party.customer_location.id + if line.quantity >= 0: + result[line.id] = line.sale.party.customer_location.id + else: + if line.warehouse: + result[line.id] = line.warehouse.input_location.id + else: + result[line.id] = None return result def _compute_delivery_date(self, product, date): @@ -1339,7 +1356,7 @@ class SaleLine(ModelSQL, ModelView): if (line.sale.invoice_method == 'order' or not line.product or line.product.type == 'service'): - quantity = line.quantity + quantity = abs(line.quantity) else: quantity = 0.0 for move in line.moves: @@ -1406,7 +1423,7 @@ class SaleLine(ModelSQL, ModelView): if line.product.type == 'service': return skip_ids = set(x.id for x in line.moves_recreated) - quantity = line.quantity + quantity = abs(line.quantity) for move in line.moves: if move.id not in skip_ids: quantity -= uom_obj.compute_qty(move.uom, move.quantity, @@ -1542,17 +1559,19 @@ class Template(ModelSQL, ModelView): def on_change_with_sale_uom(self, vals): uom_obj = Pool().get('product.uom') + res = None if vals.get('default_uom'): default_uom = uom_obj.browse(vals['default_uom']) if vals.get('sale_uom'): sale_uom = uom_obj.browse(vals['sale_uom']) if default_uom.category.id == sale_uom.category.id: - return sale_uom.id + res = sale_uom.id else: - return default_uom.id + res = default_uom.id else: - return default_uom.id + res = default_uom.id + return res Template() @@ -1653,17 +1672,68 @@ class ShipmentOut(ModelSQL, ModelView): sale_obj.process(sale_ids) return res - def button_draft(self, ids): + @ModelView.button + @Workflow.transition('draft') + def draft(self, ids): for shipment in self.browse(ids): for move in shipment.outgoing_moves: if move.state == 'cancel' and move.sale_line: self.raise_user_error('reset_move') - return super(ShipmentOut, self).button_draft(ids) + return super(ShipmentOut, self).draft(ids) ShipmentOut() +class ShipmentOutReturn(ModelSQL, ModelView): + _name = 'stock.shipment.out.return' + + def __init__(self): + super(ShipmentOutReturn, self).__init__() + self._error_messages.update({ + 'reset_move': 'You cannot reset to draft a move generated ' + 'by a sale.', + }) + + def write(self, ids, vals): + sale_obj = Pool().get('sale.sale') + sale_line_obj = Pool().get('sale.line') + + res = super(ShipmentOutReturn, self).write(ids, vals) + + if 'state' in vals and vals['state'] == 'received': + sale_ids = [] + move_ids = [] + if isinstance(ids, (int, long)): + ids = [ids] + for shipment in self.browse(ids): + move_ids.extend([x.id for x in shipment.incoming_moves]) + + sale_line_ids = sale_line_obj.search([ + ('moves', 'in', move_ids), + ]) + if sale_line_ids: + for sale_line in sale_line_obj.browse(sale_line_ids): + if sale_line.sale.id not in sale_ids: + sale_ids.append(sale_line.sale.id) + + with Transaction().set_user(0, set_context=True): + sale_obj.process(sale_ids) + return res + + @ModelView.button + @Workflow.transition('draft') + def draft(self, ids): + for shipment in self.browse(ids): + for move in shipment.incoming_moves: + if move.state == 'cancel' and move.sale_line: + self.raise_user_error('reset_move') + + return super(ShipmentOutReturn, self).draft(ids) + +ShipmentOutReturn() + + class Move(ModelSQL, ModelView): _name = 'stock.move' @@ -1996,3 +2066,47 @@ class HandleInvoiceException(Wizard): return 'end' HandleInvoiceException() + + +class ReturnSaleAsk(ModelView): + _name = 'sale.return_sale.ask' + sale_lines = fields.Many2Many('sale.line', None, None, 'Return Lines', + domain=[('sale', '=', Eval('sale'))], depends=['sale']) + sale = fields.Many2One('sale.sale', 'Sale') + +ReturnSaleAsk() + + +class ReturnSale(Wizard): + _name = 'sale.return_sale' + start_state = 'ask' + ask = StateView('sale.return_sale.ask', 'sale.return_sale_ask_form', [ + Button('Cancel', 'end', 'tryton-cancel'), + Button('Ok', 'make_return', 'tryton-ok', default=True), + ]) + make_return = StateTransition() + + def default_ask(self, session, fields): + return { + 'sale': Transaction().context['active_id'], + } + + def transition_make_return(self, session): + pool = Pool() + sale_obj = pool.get('sale.sale') + line_obj = pool.get('sale.line') + + sale = sale_obj.browse(session.ask.sale.id) + new_sale_type = 'return' if sale.type == 'sale' else 'sale' + new_sale_id = sale_obj.copy(session.ask.sale.id, { + 'type': new_sale_type, + }) + new_line_ids = line_obj.copy([x.id for x in session.ask.sale_lines]) + for new_line in line_obj.browse(new_line_ids): + line_obj.write(new_line.id, {'quantity': -new_line.quantity}) + sale_obj.write(new_sale_id, { + 'lines': [('set', new_line_ids)], + }) + return 'end' + +ReturnSale() diff --git a/sale.xml b/sale.xml index 31f1167427..56c7f7998a 100644 --- a/sale.xml +++ b/sale.xml @@ -130,6 +130,7 @@ this repository contains the full copyright notices and license terms. --> + @@ -174,7 +175,7 @@ this repository contains the full copyright notices and license terms. --> id="choose" yalign="0.0" xalign="0.0" xexpand="1"/> + view_ids="stock.move_view_tree"/> ]]> @@ -208,6 +209,16 @@ this repository contains the full copyright notices and license terms. --> sale.sale,-1 + + Returns + stock.shipment.out.return + [("id", "in", Eval('shipment_returns'))] + + + form_relate + sale.sale,-1 + + Invoices account.invoice diff --git a/tests/scenario_sale.rst b/tests/scenario_sale.rst new file mode 100644 index 0000000000..4877d9892b --- /dev/null +++ b/tests/scenario_sale.rst @@ -0,0 +1,372 @@ +============= +Sale Scenario +============= + +Imports:: + + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from proteus import config, Model, Wizard + >>> today = datetime.date.today() + +Create database:: + + >>> config = config.set_trytond() + >>> config.pool.test = True + +Install sale:: + + >>> Module = Model.get('ir.module.module') + >>> sale_module, = Module.find([('name', '=', 'sale')]) + >>> Module.install([sale_module.id], config.context) + >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + +Create company:: + + >>> Currency = Model.get('currency.currency') + >>> CurrencyRate = Model.get('currency.currency.rate') + >>> currencies = Currency.find([('code', '=', 'EUR')]) + >>> if not currencies: + ... currency = Currency(name='Euro', symbol=u'€', code='EUR', + ... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]', + ... mon_decimal_point=',') + ... currency.save() + ... CurrencyRate(date=today + relativedelta(month=1, day=1), + ... rate=Decimal('1.0'), currency=currency).save() + ... else: + ... currency, = currencies + >>> Company = Model.get('company.company') + >>> company_config = Wizard('company.company.config') + >>> company_config.execute('company') + >>> company = company_config.form + >>> company.name = 'B2CK' + >>> company.currency = currency + >>> company_config.execute('add') + >>> company, = Company.find([]) + +Reload the context:: + + >>> User = Model.get('res.user') + >>> config._context = User.get_preferences(True, config.context) + +Create fiscal year:: + + >>> FiscalYear = Model.get('account.fiscalyear') + >>> Sequence = Model.get('ir.sequence') + >>> SequenceStrict = Model.get('ir.sequence.strict') + >>> fiscalyear = FiscalYear(name=str(today.year)) + >>> fiscalyear.start_date = today + relativedelta(month=1, day=1) + >>> fiscalyear.end_date = today + relativedelta(month=12, day=31) + >>> fiscalyear.company = company + >>> post_move_seq = Sequence(name=str(today.year), code='account.move', + ... company=company) + >>> post_move_seq.save() + >>> fiscalyear.post_move_sequence = post_move_seq + >>> invoice_seq = SequenceStrict(name=str(today.year), + ... code='account.invoice', company=company) + >>> invoice_seq.save() + >>> fiscalyear.out_invoice_sequence = invoice_seq + >>> fiscalyear.in_invoice_sequence = invoice_seq + >>> fiscalyear.out_credit_note_sequence = invoice_seq + >>> fiscalyear.in_credit_note_sequence = invoice_seq + >>> fiscalyear.save() + >>> FiscalYear.create_period([fiscalyear.id], config.context) + True + +Create chart of accounts:: + + >>> AccountTemplate = Model.get('account.account.template') + >>> Account = Model.get('account.account') + >>> account_template, = AccountTemplate.find([('parent', '=', False)]) + >>> create_chart = Wizard('account.create_chart') + >>> create_chart.execute('account') + >>> create_chart.form.account_template = account_template + >>> create_chart.form.company = company + >>> create_chart.execute('create_account') + >>> receivable, = Account.find([ + ... ('kind', '=', 'receivable'), + ... ('company', '=', company.id), + ... ]) + >>> payable, = Account.find([ + ... ('kind', '=', 'payable'), + ... ('company', '=', company.id), + ... ]) + >>> revenue, = Account.find([ + ... ('kind', '=', 'revenue'), + ... ('company', '=', company.id), + ... ]) + >>> expense, = Account.find([ + ... ('kind', '=', 'expense'), + ... ('company', '=', company.id), + ... ]) + >>> create_chart.form.account_receivable = receivable + >>> create_chart.form.account_payable = payable + >>> create_chart.execute('create_properties') + +Create parties:: + + >>> Party = Model.get('party.party') + >>> supplier = Party(name='Supplier') + >>> supplier.save() + >>> customer = Party(name='Customer') + >>> customer.save() + +Create category:: + + >>> ProductCategory = Model.get('product.category') + >>> category = ProductCategory(name='Category') + >>> category.save() + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> Product = Model.get('product.product') + >>> product = Product() + >>> product.name = 'product' + >>> product.category = category + >>> product.default_uom = unit + >>> product.type = 'goods' + >>> product.purchasable = True + >>> product.salable = True + >>> product.list_price = Decimal('10') + >>> product.cost_price = Decimal('5') + >>> product.cost_price_method = 'fixed' + >>> product.account_expense = expense + >>> product.account_revenue = revenue + >>> product.save() + +Create payment term:: + + >>> PaymentTerm = Model.get('account.invoice.payment_term') + >>> PaymentTermLine = Model.get('account.invoice.payment_term.line') + >>> payment_term = PaymentTerm(name='Direct') + >>> payment_term_line = PaymentTermLine(type='remainder', days=0) + >>> payment_term.lines.append(payment_term_line) + >>> payment_term.save() + +Create an Inventory:: + + >>> Inventory = Model.get('stock.inventory') + >>> InventoryLine = Model.get('stock.inventory.line') + >>> Location = Model.get('stock.location') + >>> storage, = Location.find([ + ... ('code', '=', 'STO'), + ... ]) + >>> inventory = Inventory() + >>> inventory.location = storage + >>> inventory.save() + >>> inventory_line = InventoryLine(product=product, inventory=inventory) + >>> inventory_line.quantity = 100.0 + >>> inventory_line.expected_quantity = 0.0 + >>> inventory.save() + >>> inventory_line.save() + >>> Inventory.confirm([inventory.id], config.context) + >>> inventory.state + u'done' + +Sale 5 products:: + + >>> Sale = Model.get('sale.sale') + >>> SaleLine = Model.get('sale.line') + >>> sale = Sale() + >>> sale.party = customer + >>> sale.payment_term = payment_term + >>> sale.invoice_method = 'shipment' + >>> sale_line = SaleLine() + >>> sale.lines.append(sale_line) + >>> sale_line.product = product + >>> sale_line.quantity = 2.0 + >>> sale_line = SaleLine() + >>> sale.lines.append(sale_line) + >>> sale_line.product = product + >>> sale_line.quantity = 3.0 + >>> sale.save() + >>> Sale.quote([sale.id], config.context) + >>> Sale.confirm([sale.id], config.context) + >>> Sale.process([sale.id], config.context) + >>> sale.state + u'processing' + +Validate Shipments:: + + >>> sale.reload() + >>> ShipmentOut = Model.get('stock.shipment.out') + >>> shipment, = sale.shipments + >>> ShipmentOut.assign_try([shipment.id], config.context) + True + >>> ShipmentOut.pack([shipment.id], config.context) + >>> ShipmentOut.done([shipment.id], config.context) + +Open customer invoice:: + + >>> sale.reload() + >>> Invoice = Model.get('account.invoice') + >>> invoice, = sale.invoices + >>> invoice.type + u'out_invoice' + >>> Invoice.open([invoice.id], config.context) + >>> invoice.reload() + >>> invoice.state + u'open' + >>> receivable.reload() + >>> (receivable.debit, receivable.credit) == \ + ... (Decimal('50.00'), Decimal('0.00')) + True + >>> revenue.reload() + >>> (revenue.debit, revenue.credit) == \ + ... (Decimal('0.00'), Decimal('50.00')) + True + +Create a Return:: + + >>> return_ = Sale() + >>> return_.party = customer + >>> return_.payment_term = payment_term + >>> return_.invoice_method = 'shipment' + >>> return_line = SaleLine() + >>> return_.lines.append(return_line) + >>> return_line.product = product + >>> return_line.quantity = -4. + >>> return_.save() + >>> Sale.quote([return_.id], config.context) + >>> Sale.confirm([return_.id], config.context) + >>> Sale.process([return_.id], config.context) + >>> return_.state + u'processing' + +Check Return Shipments:: + + >>> return_.reload() + >>> ShipmentReturn = Model.get('stock.shipment.out.return') + >>> ship_return, = return_.shipment_returns + >>> len(return_.shipments) + 0 + >>> ShipmentReturn.receive([ship_return.id], config.context) + >>> move_return, = ship_return.incoming_moves + >>> move_return.product.name + u'product' + >>> move_return.quantity + 4.0 + +Open customer credit note:: + + >>> return_.reload() + >>> credit_note, = return_.invoices + >>> credit_note.type + u'out_credit_note' + >>> Invoice.open([credit_note.id], config.context) + >>> credit_note.reload() + >>> credit_note.state + u'open' + >>> receivable.reload() + >>> (receivable.debit, receivable.credit) == (Decimal(50), Decimal(40)) + True + >>> revenue.reload() + >>> (revenue.debit, revenue.credit) == (Decimal(40), Decimal(50)) + True + +Mixing return and sale:: + + >>> mix = Sale() + >>> mix.party = customer + >>> mix.payment_term = payment_term + >>> mix.invoice_method = 'order' + >>> mixline = SaleLine() + >>> mix.lines.append(mixline) + >>> mixline.product = product + >>> mixline.quantity = 7. + >>> mixline2 = SaleLine() + >>> mix.lines.append(mixline2) + >>> mixline2.product = product + >>> mixline2.quantity = -2. + >>> mix.save() + >>> Sale.quote([mix.id], config.context) + >>> Sale.confirm([mix.id], config.context) + >>> Sale.process([mix.id], config.context) + >>> mix.state + u'processing' + +Checking Shipments:: + + >>> mix.reload() + >>> mix_returns, = mix.shipment_returns + >>> mix_shipments, = mix.shipments + >>> ShipmentReturn.receive([mix_returns.id], config.context) + >>> move_return, = mix_returns.incoming_moves + >>> move_return.product.name + u'product' + >>> move_return.quantity + 2.0 + >>> ShipmentOut.assign_try([mix_shipments.id], config.context) + True + >>> ShipmentOut.pack([mix_shipments.id], config.context) + >>> ShipmentOut.done([mix_shipments.id], config.context) + >>> move_shipment, = mix_shipments.outgoing_moves + >>> move_shipment.product.name + u'product' + >>> move_shipment.quantity + 7.0 + +Checking the invoice:: + + >>> mix.reload() + >>> mix_invoice, mix_credit_note = mix.invoices + >>> mix_invoice.type, mix_credit_note.type + (u'out_invoice', u'out_credit_note') + >>> Invoice.open([mix_invoice.id], config.context) + >>> mix_invoice.reload() + >>> mix_invoice.state + u'open' + >>> Invoice.open([mix_credit_note.id], config.context) + >>> mix_credit_note.reload() + >>> mix_credit_note.state + u'open' + >>> receivable.reload() + >>> (receivable.debit, receivable.credit) == (Decimal(120), Decimal(60)) + True + >>> revenue.reload() + >>> (revenue.debit, revenue.credit) == (Decimal(60), Decimal(120)) + True + +Mixing stuff with an invoice method 'on shipment':: + + >>> mix = Sale() + >>> mix.party = customer + >>> mix.payment_term = payment_term + >>> mix.invoice_method = 'shipment' + >>> mixline = SaleLine() + >>> mix.lines.append(mixline) + >>> mixline.product = product + >>> mixline.quantity = 6. + >>> mixline2 = SaleLine() + >>> mix.lines.append(mixline2) + >>> mixline2.product = product + >>> mixline2.quantity = -3. + >>> mix.save() + >>> Sale.quote([mix.id], config.context) + >>> Sale.confirm([mix.id], config.context) + >>> Sale.process([mix.id], config.context) + >>> mix.state + u'processing' + +Checking Shipments:: + + >>> mix.reload() + >>> mix_returns, = mix.shipment_returns + >>> mix_shipments, = mix.shipments + >>> ShipmentReturn.receive([mix_returns.id], config.context) + >>> move_return, = mix_returns.incoming_moves + >>> move_return.product.name + u'product' + >>> move_return.quantity + 3.0 + >>> ShipmentOut.assign_try([mix_shipments.id], config.context) + True + >>> ShipmentOut.pack([mix_shipments.id], config.context) + >>> move_shipment, = mix_shipments.outgoing_moves + >>> move_shipment.product.name + u'product' + >>> move_shipment.quantity + 6.0 diff --git a/tests/test_sale.py b/tests/test_sale.py index d68d1c6fbc..4e157516af 100644 --- a/tests/test_sale.py +++ b/tests/test_sale.py @@ -9,8 +9,10 @@ if os.path.isdir(DIR): sys.path.insert(0, os.path.dirname(DIR)) import unittest +import doctest import trytond.tests.test_tryton from trytond.tests.test_tryton import test_view, test_depends +from trytond.backend.sqlite.database import Database as SQLiteDatabase class SaleTestCase(unittest.TestCase): @@ -33,9 +35,23 @@ class SaleTestCase(unittest.TestCase): ''' test_depends() + +def doctest_dropdb(test): + database = SQLiteDatabase().connect() + cursor = database.cursor(autocommit=True) + try: + database.drop(cursor, ':memory:') + cursor.commit() + finally: + cursor.close() + + def suite(): suite = trytond.tests.test_tryton.suite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(SaleTestCase)) + suite.addTests(doctest.DocFileSuite('scenario_sale.rst', + setUp=doctest_dropdb, tearDown=doctest_dropdb, encoding='utf-8', + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite if __name__ == '__main__':