@@ -451,8 +451,8 @@ class SaleLine(metaclass=PoolMeta):
|
||||
return None
|
||||
return move
|
||||
|
||||
def get_purchase_request(self):
|
||||
request = super(SaleLine, self).get_purchase_request()
|
||||
def get_purchase_request(self, product_quantities):
|
||||
request = super().get_purchase_request(product_quantities)
|
||||
if (self.sale.advance_payment_eligible()
|
||||
and self.sale.supply_blocked):
|
||||
return None
|
||||
|
||||
@@ -285,7 +285,7 @@ Create the product::
|
||||
>>> sos_template.salable = True
|
||||
>>> sos_template.list_price = Decimal('10')
|
||||
>>> sos_template.account_category = account_category
|
||||
>>> sos_template.supply_on_sale = True
|
||||
>>> sos_template.supply_on_sale = 'always'
|
||||
>>> sos_template.save()
|
||||
>>> sos_product, = sos_template.products
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* Add stock_first option to supply_on_sale
|
||||
* Change supply_on_sale into selection
|
||||
* Remove support for Python 3.7
|
||||
* Add support for Python 3.11
|
||||
|
||||
@@ -2,9 +2,10 @@ Sale Supply Module
|
||||
##################
|
||||
|
||||
The Sale Supply module adds a "supply on sale option" to purchasable products.
|
||||
If checked, it will generate a purchase request for each sale line of this
|
||||
product regardless of the stock levels. Once the purchased products are
|
||||
received they are assigned on the customer shipments.
|
||||
If selected, it will generate a purchase request for each sale line of this
|
||||
product depending of the option and the stock levels.
|
||||
Once the purchased products are received they are assigned on the customer
|
||||
shipments.
|
||||
If the purchase is cancelled the sale goes back to the default supply method.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -15,6 +15,7 @@ class Template(metaclass=PoolMeta):
|
||||
|
||||
supply_on_sale = fields.Selection([
|
||||
(None, "Never"),
|
||||
('stock_first', "Stock First"),
|
||||
('always', "Always"),
|
||||
], "Supply On Sale",
|
||||
states={
|
||||
|
||||
+71
-21
@@ -1,5 +1,10 @@
|
||||
# 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 collections import defaultdict
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
|
||||
from trytond.model import Model, fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
@@ -19,6 +24,31 @@ class Sale(metaclass=PoolMeta):
|
||||
|
||||
@classmethod
|
||||
def _process_shipment(cls, sales):
|
||||
pool = Pool()
|
||||
Product = pool.get('product.product')
|
||||
|
||||
product_quantities = defaultdict(float)
|
||||
get_warehouse = attrgetter('warehouse')
|
||||
for warehouse, sub_sales in groupby(
|
||||
filter(get_warehouse, sales), key=get_warehouse):
|
||||
products = {
|
||||
l.product for s in sub_sales for l in s.lines
|
||||
if l.product and l.product.supply_on_sale == 'stock_first'}
|
||||
locations = [warehouse.id]
|
||||
with Transaction().set_context(
|
||||
locations=locations, stock_date_end=None):
|
||||
products = Product.browse(products)
|
||||
product_quantities.update(
|
||||
(p, min(p.quantity, p.forecast_quantity))
|
||||
for p in Product.browse(products))
|
||||
|
||||
# purchase requests must be created before shipments to get information
|
||||
# about requests during the shipments creation like the supplier
|
||||
cls._process_supply(sales, product_quantities)
|
||||
super()._process_shipment(sales)
|
||||
|
||||
@classmethod
|
||||
def _process_supply(cls, sales, product_quantities):
|
||||
pool = Pool()
|
||||
Line = pool.get('sale.line')
|
||||
Move = pool.get('stock.move')
|
||||
@@ -28,10 +58,7 @@ class Sale(metaclass=PoolMeta):
|
||||
requests, lines = [], []
|
||||
moves_to_draft, shipments_to_wait = [], []
|
||||
for sale in sales:
|
||||
# purchase requests must be created before shipments to get
|
||||
# information about requests during the shipments creation
|
||||
# like the supplier
|
||||
reqs, lns = sale.create_purchase_requests()
|
||||
reqs, lns = sale.create_purchase_requests(product_quantities)
|
||||
requests.extend(reqs)
|
||||
lines.extend(lns)
|
||||
|
||||
@@ -42,12 +69,11 @@ class Sale(metaclass=PoolMeta):
|
||||
Line.save(lines)
|
||||
Move.draft(moves_to_draft)
|
||||
ShipmentOut.wait(shipments_to_wait)
|
||||
super()._process_shipment(sales)
|
||||
|
||||
def create_purchase_requests(self):
|
||||
def create_purchase_requests(self, product_quantities):
|
||||
requests, lines = [], []
|
||||
for line in self.lines:
|
||||
request = line.get_purchase_request()
|
||||
request = line.get_purchase_request(product_quantities)
|
||||
if not request:
|
||||
continue
|
||||
requests.append(request)
|
||||
@@ -123,48 +149,72 @@ class Line(metaclass=PoolMeta):
|
||||
return False
|
||||
return bool(self.product.supply_on_sale)
|
||||
|
||||
@property
|
||||
def ready_for_supply(self):
|
||||
if self.sale.shipment_method == 'invoice':
|
||||
# Ensure to create the request for the maximum paid
|
||||
invoice_skips = (
|
||||
set(self.sale.invoices_ignored)
|
||||
| set(self.sale.invoices_recreated))
|
||||
invoice_lines = [
|
||||
l for l in self.invoice_lines
|
||||
if l.invoice not in invoice_skips]
|
||||
if (not invoice_lines
|
||||
or any(
|
||||
(not l.invoice) or l.invoice.state != 'paid'
|
||||
for l in invoice_lines)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_move(self, shipment_type):
|
||||
move = super().get_move(shipment_type)
|
||||
if (move
|
||||
and shipment_type == 'out'
|
||||
and (self.supply_on_sale or self.has_supply)):
|
||||
and self.has_supply):
|
||||
if self.supply_state in {'', 'requested'}:
|
||||
move.state = 'staging'
|
||||
return move
|
||||
|
||||
def _get_move_quantity(self, shipment_type):
|
||||
quantity = super()._get_move_quantity(shipment_type)
|
||||
if self.supply_on_sale and not self.ready_for_supply:
|
||||
quantity = 0
|
||||
return quantity
|
||||
|
||||
def _get_purchase_request_product_supplier_pattern(self):
|
||||
return {
|
||||
'company': self.sale.company.id,
|
||||
}
|
||||
|
||||
def get_purchase_request(self):
|
||||
'Return purchase request for the sale line'
|
||||
def get_purchase_request(self, product_quantities):
|
||||
"""Return purchase request for the sale line
|
||||
depending on the product quantities"""
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
Request = pool.get('purchase.request')
|
||||
|
||||
if (not self.supply_on_sale
|
||||
or self.purchase_request
|
||||
or not self.ready_for_supply
|
||||
or not self.product.purchasable):
|
||||
return
|
||||
|
||||
# Ensure to create the request for the maximum paid
|
||||
if self.sale.shipment_method == 'invoice':
|
||||
invoice_skips = (set(self.sale.invoices_ignored)
|
||||
| set(self.sale.invoices_recreated))
|
||||
invoice_lines = [l for l in self.invoice_lines
|
||||
if l.invoice not in invoice_skips]
|
||||
if (not invoice_lines
|
||||
or any((not l.invoice) or l.invoice.state != 'paid'
|
||||
for l in invoice_lines)):
|
||||
product = self.product
|
||||
quantity = self._get_move_quantity('out')
|
||||
if product.supply_on_sale == 'stock_first':
|
||||
available_qty = product_quantities[product]
|
||||
available_qty = Uom.compute_qty(
|
||||
product.default_uom, available_qty, self.unit,
|
||||
round=False)
|
||||
if available_qty > 0:
|
||||
product_quantities[product] -= Uom.compute_qty(
|
||||
self.unit, quantity, product.default_uom, round=False)
|
||||
return
|
||||
|
||||
product = self.product
|
||||
supplier, purchase_date = Request.find_best_supplier(product,
|
||||
self.shipping_date,
|
||||
**self._get_purchase_request_product_supplier_pattern())
|
||||
uom = product.purchase_uom or product.default_uom
|
||||
quantity = self._get_move_quantity('out')
|
||||
quantity = Uom.compute_qty(self.unit, quantity, uom)
|
||||
return Request(
|
||||
product=product,
|
||||
|
||||
@@ -36,7 +36,7 @@ Create product::
|
||||
>>> product_template.default_uom = unit
|
||||
>>> product_template.purchasable = True
|
||||
>>> product_template.salable = True
|
||||
>>> product_template.supply_on_sale = True
|
||||
>>> product_template.supply_on_sale = 'always'
|
||||
>>> product_template.save()
|
||||
>>> product, = product_template.products
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
================================
|
||||
Sale Supply Stock First Scenario
|
||||
================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from decimal import Decimal
|
||||
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
>>> from trytond.modules.company.tests.tools import create_company
|
||||
>>> from trytond.modules.account.tests.tools import (
|
||||
... create_chart, get_accounts)
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('sale_supply')
|
||||
|
||||
>>> Inventory = Model.get('stock.inventory')
|
||||
>>> Location = Model.get('stock.location')
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> ProductCategory = Model.get('product.category')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> Sale = Model.get('sale.sale')
|
||||
>>> UoM = Model.get('product.uom')
|
||||
|
||||
Create company::
|
||||
|
||||
>>> _ = create_company()
|
||||
|
||||
Create chart of accounts::
|
||||
|
||||
>>> _ = create_chart()
|
||||
>>> accounts = get_accounts()
|
||||
|
||||
Create parties::
|
||||
|
||||
>>> supplier = Party(name="Supplier")
|
||||
>>> supplier.save()
|
||||
>>> customer = Party(name="Customer")
|
||||
>>> customer.save()
|
||||
|
||||
Create account category::
|
||||
|
||||
>>> account_category = ProductCategory(name="Account Category")
|
||||
>>> account_category.accounting = True
|
||||
>>> account_category.account_expense = accounts['expense']
|
||||
>>> account_category.account_revenue = accounts['revenue']
|
||||
>>> account_category.save()
|
||||
|
||||
Create product::
|
||||
|
||||
>>> unit, = UoM.find([('name', '=', "Unit")])
|
||||
|
||||
>>> template = ProductTemplate(name="Product")
|
||||
>>> template.default_uom = unit
|
||||
>>> template.type = 'goods'
|
||||
>>> template.purchasable = True
|
||||
>>> template.salable = True
|
||||
>>> template.list_price = Decimal('100.0000')
|
||||
>>> template.supply_on_sale = 'stock_first'
|
||||
>>> template.account_category = account_category
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
|
||||
Fill warehouse::
|
||||
|
||||
>>> inventory = Inventory()
|
||||
>>> inventory.location, = Location.find([('code', '=', 'STO')])
|
||||
>>> line = inventory.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 5
|
||||
>>> inventory.click('confirm')
|
||||
>>> inventory.state
|
||||
'done'
|
||||
|
||||
Sale 3 products with 5 in stock::
|
||||
|
||||
>>> sale = Sale()
|
||||
>>> sale.party = customer
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 3
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
>>> line, = sale.lines
|
||||
>>> bool(line.purchase_request)
|
||||
False
|
||||
>>> move, = line.moves
|
||||
>>> move.state
|
||||
'draft'
|
||||
|
||||
Sale 3 products with 2 in stock::
|
||||
|
||||
>>> sale = Sale()
|
||||
>>> sale.party = customer
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 3
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
>>> line, = sale.lines
|
||||
>>> bool(line.purchase_request)
|
||||
False
|
||||
>>> move, = line.moves
|
||||
>>> move.state
|
||||
'draft'
|
||||
|
||||
Sale 4 products with no stock::
|
||||
|
||||
>>> sale = Sale()
|
||||
>>> sale.party = customer
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 4
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
>>> line, = sale.lines
|
||||
>>> bool(line.purchase_request)
|
||||
True
|
||||
>>> move, = line.moves
|
||||
>>> move.state
|
||||
'staging'
|
||||
@@ -2,10 +2,10 @@ Sale Supply Drop Shipment Model
|
||||
###############################
|
||||
|
||||
The Sale Supply Drop Shipment module adds a drop shipment option on product
|
||||
supplier if "supply on request" is checked. When checked, the purchase request
|
||||
and the linked purchase have the address of customer as Delivery Address;
|
||||
at the confirmation of the purchase a drop shipment is created and linked to
|
||||
both the purchase and the sale.
|
||||
supplier if "supply on request" is selected.
|
||||
When selected, the purchase request and the linked purchase have the address of
|
||||
customer as Delivery Address; at the confirmation of the purchase a drop
|
||||
shipment is created and linked to both the purchase and the sale.
|
||||
|
||||
Drop Shipment
|
||||
*************
|
||||
|
||||
@@ -7,7 +7,6 @@ from trytond.modules.sale.sale import (
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools.multivalue import migrate_property
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
sale_drop_location = fields.Many2One(
|
||||
'stock.location', "Sale Drop Location", domain=[('type', '=', 'drop')])
|
||||
@@ -82,14 +81,13 @@ class Sale(metaclass=PoolMeta):
|
||||
search_drop_shipments = search_shipments_returns('stock.shipment.drop')
|
||||
|
||||
@classmethod
|
||||
def _process_shipment(cls, sales):
|
||||
def _process_supply(cls, sales, product_quantities):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
super()._process_shipment(sales)
|
||||
super()._process_supply(sales, product_quantities)
|
||||
moves = []
|
||||
with Transaction().set_context(_drop_shipment=True):
|
||||
for sale in sales:
|
||||
moves.extend(sale.create_drop_shipment_moves())
|
||||
for sale in sales:
|
||||
moves.extend(sale.create_drop_shipment_moves())
|
||||
Move.save(moves)
|
||||
|
||||
def create_drop_shipment_moves(self):
|
||||
@@ -125,15 +123,13 @@ class Line(metaclass=PoolMeta):
|
||||
return True
|
||||
|
||||
def get_move(self, shipment_type):
|
||||
result = super().get_move(shipment_type)
|
||||
if (shipment_type == 'out'
|
||||
and not Transaction().context.get('_drop_shipment')):
|
||||
if self.supply_on_sale_drop_move:
|
||||
return
|
||||
return result
|
||||
move = super().get_move(shipment_type)
|
||||
if self.supply_on_sale_drop_move and not self.purchase_request:
|
||||
move = None
|
||||
return move
|
||||
|
||||
def get_purchase_request(self):
|
||||
request = super().get_purchase_request()
|
||||
def get_purchase_request(self, product_quantities):
|
||||
request = super().get_purchase_request(product_quantities)
|
||||
if request and request.party:
|
||||
if self.product and self.product.type in ('goods', 'assets'):
|
||||
product_supplier = request.find_best_product_supplier(
|
||||
|
||||
@@ -3,9 +3,9 @@ Sale Supply Production Module
|
||||
|
||||
The Sale Supply Production module adds a "supply on sale" option to producible
|
||||
products.
|
||||
If checked, it will generate a production request for each sale line of this
|
||||
product regardless of the stock levels. Once the products are produced they are
|
||||
assigned to the customer shipments.
|
||||
If selected, it will generate a production request for each sale line of this
|
||||
product depending of the option and the stock levels.
|
||||
Once the products are produced they are assigned to the customer shipments.
|
||||
If the production request is cancelled, the sale goes back to the default
|
||||
supply method.
|
||||
|
||||
|
||||
@@ -11,21 +11,21 @@ class Sale(metaclass=PoolMeta):
|
||||
__name__ = 'sale.sale'
|
||||
|
||||
@classmethod
|
||||
def _process_shipment(cls, sales):
|
||||
def _process_supply(cls, sales, product_quantities):
|
||||
pool = Pool()
|
||||
Production = pool.get('production')
|
||||
|
||||
productions = []
|
||||
for sale in sales:
|
||||
productions.extend(sale.create_productions())
|
||||
productions.extend(sale.create_productions(product_quantities))
|
||||
Production.save(productions)
|
||||
Production.set_moves(productions)
|
||||
super()._process_shipment(sales)
|
||||
super()._process_supply(sales, product_quantities)
|
||||
|
||||
def create_productions(self):
|
||||
def create_productions(self, product_quantities):
|
||||
productions = []
|
||||
for line in self.lines:
|
||||
production = line.get_production()
|
||||
production = line.get_production(product_quantities)
|
||||
if not production:
|
||||
continue
|
||||
production.planned_start_date = (
|
||||
@@ -67,28 +67,32 @@ class Line(metaclass=PoolMeta):
|
||||
default.setdefault('productions', None)
|
||||
return super().copy(lines, default=default)
|
||||
|
||||
def get_production(self):
|
||||
def get_production(self, product_quantities):
|
||||
"Return production for the sale line"
|
||||
pool = Pool()
|
||||
Production = pool.get('production')
|
||||
Date = pool.get('ir.date')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
with Transaction().set_context(company=self.sale.company.id):
|
||||
today = Date.today()
|
||||
|
||||
if (not self.supply_on_sale
|
||||
or self.productions
|
||||
or not self.ready_for_supply
|
||||
or not self.product.producible):
|
||||
return
|
||||
|
||||
# Ensure to create the request for the maximum paid
|
||||
if self.sale.shipment_method == 'invoice':
|
||||
invoice_skips = (set(self.sale.invoices_ignored)
|
||||
| set(self.sale.invoices_recreated))
|
||||
invoice_lines = [l for l in self.invoice_lines
|
||||
if l.invoice not in invoice_skips]
|
||||
if (not invoice_lines
|
||||
or any((not l.invoice) or l.invoice.state != 'paid'
|
||||
for l in invoice_lines)):
|
||||
product = self.product
|
||||
quantity = self._get_move_quantity('out')
|
||||
if product.supply_on_sale == 'stock_first':
|
||||
available_qty = product_quantities[product]
|
||||
available_qty = Uom.compute_qty(
|
||||
product.default_uom, available_qty, self.unit,
|
||||
round=False)
|
||||
if quantity < available_qty:
|
||||
product_quantities[product] -= Uom.compute_qty(
|
||||
self.unit, quantity, product.default_uom, round=False)
|
||||
return
|
||||
|
||||
date = self.shipping_date or today
|
||||
@@ -96,8 +100,6 @@ class Line(metaclass=PoolMeta):
|
||||
date = today
|
||||
else:
|
||||
date -= dt.timedelta(1)
|
||||
product = self.product
|
||||
quantity = self._get_move_quantity('out')
|
||||
return Production(
|
||||
planned_date=date,
|
||||
company=self.sale.company,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
===========================================
|
||||
Sale Supply Production Stock First Scenario
|
||||
===========================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from decimal import Decimal
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
>>> from trytond.modules.company.tests.tools import (
|
||||
... create_company, get_company)
|
||||
>>> from trytond.modules.account.tests.tools import (
|
||||
... create_chart, get_accounts)
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('sale_supply_production')
|
||||
|
||||
>>> Inventory = Model.get('stock.inventory')
|
||||
>>> Location = Model.get('stock.location')
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> ProductCategory = Model.get('product.category')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> Sale = Model.get('sale.sale')
|
||||
>>> UoM = Model.get('product.uom')
|
||||
|
||||
Create company::
|
||||
|
||||
>>> _ = create_company()
|
||||
|
||||
Create chart of accounts::
|
||||
|
||||
>>> _ = create_chart()
|
||||
>>> accounts = get_accounts()
|
||||
|
||||
Create parties::
|
||||
|
||||
>>> customer = Party(name='Customer')
|
||||
>>> customer.save()
|
||||
|
||||
Create account category::
|
||||
|
||||
>>> account_category = ProductCategory(name="Account Category")
|
||||
>>> account_category.accounting = True
|
||||
>>> account_category.account_revenue = accounts['revenue']
|
||||
>>> account_category.save()
|
||||
|
||||
Create product::
|
||||
|
||||
>>> unit, = UoM.find([('name', '=', 'Unit')])
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.default_uom = unit
|
||||
>>> template.type = 'goods'
|
||||
>>> template.producible = True
|
||||
>>> template.salable = True
|
||||
>>> template.supply_on_sale = 'stock_first'
|
||||
>>> template.list_price = Decimal(30)
|
||||
>>> template.account_category = account_category
|
||||
>>> product, = template.products
|
||||
>>> product.cost_price = Decimal(20)
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
|
||||
Fill warehouse::
|
||||
|
||||
>>> inventory = Inventory()
|
||||
>>> inventory.location, = Location.find([('code', '=', 'STO')])
|
||||
>>> line = inventory.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 5
|
||||
>>> inventory.click('confirm')
|
||||
>>> inventory.state
|
||||
'done'
|
||||
|
||||
Sale 3 products without production request::
|
||||
|
||||
>>> sale = Sale()
|
||||
>>> sale.party = customer
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 3
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
>>> line, = sale.lines
|
||||
>>> len(line.productions)
|
||||
0
|
||||
>>> move, = line.moves
|
||||
>>> move.state
|
||||
'draft'
|
||||
|
||||
Sale 4 products with production request::
|
||||
|
||||
>>> sale = Sale()
|
||||
>>> sale.party = customer
|
||||
>>> line = sale.lines.new()
|
||||
>>> line.product = product
|
||||
>>> line.quantity = 4
|
||||
>>> sale.click('quote')
|
||||
>>> sale.click('confirm')
|
||||
>>> sale.state
|
||||
'processing'
|
||||
>>> line, = sale.lines
|
||||
>>> len(line.productions)
|
||||
1
|
||||
>>> move, = line.moves
|
||||
>>> move.state
|
||||
'staging'
|
||||
Reference in New Issue
Block a user