Add WR draft management
This commit is contained in:
@@ -248,29 +248,10 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
ShipmentWR.save([swr])
|
ShipmentWR.save([swr])
|
||||||
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
|
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
|
||||||
logger.info("BL_NUMBER:%s",sh[0].bl_number)
|
logger.info("BL_NUMBER:%s",sh[0].bl_number)
|
||||||
if sh[0].incoming_moves:
|
doc.notes = (
|
||||||
factor_net = wr.net_landed_kg / wr.bales if wr.bales else 1
|
(doc.notes or "")
|
||||||
factor_gross = wr.gross_landed_kg / wr.bales if wr.bales else 1
|
+ "Global WR linked to shipment. "
|
||||||
for move in sh[0].incoming_moves:
|
+ "Create remote lot WRs from the weight report form.\n")
|
||||||
lot = move.lot
|
|
||||||
if lot.lot_type == 'physic':
|
|
||||||
wr_payload = {
|
|
||||||
"chunk_key": lot.lot_chunk_key,
|
|
||||||
"gross_weight": float(round(Decimal(lot.lot_qt) * factor_gross,5)),
|
|
||||||
"net_weight": float(round(Decimal(lot.lot_qt) * factor_net,5)),
|
|
||||||
"tare_total": float(round(wr.tare_kg * (Decimal(lot.lot_qt) / wr.bales),5)) ,
|
|
||||||
"bags": int(lot.lot_qt),
|
|
||||||
"surveyor_code": sh[0].controller.get_alf(),
|
|
||||||
"place_key": sh[0].to_location.get_places(),
|
|
||||||
"report_date": int(wr.report_date.strftime("%Y%m%d")),#wr.report_date.isoformat() if wr.report_date else None,
|
|
||||||
"weight_date": int(wr.weight_date.strftime("%Y%m%d")),#wr.weight_date.isoformat() if wr.weight_date else None,
|
|
||||||
"agent": sh[0].agent.get_alf(),
|
|
||||||
"forwarder_ref": sh[0].returned_id
|
|
||||||
}
|
|
||||||
logger.info("PAYLOAD:%s",wr_payload)
|
|
||||||
data = doc.create_weight_report(wr_payload)
|
|
||||||
doc.notes = (doc.notes or "") + f"WR created in Fintrade: {data.get('success')}\n"
|
|
||||||
doc.notes = (doc.notes or "") + f"WR key: {data.get('weight_report_key')}\n"
|
|
||||||
|
|
||||||
# if cls.rule_set.ocr_required:[]
|
# if cls.rule_set.ocr_required:[]
|
||||||
# cls.run_ocr([doc])
|
# cls.run_ocr([doc])
|
||||||
@@ -293,4 +274,4 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
# except Exception as e:
|
# except Exception as e:
|
||||||
# doc.state = "error"
|
# doc.state = "error"
|
||||||
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|||||||
@@ -306,10 +306,35 @@ Owner technique: `a completer`
|
|||||||
- Resultat attendu:
|
- Resultat attendu:
|
||||||
- pour une ligne `party.execution`, `achieved_percent` =
|
- pour une ligne `party.execution`, `achieved_percent` =
|
||||||
`shipments de la zone avec ce controller / shipments controles de la zone`
|
`shipments de la zone avec ce controller / shipments controles de la zone`
|
||||||
|
- le denominateur ne compte que les `stock.shipment.in` qui ont deja un
|
||||||
|
`controller`; les shipments encore non affectes ne biaisent donc pas la
|
||||||
|
statistique affichee
|
||||||
- lors d'un choix automatique de controller, la priorite va a la regle dont
|
- lors d'un choix automatique de controller, la priorite va a la regle dont
|
||||||
l'ecart `targeted - achieved` est le plus eleve
|
l'ecart `targeted - achieved` est le plus eleve
|
||||||
- un controller a `80%` cible et `40%` reel doit donc passer avant un
|
- un controller a `80%` cible et `40%` reel doit donc passer avant un
|
||||||
controller a `50%` cible et `45%` reel sur la meme zone
|
controller a `50%` cible et `45%` reel sur la meme zone
|
||||||
|
- l'appartenance a la zone se lit depuis `shipment.to_location.country`, et
|
||||||
|
une region parente couvre aussi ses sous-regions
|
||||||
|
- Priorite:
|
||||||
|
- `importante`
|
||||||
|
|
||||||
|
### BR-PT-015 - Les weight reports distants par lot partent du weight report global attache au shipment
|
||||||
|
|
||||||
|
- Intent: separer la creation du `weight.report` global et l'export detaille
|
||||||
|
par lot vers le systeme distant.
|
||||||
|
- Description:
|
||||||
|
- l'automation cree le `weight.report` global et l'attache au
|
||||||
|
`stock.shipment.in`
|
||||||
|
- l'export FastAPI par lot ne part plus directement de l'automation
|
||||||
|
- l'utilisateur ouvre le `weight.report` voulu depuis le shipment et lance
|
||||||
|
l'action d'export depuis ce rapport
|
||||||
|
- Resultat attendu:
|
||||||
|
- le rapport choisi sert de base unique pour calculer les payloads par lot
|
||||||
|
- seuls les lots physiques des `incoming_moves` du shipment sont exportes
|
||||||
|
- l'action exige au minimum un `controller` et un `returned_id` sur le
|
||||||
|
shipment
|
||||||
|
- les cles renvoyees par le systeme distant et la date d'envoi sont
|
||||||
|
conservees sur le `weight.report` local
|
||||||
- Priorite:
|
- Priorite:
|
||||||
- `importante`
|
- `importante`
|
||||||
- Resultat attendu:
|
- Resultat attendu:
|
||||||
|
|||||||
@@ -259,6 +259,48 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
self.assertIs(shipment.get_controller(), party_a)
|
self.assertIs(shipment.get_controller(), party_a)
|
||||||
|
|
||||||
|
def test_weight_report_get_source_shipment_rejects_multiple_shipments(self):
|
||||||
|
'weight report export must not guess when the same WR is linked twice'
|
||||||
|
WeightReport = Pool().get('weight.report')
|
||||||
|
report = WeightReport()
|
||||||
|
report.id = 7
|
||||||
|
|
||||||
|
shipment_wr_model = Mock()
|
||||||
|
shipment_wr_model.search.return_value = [
|
||||||
|
Mock(shipment_in=Mock(id=1)),
|
||||||
|
Mock(shipment_in=Mock(id=2)),
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.weight_report.Pool'
|
||||||
|
) as PoolMock:
|
||||||
|
PoolMock.return_value.get.return_value = shipment_wr_model
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
report.get_source_shipment()
|
||||||
|
|
||||||
|
def test_weight_report_remote_context_requires_controller_and_returned_id(self):
|
||||||
|
'weight report export checks the shipment prerequisites before calling FastAPI'
|
||||||
|
WeightReport = Pool().get('weight.report')
|
||||||
|
report = WeightReport()
|
||||||
|
report.bales = 100
|
||||||
|
report.report_date = Mock(strftime=Mock(return_value='20260406'))
|
||||||
|
report.weight_date = Mock(strftime=Mock(return_value='20260406'))
|
||||||
|
|
||||||
|
shipment = Mock(
|
||||||
|
controller=None,
|
||||||
|
returned_id='RET-001',
|
||||||
|
agent=Mock(),
|
||||||
|
to_location=Mock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
report.validate_remote_weight_report_context(shipment)
|
||||||
|
|
||||||
|
shipment.controller = Mock()
|
||||||
|
shipment.returned_id = None
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
report.validate_remote_weight_report_context(shipment)
|
||||||
|
|
||||||
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
|
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
|
||||||
'sale report helpers aggregate quantity, price lines and shipment periods'
|
'sale report helpers aggregate quantity, price lines and shipment periods'
|
||||||
Sale = Pool().get('sale.sale')
|
Sale = Pool().get('sale.sale')
|
||||||
|
|||||||
@@ -79,9 +79,16 @@
|
|||||||
<label name="weight_date"/>
|
<label name="weight_date"/>
|
||||||
<field name="weight_date"/>
|
<field name="weight_date"/>
|
||||||
</group>
|
</group>
|
||||||
|
<group id="remote_wr" colspan="8" col="4">
|
||||||
|
<button name="create_remote_weight_reports" string="Create Remote WRs" colspan="4"/>
|
||||||
|
<label name="remote_weight_report_sent_at"/>
|
||||||
|
<field name="remote_weight_report_sent_at"/>
|
||||||
|
<label name="remote_weight_report_keys"/>
|
||||||
|
<field name="remote_weight_report_keys" colspan="4"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
<!-- <group id="buttons" colspan="8">
|
<!-- <group id="buttons" colspan="8">
|
||||||
<button name="import_json" string="Import JSON"/>
|
<button name="import_json" string="Import JSON"/>
|
||||||
<button name="export_json" string="Export JSON"/>
|
<button name="export_json" string="Export JSON"/>
|
||||||
</group> -->
|
</group> -->
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<field name="invoice_net_kg"/>
|
<field name="invoice_net_kg"/>
|
||||||
<field name="gain_loss_kg"/>
|
<field name="gain_loss_kg"/>
|
||||||
<field name="gain_loss_percent"/>
|
<field name="gain_loss_percent"/>
|
||||||
|
<field name="remote_weight_report_sent_at"/>
|
||||||
<!-- <button name="import_json" tree_invisible="1"/>
|
<!-- <button name="import_json" tree_invisible="1"/>
|
||||||
<button name="export_json" tree_invisible="1"/> -->
|
<button name="export_json" tree_invisible="1"/> -->
|
||||||
</tree>
|
</tree>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from trytond.model import ModelSQL, ModelView, fields
|
from trytond.model import ModelSQL, ModelView, fields
|
||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
|
from trytond.exceptions import UserError
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -49,21 +52,127 @@ class WeightReport(ModelSQL, ModelView):
|
|||||||
invoice_net_kg = fields.Numeric('Invoice Net (kg)', digits=(16, 2))
|
invoice_net_kg = fields.Numeric('Invoice Net (kg)', digits=(16, 2))
|
||||||
gain_loss_kg = fields.Numeric('Gain/Loss (kg)', digits=(16, 2))
|
gain_loss_kg = fields.Numeric('Gain/Loss (kg)', digits=(16, 2))
|
||||||
gain_loss_percent = fields.Numeric('Gain/Loss (%)', digits=(16, 2))
|
gain_loss_percent = fields.Numeric('Gain/Loss (%)', digits=(16, 2))
|
||||||
|
remote_weight_report_keys = fields.Text('Remote WR Keys', readonly=True)
|
||||||
|
remote_weight_report_sent_at = fields.DateTime(
|
||||||
|
'Remote WR Sent At', readonly=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super().__setup__()
|
super().__setup__()
|
||||||
cls._order = [('report_date', 'DESC')]
|
cls._order = [('report_date', 'DESC')]
|
||||||
# cls._buttons.update({
|
cls._buttons.update({
|
||||||
# 'import_json': {},
|
'create_remote_weight_reports': {},
|
||||||
# 'export_json': {},
|
})
|
||||||
# })
|
|
||||||
|
|
||||||
def get_rec_name(self, name):
|
def get_rec_name(self, name):
|
||||||
items = [self.lab]
|
items = [self.lab]
|
||||||
if self.reference:
|
if self.reference:
|
||||||
items.append('[%s]' % self.reference)
|
items.append('[%s]' % self.reference)
|
||||||
return ' '.join(items)
|
return ' '.join(items)
|
||||||
|
|
||||||
|
def create_remote_weight_report(self, wr_payload):
|
||||||
|
response = requests.post(
|
||||||
|
"http://automation-service:8006/weight-report",
|
||||||
|
json=wr_payload,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_related_shipments(self):
|
||||||
|
ShipmentWR = Pool().get('shipment.wr')
|
||||||
|
links = ShipmentWR.search([('wr', '=', self.id)])
|
||||||
|
return [link.shipment_in for link in links if link.shipment_in]
|
||||||
|
|
||||||
|
def get_source_shipment(self):
|
||||||
|
shipments = self.get_related_shipments()
|
||||||
|
if not shipments:
|
||||||
|
raise UserError('No shipment is linked to this weight report.')
|
||||||
|
unique_shipments = {shipment.id: shipment for shipment in shipments}
|
||||||
|
if len(unique_shipments) > 1:
|
||||||
|
raise UserError(
|
||||||
|
'This weight report is linked to multiple shipments.')
|
||||||
|
return next(iter(unique_shipments.values()))
|
||||||
|
|
||||||
|
def get_remote_weight_report_lots(self, shipment):
|
||||||
|
lots = []
|
||||||
|
seen = set()
|
||||||
|
for move in shipment.incoming_moves or []:
|
||||||
|
lot = getattr(move, 'lot', None)
|
||||||
|
if (not lot or lot.lot_type != 'physic'
|
||||||
|
or lot.id in seen):
|
||||||
|
continue
|
||||||
|
seen.add(lot.id)
|
||||||
|
lots.append(lot)
|
||||||
|
if not lots:
|
||||||
|
raise UserError(
|
||||||
|
'No physical lot was found on the incoming moves.')
|
||||||
|
return lots
|
||||||
|
|
||||||
|
def validate_remote_weight_report_context(self, shipment):
|
||||||
|
if not shipment.controller:
|
||||||
|
raise UserError(
|
||||||
|
'A controller is required before creating remote weight reports.')
|
||||||
|
if not shipment.returned_id:
|
||||||
|
raise UserError(
|
||||||
|
'A returned ID is required before creating remote weight reports.')
|
||||||
|
if not shipment.agent:
|
||||||
|
raise UserError(
|
||||||
|
'A booking agent is required before creating remote weight reports.')
|
||||||
|
if not shipment.to_location:
|
||||||
|
raise UserError(
|
||||||
|
'A destination location is required before creating remote weight reports.')
|
||||||
|
if not self.bales:
|
||||||
|
raise UserError(
|
||||||
|
'The global weight report must define the number of bales.')
|
||||||
|
if not self.report_date or not self.weight_date:
|
||||||
|
raise UserError(
|
||||||
|
'Report date and weight date are required.')
|
||||||
|
|
||||||
|
def build_remote_weight_report_payload(self, shipment, lot):
|
||||||
|
if not lot.lot_chunk_key:
|
||||||
|
raise UserError(
|
||||||
|
'Each physical lot must have a chunk key before export.')
|
||||||
|
factor_net = self.net_landed_kg / self.bales
|
||||||
|
factor_gross = self.gross_landed_kg / self.bales
|
||||||
|
lot_ratio = Decimal(lot.lot_qt) / self.bales
|
||||||
|
return {
|
||||||
|
"chunk_key": lot.lot_chunk_key,
|
||||||
|
"gross_weight": float(round(
|
||||||
|
Decimal(lot.lot_qt) * factor_gross, 5)),
|
||||||
|
"net_weight": float(round(
|
||||||
|
Decimal(lot.lot_qt) * factor_net, 5)),
|
||||||
|
"tare_total": float(round(self.tare_kg * lot_ratio, 5)),
|
||||||
|
"bags": int(lot.lot_qt),
|
||||||
|
"surveyor_code": shipment.controller.get_alf(),
|
||||||
|
"place_key": shipment.to_location.get_places(),
|
||||||
|
"report_date": int(self.report_date.strftime("%Y%m%d")),
|
||||||
|
"weight_date": int(self.weight_date.strftime("%Y%m%d")),
|
||||||
|
"agent": shipment.agent.get_alf(),
|
||||||
|
"forwarder_ref": shipment.returned_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@ModelView.button
|
||||||
|
def create_remote_weight_reports(cls, reports):
|
||||||
|
to_save = []
|
||||||
|
for report in reports:
|
||||||
|
shipment = report.get_source_shipment()
|
||||||
|
report.validate_remote_weight_report_context(shipment)
|
||||||
|
lots = report.get_remote_weight_report_lots(shipment)
|
||||||
|
created = []
|
||||||
|
for lot in lots:
|
||||||
|
payload = report.build_remote_weight_report_payload(
|
||||||
|
shipment, lot)
|
||||||
|
logger.info("REMOTE_WR_PAYLOAD:%s", payload)
|
||||||
|
data = report.create_remote_weight_report(payload)
|
||||||
|
created.append(
|
||||||
|
f"{lot.rec_name}: {data.get('weight_report_key')}")
|
||||||
|
report.remote_weight_report_keys = '\n'.join(created)
|
||||||
|
report.remote_weight_report_sent_at = datetime.datetime.now()
|
||||||
|
to_save.append(report)
|
||||||
|
if to_save:
|
||||||
|
cls.save(to_save)
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# @ModelView.button_action('weight_report.act_import_json')
|
# @ModelView.button_action('weight_report.act_import_json')
|
||||||
# def import_json(cls, reports):
|
# def import_json(cls, reports):
|
||||||
@@ -200,4 +309,4 @@ class WeightReport(ModelSQL, ModelView):
|
|||||||
report['gain_loss_percent'] = gain_loss_percent.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
report['gain_loss_percent'] = gain_loss_percent.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||||
|
|
||||||
# 7. Création du rapport
|
# 7. Création du rapport
|
||||||
return cls.create([report])[0]
|
return cls.create([report])[0]
|
||||||
|
|||||||
@@ -103,18 +103,12 @@
|
|||||||
</record> -->
|
</record> -->
|
||||||
|
|
||||||
<!-- Model Buttons -->
|
<!-- Model Buttons -->
|
||||||
<!-- <record model="ir.model.button" id="weight_report_import_button">
|
<record model="ir.model.button" id="weight_report_create_remote_button">
|
||||||
<field name="model">weight.report</field>
|
<field name="model">weight.report</field>
|
||||||
<field name="name">import_json</field>
|
<field name="name">create_remote_weight_reports</field>
|
||||||
<field name="string">Import JSON</field>
|
<field name="string">Create Remote WRs</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.model.button" id="weight_report_export_button">
|
|
||||||
<field name="model">weight.report</field>
|
|
||||||
<field name="name">export_json</field>
|
|
||||||
<field name="string">Export JSON</field>
|
|
||||||
</record> -->
|
|
||||||
|
|
||||||
<!-- Menu Structure -->
|
<!-- Menu Structure -->
|
||||||
<menuitem
|
<menuitem
|
||||||
name="Weight Reports"
|
name="Weight Reports"
|
||||||
@@ -136,4 +130,4 @@
|
|||||||
sequence="30"
|
sequence="30"
|
||||||
id="menu_gr_weight_reports"/> -->
|
id="menu_gr_weight_reports"/> -->
|
||||||
</data>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
|
|||||||
Reference in New Issue
Block a user