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__':