diff --git a/modules/automation/automation.py b/modules/automation/automation.py
index 4e98aee..f3ea1a3 100644
--- a/modules/automation/automation.py
+++ b/modules/automation/automation.py
@@ -248,29 +248,10 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
ShipmentWR.save([swr])
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
logger.info("BL_NUMBER:%s",sh[0].bl_number)
- if sh[0].incoming_moves:
- factor_net = wr.net_landed_kg / wr.bales if wr.bales else 1
- factor_gross = wr.gross_landed_kg / wr.bales if wr.bales else 1
- for move in sh[0].incoming_moves:
- 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"
+ doc.notes = (
+ (doc.notes or "")
+ + "Global WR linked to shipment. "
+ + "Create remote lot WRs from the weight report form.\n")
# if cls.rule_set.ocr_required:[]
# cls.run_ocr([doc])
@@ -293,4 +274,4 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
# except Exception as e:
# doc.state = "error"
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
- doc.save()
\ No newline at end of file
+ doc.save()
diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md
index eb1c3fb..0c706e8 100644
--- a/modules/purchase_trade/docs/business-rules.md
+++ b/modules/purchase_trade/docs/business-rules.md
@@ -306,10 +306,35 @@ Owner technique: `a completer`
- Resultat attendu:
- pour une ligne `party.execution`, `achieved_percent` =
`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
l'ecart `targeted - achieved` est le plus eleve
- un controller a `80%` cible et `40%` reel doit donc passer avant un
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:
- `importante`
- Resultat attendu:
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index ae8e15c..a7ea2fe 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -259,6 +259,48 @@ class PurchaseTradeTestCase(ModuleTestCase):
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):
'sale report helpers aggregate quantity, price lines and shipment periods'
Sale = Pool().get('sale.sale')
diff --git a/modules/purchase_trade/view/weight_report_form.xml b/modules/purchase_trade/view/weight_report_form.xml
index 84773fb..3206dcf 100644
--- a/modules/purchase_trade/view/weight_report_form.xml
+++ b/modules/purchase_trade/view/weight_report_form.xml
@@ -79,9 +79,16 @@
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/modules/purchase_trade/view/weight_report_list.xml b/modules/purchase_trade/view/weight_report_list.xml
index 21552db..b672151 100644
--- a/modules/purchase_trade/view/weight_report_list.xml
+++ b/modules/purchase_trade/view/weight_report_list.xml
@@ -12,6 +12,7 @@
+
-
\ No newline at end of file
+
diff --git a/modules/purchase_trade/weight_report.py b/modules/purchase_trade/weight_report.py
index d8fbcc1..b5fc167 100644
--- a/modules/purchase_trade/weight_report.py
+++ b/modules/purchase_trade/weight_report.py
@@ -1,7 +1,10 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
+from trytond.exceptions import UserError
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime as dt
+import datetime
+import requests
import logging
logger = logging.getLogger(__name__)
@@ -49,21 +52,127 @@ class WeightReport(ModelSQL, ModelView):
invoice_net_kg = fields.Numeric('Invoice Net (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))
+ 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
def __setup__(cls):
super().__setup__()
cls._order = [('report_date', 'DESC')]
- # cls._buttons.update({
- # 'import_json': {},
- # 'export_json': {},
- # })
+ cls._buttons.update({
+ 'create_remote_weight_reports': {},
+ })
def get_rec_name(self, name):
items = [self.lab]
if self.reference:
items.append('[%s]' % self.reference)
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
# @ModelView.button_action('weight_report.act_import_json')
# 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)
# 7. Création du rapport
- return cls.create([report])[0]
\ No newline at end of file
+ return cls.create([report])[0]
diff --git a/modules/purchase_trade/weight_report.xml b/modules/purchase_trade/weight_report.xml
index 4f3d960..f213dfe 100644
--- a/modules/purchase_trade/weight_report.xml
+++ b/modules/purchase_trade/weight_report.xml
@@ -103,18 +103,12 @@
-->
-
-
-->
-
\ No newline at end of file
+