Lots Management

This commit is contained in:
2026-06-13 11:25:57 +02:00
parent ec90d19cc7
commit 5c9f437945
12 changed files with 307 additions and 37 deletions

View File

@@ -1,5 +1,6 @@
from trytond.pool import Pool
from trytond.transaction import Transaction
import datetime
ITSA_COMPANY_NAME = 'ITSA'
@@ -48,3 +49,79 @@ def default_itsa_unit():
[('name', '=', 'MT')],
])
return record_id(unit) if unit else None
def _company_from_values(values):
company = values.get('company')
if company and hasattr(company, 'party'):
return company
company_id = record_id(company) or Transaction().context.get('company')
if not company_id:
return None
return Pool().get('company.company')(company_id)
def _dimension(name):
return _search_first('analytic.dimension', [
[('code', '=', name)],
[('name', '=', name)],
[('code', 'ilike', '%%%s%%' % name)],
[('name', 'ilike', '%%%s%%' % name)],
])
def _book_year_suffix(today=None):
if today is None:
today = Pool().get('ir.date').today()
year = today.year
if today < datetime.date(year, 4, 1):
year -= 1
return '%02d' % (year % 100)
def _book_value(dimension, today=None):
if not dimension:
return None
suffix = _book_year_suffix(today)
return _search_first('analytic.dimension.value', [
[
('dimension', '=', record_id(dimension)),
('code', 'ilike', '%%%s%%' % suffix),
],
[
('dimension', '=', record_id(dimension)),
('name', 'ilike', '%%%s%%' % suffix),
],
])
def default_itsa_analytic_dimension_assignments(company=None, today=None):
if not is_itsa_company(company):
return []
assignments = []
book = _dimension('Book')
if book:
value = _book_value(book, today=today)
assignments.append({
'dimension': record_id(book),
'value': record_id(value) if value else None,
})
strategy = _dimension('Strategy')
if strategy:
assignments.append({
'dimension': record_id(strategy),
'value': None,
})
return assignments
def add_itsa_analytic_dimension_defaults(values):
if values.get('analytic_dimensions'):
return
company = _company_from_values(values)
assignments = default_itsa_analytic_dimension_assignments(company=company)
if assignments:
values['analytic_dimensions'] = [('create', assignments)]

View File

@@ -65,7 +65,6 @@ class AnalyticDimensionAssignment(ModelSQL, ModelView):
value = fields.Many2One(
'analytic.dimension.value',
'Value',
required=True,
domain=[
('dimension', '=', Eval('dimension')),
],
@@ -76,4 +75,4 @@ class AnalyticDimensionAssignment(ModelSQL, ModelView):
'purchase.purchase',
'Purchase',
ondelete='CASCADE'
)
)

View File

@@ -1253,21 +1253,75 @@ class LotQt(
('delivered', 'Delivered')
], 'Where')
lot_visible = fields.Boolean("Visible")
lot_shipment_origin = fields.Function(
fields.Reference(
selection=[
("stock.shipment.in", "In"),
("stock.shipment.out", "Out"),
lot_shipment_origin = fields.Function(
fields.Reference(
selection=[
("stock.shipment.in", "In"),
("stock.shipment.out", "Out"),
("stock.shipment.internal", "Internal"),
],
string="Shipment",
),
"get_shipment_origin",
)
def get_shipment_origin(self, name):
if self.lot_shipment_in:
return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
),
"get_shipment_origin",
)
purchase = fields.Function(
fields.Many2One('purchase.purchase', "Purchase"), 'get_purchase')
sale = fields.Function(
fields.Many2One('sale.sale', "Sale"), 'get_sale')
purchase_reference = fields.Function(
fields.Char("Purchase Ref."), 'get_purchase_reference')
sale_reference = fields.Function(
fields.Char("Sale Ref."), 'get_sale_reference')
purchase_line_price = fields.Function(
fields.Numeric("Purchase Price", digits=(16, 4)),
'get_purchase_line_price')
sale_line_price = fields.Function(
fields.Numeric("Sale Price", digits=(16, 4)),
'get_sale_line_price')
def _purchase_line(self):
return getattr(getattr(self, 'lot_p', None), 'line', None)
def _sale_line(self):
return getattr(getattr(self, 'lot_s', None), 'sale_line', None)
@staticmethod
def _contract_reference(contract):
if not contract:
return None
return (
getattr(contract, 'reference', None)
or getattr(contract, 'our_reference', None))
def get_purchase(self, name):
line = self._purchase_line()
purchase = getattr(line, 'purchase', None)
return purchase.id if purchase else None
def get_sale(self, name):
line = self._sale_line()
sale = getattr(line, 'sale', None)
return sale.id if sale else None
def get_purchase_reference(self, name):
line = self._purchase_line()
return self._contract_reference(getattr(line, 'purchase', None))
def get_sale_reference(self, name):
line = self._sale_line()
return self._contract_reference(getattr(line, 'sale', None))
def get_purchase_line_price(self, name):
line = self._purchase_line()
return getattr(line, 'unit_price', None)
def get_sale_line_price(self, name):
line = self._sale_line()
return getattr(line, 'unit_price', None)
def get_shipment_origin(self, name):
if self.lot_shipment_in:
return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
elif self.lot_shipment_out:
return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
elif self.lot_shipment_internal:
@@ -2725,6 +2779,16 @@ class LotReport(
r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
r_purchase_reference = fields.Function(
fields.Char("Purchase Ref."), 'get_purchase_reference')
r_sale_reference = fields.Function(
fields.Char("Sale Ref."), 'get_sale_reference')
r_purchase_line_price = fields.Function(
fields.Numeric("Purchase Price", digits=(16, 4)),
'get_purchase_line_price')
r_sale_line_price = fields.Function(
fields.Numeric("Sale Price", digits=(16, 4)),
'get_sale_line_price')
r_del_period = fields.Many2One(
'product.month', "Purchase Shipment Period")
r_sale_del_period = fields.Many2One(
@@ -2941,17 +3005,37 @@ class LotReport(
price = premium + l.unit_price
return round(price,2)
def get_lot_sale_price(self,name=None):
price = Decimal(0)
# if self.sale_line:
# l = Pool().get('sale.line')(self.sale_line)
def get_lot_sale_price(self,name=None):
price = Decimal(0)
# if self.sale_line:
# l = Pool().get('sale.line')(self.sale_line)
# premium = Decimal(0)
# if self.lot_premium_sale:
# premium = self.lot_premium_sale
# price = premium + l.unit_price
return round(price,2)
#@classmethod
# price = premium + l.unit_price
return round(price,2)
@staticmethod
def _contract_reference(contract):
if not contract:
return None
return (
getattr(contract, 'reference', None)
or getattr(contract, 'our_reference', None))
def get_purchase_reference(self, name=None):
return self._contract_reference(self.r_purchase)
def get_sale_reference(self, name=None):
return self._contract_reference(self.r_sale)
def get_purchase_line_price(self, name=None):
return getattr(self.r_line, 'unit_price', None)
def get_sale_line_price(self, name=None):
return getattr(self.r_sale_line, 'unit_price', None)
#@classmethod
#def delete(cls, records):
# Unmatch = Pool().get('lot.unmatch', type='wizard')
# session_id, _, _ = Unmatch.create()
@@ -3853,8 +3937,8 @@ class LotMatchingStart(ModelView):
val = {}
val['lot_id'] = r['r_lot_s']
val['lot_r_id'] = r['id'] - 10000000
val['lot_sale'] = r['r_sale']
val['lot_purchase'] = r['r_purchase']
val['lot_sale'] = r['r_sale']
val['lot_purchase'] = r['r_purchase']
val['lot_shipment_in'] = r['r_lot_shipment_in']
val['lot_shipment_internal'] = r['r_lot_shipment_internal']
val['lot_shipment_out'] = r['r_lot_shipment_out']
@@ -3866,10 +3950,15 @@ class LotMatchingStart(ModelView):
val['lot_matched_qt'] = 0
val['lot_cp'] = r['r_client']
lqt = LotQt(r['id'] - 10000000)
line = LotQt._matching_line(lqt, 'sale')
sale = getattr(line, 'sale', None)
val['lot_sale_line_price'] = getattr(line, 'unit_price', None)
val['lot_sale_reference'] = (
getattr(sale, 'reference', None)
or getattr(sale, 'our_reference', None))
val['lot_already_matched_qt'] = (
LotQt._matching_already_matched(lqt, 'sale'))
val.update(LotQt._matching_line_tolerance_info(
LotQt._matching_line(lqt, 'sale')))
val.update(LotQt._matching_line_tolerance_info(line))
lp.append(val)
return lp
@@ -3896,10 +3985,15 @@ class LotMatchingStart(ModelView):
val['lot_matched_qt'] = 0
val['lot_cp'] = r['r_supplier']
lqt = LotQt(r['id'] - 10000000)
line = LotQt._matching_line(lqt, 'purchase')
purchase = getattr(line, 'purchase', None)
val['lot_purchase_line_price'] = getattr(line, 'unit_price', None)
val['lot_purchase_reference'] = (
getattr(purchase, 'reference', None)
or getattr(purchase, 'our_reference', None))
val['lot_already_matched_qt'] = (
LotQt._matching_already_matched(lqt, 'purchase'))
val.update(LotQt._matching_line_tolerance_info(
LotQt._matching_line(lqt, 'purchase')))
val.update(LotQt._matching_line_tolerance_info(line))
lp.append(val)
return lp
@@ -3910,9 +4004,15 @@ class LotMatchingLot(ModelView):
lms = fields.Many2One('lot.matching.start',"Matching")
lot_id = fields.Many2One('lot.lot',"Lot")
lot_r_id = fields.Many2One('lot.qt',"Lot Qt")
lot_purchase = fields.Many2One('purchase.purchase', "Purchase")
lot_sale = fields.Many2One('sale.sale', "Sale")
lot_name = fields.Char("Name",readonly=True)
lot_purchase = fields.Many2One('purchase.purchase', "Purchase")
lot_sale = fields.Many2One('sale.sale', "Sale")
lot_purchase_reference = fields.Char("Purchase Ref.", readonly=True)
lot_sale_reference = fields.Char("Sale Ref.", readonly=True)
lot_purchase_line_price = fields.Numeric(
"Purchase Price", digits=(16, 4), readonly=True)
lot_sale_line_price = fields.Numeric(
"Sale Price", digits=(16, 4), readonly=True)
lot_name = fields.Char("Name",readonly=True)
lot_type = fields.Selection([
('virtual', 'Open'),
('physic', 'Physic'),

View File

@@ -23,10 +23,12 @@ import json
import re
import jwt
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
from trytond.modules.purchase_trade.company_defaults import (
default_itsa_currency, default_itsa_unit, is_itsa_company, record_id)
add_itsa_analytic_dimension_defaults,
default_itsa_analytic_dimension_assignments, default_itsa_currency,
default_itsa_unit, is_itsa_company, record_id)
import requests
import io
@@ -293,6 +295,18 @@ class Purchase(metaclass=PoolMeta):
if purchases:
super().process(purchases)
@classmethod
def create(cls, vlist):
for values in vlist:
add_itsa_analytic_dimension_defaults(values)
return super().create(vlist)
@classmethod
def default_analytic_dimensions(cls):
default = getattr(super(), 'default_analytic_dimensions', None)
values = default() if default else []
return values or default_itsa_analytic_dimension_assignments()
btb = fields.Many2One('back.to.back',"Back to back")
charter_conditions = fields.One2Many(
'charter.condition', 'purchase', "Charter Conditions")

View File

@@ -19,7 +19,9 @@ from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
from trytond.modules.purchase_trade.company_defaults import (
default_itsa_currency, default_itsa_unit, is_itsa_company, record_id)
add_itsa_analytic_dimension_defaults,
default_itsa_analytic_dimension_assignments, default_itsa_currency,
default_itsa_unit, is_itsa_company, record_id)
logger = logging.getLogger(__name__)
@@ -254,6 +256,18 @@ class Sale(metaclass=PoolMeta):
if sales:
super().process(sales)
@classmethod
def create(cls, vlist):
for values in vlist:
add_itsa_analytic_dimension_defaults(values)
return super().create(vlist)
@classmethod
def default_analytic_dimensions(cls):
default = getattr(super(), 'default_analytic_dimensions', None)
values = default() if default else []
return values or default_itsa_analytic_dimension_assignments()
btb = fields.Many2One('back.to.back',"Back to back")
charter_conditions = fields.One2Many(
'charter.condition', 'sale', "Charter Conditions")

View File

@@ -73,9 +73,49 @@ class Move(metaclass=PoolMeta):
blnumber = fields.Char("BL number")
lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
lot = fields.Many2One('lot.lot',"Lot")
def get_linked_transit_move(self):
if self.from_location.is_transit():
trade_purchase = fields.Function(
fields.Many2One('purchase.purchase', "Purchase"), 'get_trade_purchase')
trade_sale = fields.Function(
fields.Many2One('sale.sale', "Sale"), 'get_trade_sale')
trade_purchase_reference = fields.Function(
fields.Char("Purchase Ref."), 'get_trade_purchase_reference')
trade_sale_reference = fields.Function(
fields.Char("Sale Ref."), 'get_trade_sale_reference')
def _trade_purchase_line(self):
return getattr(getattr(self, 'lot', None), 'line', None)
def _trade_sale_line(self):
return getattr(getattr(self, 'lot', None), 'sale_line', None)
@staticmethod
def _contract_reference(contract):
if not contract:
return None
return (
getattr(contract, 'reference', None)
or getattr(contract, 'our_reference', None))
def get_trade_purchase(self, name):
line = self._trade_purchase_line()
purchase = getattr(line, 'purchase', None)
return purchase.id if purchase else None
def get_trade_sale(self, name):
line = self._trade_sale_line()
sale = getattr(line, 'sale', None)
return sale.id if sale else None
def get_trade_purchase_reference(self, name):
line = self._trade_purchase_line()
return self._contract_reference(getattr(line, 'purchase', None))
def get_trade_sale_reference(self, name):
line = self._trade_sale_line()
return self._contract_reference(getattr(line, 'sale', None))
def get_linked_transit_move(self):
if self.from_location.is_transit():
Move = Pool().get('stock.move')
Location = Pool().get('stock.location')
moves = Move.search([('lot','=',self.lot),('to_location','=',Location.get_transit_id())],order=[('id', 'DESC')],limit=1)

View File

@@ -17,6 +17,7 @@ from trytond.modules.purchase_trade import purchase as purchase_module
from trytond.modules.purchase_trade import sale as sale_module
from trytond.modules.purchase_trade import stock as stock_module
from trytond.modules.purchase_trade import fee as fee_module
from trytond.modules.purchase_trade import company_defaults
from trytond.modules.purchase_trade.service import ContractFactory
@@ -24,6 +25,15 @@ class PurchaseTradeTestCase(ModuleTestCase):
'Test purchase_trade module'
module = 'purchase_trade'
def test_itsa_book_year_suffix_uses_april_fiscal_start(self):
'ITSA book year changes on April 1st'
self.assertEqual(
company_defaults._book_year_suffix(datetime.date(2026, 3, 31)),
'25')
self.assertEqual(
company_defaults._book_year_suffix(datetime.date(2026, 4, 1)),
'26')
@with_transaction()
def test_purchase_list_fields_use_first_trade_line(self):
'purchase list helper fields use the first real purchase line'

View File

@@ -1,6 +1,8 @@
<tree editable="1">
<field name="lot_id" width="70"/>
<field name="lot_purchase"/>
<field name="lot_purchase_reference"/>
<field name="lot_purchase_line_price"/>
<field name="lot_type" width="80"/>
<field name="lot_cp"/>
<field name="lot_product"/>

View File

@@ -1,6 +1,8 @@
<tree editable="1">
<field name="lot_id" width="70"/>
<field name="lot_sale"/>
<field name="lot_sale_reference"/>
<field name="lot_sale_line_price"/>
<field name="lot_type" width="80"/>
<field name="lot_cp"/>
<field name="lot_product"/>

View File

@@ -1,4 +1,8 @@
<tree>
<field name="purchase"/>
<field name="purchase_reference"/>
<field name="sale"/>
<field name="sale_reference"/>
<field name="lot_p"/>
<field name="lot_s"/>
<field name="planned_from_location"/>

View File

@@ -5,8 +5,12 @@
<field name="r_sale_del_period" string="Sale Sh. period" width="110"/>
<field name="r_supplier" width="90"/>
<field name="r_purchase" width="120"/>
<field name="r_purchase_reference" width="120"/>
<field name="r_purchase_line_price" width="100"/>
<field name="r_lot_pur_inv" width="120"/>
<field name="r_sale" width="120"/>
<field name="r_sale_reference" width="120"/>
<field name="r_sale_line_price" width="100"/>
<field name="r_client" width="90"/>
<field name="r_lot_sal_inv" width="120"/>
<field name="r_lot_state" width="60"/>

View File

@@ -4,6 +4,10 @@ this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//field[@name='planned_date']" position="replace">
<field name="lot" optional="0"/>
<field name="trade_purchase" optional="0"/>
<field name="trade_purchase_reference" optional="0"/>
<field name="trade_sale" optional="0"/>
<field name="trade_sale_reference" optional="0"/>
<field name="bldate" optional="0"/>
<field name="blnumber" optional="0"/>
</xpath>