main #7

Merged
admin merged 620 commits from main into dev 2026-03-29 13:03:25 +00:00
155 changed files with 27371 additions and 682 deletions

107
AGENTS.md Normal file
View File

@@ -0,0 +1,107 @@
# AGENTS.md
Guide rapide pour les agents qui codent dans ce repository.
## 1) Contexte du projet
- Codebase Tryton monolithique (coeur + modules metier).
- Noyau serveur a la racine: `application.py`, `wsgi.py`, `admin.py`, `worker.py`, `cron.py`.
- Couches framework importantes:
- ORM: `model/`
- Meta/systeme (`ir`): `ir/`
- Protocoles RPC: `protocols/`
- Backend DB: `backend/`
- Modules metier: `modules/<module_name>/` (~220 modules).
## 2) Regles de travail pour agent
- Ne jamais toucher des fichiers sans rapport avec la demande.
- Limiter le scope de modif au minimum necessaire.
- Respecter le style existant du module cible.
- Ne pas supprimer du code legacy sans verifier les usages.
- Si comportement incertain: preferer un patch conservateur + test.
## 3) Zones de bruit a ignorer pendant l'exploration
- `.venv/`
- `__pycache__/`
- `build/` (quand present dans des sous-modules)
- Fichiers temporaires editeur (ex: `*.swp`)
## 4) Comment choisir ou coder selon le besoin
- Si bug ORM/champs:
- Lire `model/fields/*.py` et les tests `tests/test_field_*.py`.
- Si bug transaction/DB:
- Lire `transaction.py`, `backend/*/database.py`, `tests/test_backend.py`.
- Si bug API/RPC/HTTP:
- Lire `wsgi.py`, `rpc.py`, `protocols/*`, `tests/test_rpc.py`, `tests/test_wsgi.py`.
- Si bug metier:
- Modifier uniquement `modules/<module>/` + ses tests.
- Si bug template Relatorio (`.fodt`):
- Lire d'abord le template standard voisin du meme domaine (`invoice.fodt`, `sale.fodt`, etc.).
- Preferer des proprietes Python simples exposees par le modele plutot que des expressions Genshi complexes dans le template.
- Dans les placeholders XML, utiliser `&quot;` et `&apos;` plutot que des antislashs type `\'`.
- Si un document facture depend fortement d'une vente/achat, ajouter au besoin un petit pont Python pour exposer des `report_*` stables au template.
- Si plusieurs actions de report pointent vers `report_name = 'account.invoice'`, verifier aussi le cache `invoice_report_cache` dans `modules/account_invoice/invoice.py`: un mauvais cache peut faire croire que plusieurs actions utilisent le meme `.fodt`.
- Avant de conclure qu'un template ou une action est faux, verifier si le report alternatif doit bypasser le cache standard.
- Dans `purchase_trade`, pour remonter d'une facture vers shipment, pro forma, freight ou autres donnees logistiques, privilegier le lot physique comme pont entre `purchase.line`, `sale.line` et shipment.
- Pour `FREIGHT VALUE`, ne pas lire un champ direct sur la facture: retrouver le fee de shipment (`shipment_in`) dont le produit est `Maritime freight`, puis utiliser `fee.get_amount()`.
## 5) Workflow de modification (obligatoire)
1. Identifier le module et le flux impacte.
2. Localiser un test existant proche du comportement a changer.
3. Implementer le plus petit patch possible.
4. Ajouter/adapter les tests au plus pres du changement.
5. Lancer la validation ciblee (pas toute la suite si inutile).
6. Donner un resume du risque residuel.
## 6) Checklist avant de rendre une modif
- Le changement est-il limite au domaine demande ?
- Le comportement existant non cible est-il preserve ?
- Les droits/regles (`ir.rule`, acces) sont-ils impactes ?
- Les vues XML et labels sont-ils coherents si un champ change ?
- Les tests modifies couvrent-ils le bug/la feature ?
- Le message de commit (si demande) explique clairement le pourquoi ?
## 7) Tests: point de depart pratique
- Suite coeur: `tests/test_tryton.py`
- Tests coeur par domaine: `tests/test_*.py`
- Tests module:
- `modules/<module>/tests/test_module.py`
- `modules/<module>/tests/test_scenario.py`
- `modules/<module>/tests/scenario_*.rst`
Quand possible, lancer d'abord la cible minimale:
- fichier de test touche
- puis fichier voisin de regression
- puis suite plus large uniquement si necessaire
## 8) Contrat de sortie attendu de l'agent
Toujours fournir:
- Liste des fichiers modifies
- Resume fonctionnel (ce qui change)
- Resume technique (pourquoi ce design)
- Tests executes + resultat
- Risques residuels et impacts potentiels
## 9) Cas sensibles (demander confirmation humaine)
- Changement schema/structure de donnees
- Changement de logique de securite/acces
- Changement de comportement transverse (transaction, pool, RPC, worker)
- Refactor multi-modules sans ticket explicite
## 10) Raccourci de demarrage pour agent
1. Lire ce fichier.
2. Lire le(s) fichier(s) touche(s) et leurs tests.
3. Proposer le patch minimal.
4. Implementer + tester cible.
5. Rendre avec le contrat de sortie (section 8).

View File

@@ -117,6 +117,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
post_date = fields.Date('Post Date', readonly=True)
description = fields.Char('Description', states=_MOVE_STATES)
ext_ref = fields.Char('Ext. Ref')
origin = fields.Reference('Origin', selection='get_origin',
states=_MOVE_STATES)
state = fields.Selection([
@@ -921,6 +922,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
fields.Reference("Move Origin", selection='get_move_origin'),
'get_move_field', searcher='search_move_field')
description = fields.Char('Description', states=_states)
ext_ref = fields.Char('Ext. Ref')
move_description_used = fields.Function(
fields.Char("Move Description", states=_states),
'get_move_field',

View File

@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="origin" colspan="3"/>
<label name="description_used"/>
<field name="description_used" colspan="3"/>
<label name="ext_ref"/>
<field name="ext_ref" colspan="3"/>
<notebook>
<page name="lines">
<field name="lines" colspan="4"

View File

@@ -26,6 +26,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="origin"/>
<label name="description_used"/>
<field name="description_used" colspan="3"/>
<label name="ext_ref"/>
<field name="ext_ref" colspan="3"/>
<notebook colspan="4">
<page string="Other Info" id="info">
<label name="date"/>

View File

@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
<tree editable="1">
<field name="move"/>
<field name="account" expand="1"/>
<field name="ext_ref" expand="1" optional="1"/>
<field name="party" expand="1"/>
<field name="debit" sum="1"/>
<field name="credit" sum="1"/>

View File

@@ -1286,9 +1286,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
remainder = sum(l.debit - l.credit for l in move_lines)
if self.payment_term:
payment_date = self.payment_term_date or self.invoice_date or today
purchase_line = int(str(self.lines[0].origin).split(",")[1]) if self.lines[0].origin else None
term_lines = self.payment_term.compute(
self.total_amount, self.currency, payment_date, purchase_line)
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
logger.info("MODEL:%s",model)
if model:
Line = Pool().get(model)
line = Line(int(str(self.lines[0].origin).split(",")[1]))
logger.info("LINE:%s",line)
term_lines = self.payment_term.compute(
self.total_amount, self.currency, payment_date, line)
else:
term_lines = [(self.payment_term_date or today, self.total_amount)]
past_payment_term_dates = []
@@ -1960,14 +1965,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
if amount < 0:
move_line.debit = Decimal(0)
move_line.credit = -amount
move_line.account = gl.product.account_stock_used
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.credit = Decimal(0)
move_line_.debit = -amount
move_line_.account = gl.product.account_stock_in_used
else:
move_line.debit = amount
move_line.credit = Decimal(0)
move_line.account = gl.product.account_stock_used
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.debit = Decimal(0)
move_line_.credit = amount
move_line_.account = gl.product.account_stock_in_used
@@ -2031,7 +2038,11 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
var_qt = sum([i.quantity for i in gl])
logger.info("LOT_TO_PROCESS:%s",lot)
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
if lot:
if (gl[0].fee and not gl[0].product.landed_cost):
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
Move.save([account_move])
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
adjust_move_lines = []
mov = None
if self.type == 'in':
@@ -3684,13 +3695,19 @@ class InvoiceReport(Report):
Invoice = pool.get('account.invoice')
# Re-instantiate because records are TranslateModel
invoice, = Invoice.browse(records)
if invoice.invoice_report_cache:
report_path = cls._get_action_report_path(action)
use_cache = (
report_path in (None, 'account_invoice/invoice.fodt')
and invoice.invoice_report_cache
)
if use_cache:
return (
invoice.invoice_report_format,
invoice.invoice_report_cache)
else:
result = super()._execute(records, header, data, action)
if invoice.invoice_report_versioned:
if (invoice.invoice_report_versioned
and report_path in (None, 'account_invoice/invoice.fodt')):
format_, data = result
if isinstance(data, str):
data = bytes(data, 'utf-8')
@@ -3707,6 +3724,12 @@ class InvoiceReport(Report):
with Transaction().set_context(language=False):
return super().render(*args, **kwargs)
@staticmethod
def _get_action_report_path(action):
if isinstance(action, dict):
return action.get('report')
return getattr(action, 'report', None)
@classmethod
def execute(cls, ids, data):
pool = Pool()

View File

@@ -264,11 +264,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
<field name="model">account.invoice</field>
</record>
<record model="ir.action.keyword" id="refresh_invoice_report_keyword">
<field name="keyword">form_print</field>
<field name="model">account.invoice,-1</field>
<field name="action" ref="refresh_invoice_report_wizard"/>
</record>
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
<field name="action" ref="refresh_invoice_report_wizard"/>
<field name="group" ref="account.group_account_admin"/>
@@ -293,7 +288,7 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.action.report" id="report_invoice">
<field name="name">Invoice</field>
<field name="name">Provisional Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice.fodt</field>
@@ -318,6 +313,19 @@ this repository contains the full copyright notices and license terms. -->
<field name="action" ref="report_prepayment"/>
</record>
<record model="ir.action.report" id="report_invoice_ict_final">
<field name="name">Final Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice_ict_final.fodt</field>
<field name="single" eval="True"/>
</record>
<record model="ir.action.keyword" id="report_invoice_ict_final_keyword">
<field name="keyword">form_print</field>
<field name="model">account.invoice,-1</field>
<field name="action" ref="report_invoice_ict_final"/>
</record>
<record model="ir.sequence.type" id="sequence_type_account_invoice">
<field name="name">Invoice</field>
</record>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,10 @@ from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateView, Wizard
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
import logging
logger = logging.getLogger(__name__)
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
@@ -46,7 +48,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
'.msg_payment_term_missing_last_remainder',
payment_term=term.rec_name))
def compute(self, amount, currency, date, purchase_line = None):
def compute(self, amount, currency, date, line_ = None):
"""Calculate payment terms and return a list of tuples
with (date, amount) for each payment term line.
@@ -59,7 +61,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
remainder = amount
for line in self.lines:
value = line.get_value(remainder, amount, currency)
value_date = line.get_date(date, purchase_line)
value_date = line.get_date(date, line_)
if value is None or not value_date:
continue
if ((remainder - value) * sign) < Decimal(0):
@@ -155,12 +157,11 @@ class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView):
self.ratio = self.round(1 / self.divisor,
self.__class__.ratio.digits[1])
def get_date(self, date, purchase_line = None):
def get_date(self, date, line = None):
#find date based on trigger:
if purchase_line and self.trigger_event:
PurchaseLine = Pool().get('purchase.line')
purchase_line = PurchaseLine(purchase_line)
trigger_date = purchase_line.get_date(self.trigger_event)
if line and self.trigger_event:
trigger_date = line.get_date(self.trigger_event)
logger.info("DATE_FROM_LINE:%s",trigger_date)
if trigger_date:
date = trigger_date

View File

@@ -92,8 +92,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="invoice_report_revisions" colspan="4"/>
</page>
<page string="Rate management" id="rate">
<label name="warning"/>
<field name="warning"/>
<!-- <label name="warning"/>
<field name="warning"/> -->
<newline/>
<label name="rate"/>
<field name="rate"/>

View File

@@ -0,0 +1,14 @@
# 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 trytond.pool import Pool
from . import account
def register():
Pool.register(
account.AccountTemplate,
module='account_itsa', type_='model')
Pool.register(
account.CreateChart,
module='account_itsa', type_='wizard')

View File

@@ -0,0 +1,40 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import csv
from io import BytesIO, TextIOWrapper
from sql import Table
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from trytond.config import config
from trytond.model import ModelStorage, ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
class AccountTemplate(metaclass=PoolMeta):
__name__ = 'account.account.template'
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
model_data = Table('ir_model_data')
super().__register__(module_name)
class CreateChart(metaclass=PoolMeta):
__name__ = 'account.create_chart'
def default_properties(self, fields):
pool = Pool()
ModelData = pool.get('ir.model.data')
defaults = super().default_properties(fields)
# template_id = ModelData.get_id('account_ch.root')
# if self.account.account_template.id == template_id:
# defaults['account_receivable'] = self.get_account(
# 'account_ch.3400')
# defaults['account_payable'] = self.get_account(
# 'account_ch.6040')
return defaults

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
[tryton]
version=7.2.3
depends:
account
extras_depend:
account_invoice
xml:
account_itsa.xml
#tax_ict.xml

View File

@@ -6,7 +6,7 @@ from decimal import Decimal
from trytond.i18n import gettext
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.exceptions import UserWarning, UserError
from .exceptions import COGSWarning
import logging
@@ -74,8 +74,8 @@ class InvoiceLine(metaclass=PoolMeta):
if move_line.second_currency:
move_line.amount_second_currency = amount
else:
move_line.debit = Decimal(0)
move_line.credit = -amount_converted
move_line.debit = -amount_converted
move_line.credit = Decimal(0)
move_line.account = self.product.account_stock_out_used
if move_line.second_currency:
move_line.amount_second_currency = amount
@@ -171,10 +171,28 @@ class InvoiceLine(metaclass=PoolMeta):
cost = self.amount
else:
cost = self.lot.get_cog()
if not cost or cost == 0:
raise UserError('No COG for this invoice, please generate the reception of the goods')
if self.amount < 0 :
cost *= -1
logger.info("GETMOVELINES_COST:%s",cost)
anglo_saxon_move_lines_ = []
with Transaction().set_context(
company=self.invoice.company.id, date=accounting_date):
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
cost, type_)
if type_ == 'in_supplier' and (self.lot.sale_invoice_line_prov or self.lot.sale_invoice_line) and not self.fee:
anglo_saxon_move_lines_ = self._get_anglo_saxon_move_lines(cost, 'out_customer')
result.extend(anglo_saxon_move_lines)
result.extend(anglo_saxon_move_lines_)
#Fee inventoried delivery management
if self.lot and type_ != 'in_supplier':
FeeLots = Pool().get('fee.lots')
fees = FeeLots.search(['lot','=',self.lot.id])
for fl in fees:
if fl.fee.type == 'ordered' and fl.fee.product.template.landed_cost:
AccountMove = Pool().get('account.move')
account_move = fl.fee._get_account_move_fee(fl.lot,'out')
AccountMove.save([account_move])
return result

View File

@@ -16,11 +16,11 @@ account_names = [
class Category(metaclass=PoolMeta):
__name__ = 'product.category'
account_stock = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock",
'account.account', "Account Stock/Cost Income",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
('type.statement', '=', 'balance'),
# ('type.stock', '=', True),
# ('type.statement', '=', 'balance'),
('company', '=', Eval('context', {}).get('company', -1)),
],
states={
@@ -29,7 +29,7 @@ class Category(metaclass=PoolMeta):
| ~Eval('accounting', False)),
}))
account_stock_in = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock IN",
'account.account', "Account Stock IN/Cost liability",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
@@ -41,7 +41,7 @@ class Category(metaclass=PoolMeta):
| ~Eval('accounting', False)),
}))
account_stock_out = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock OUT",
'account.account', "Account Stock OUT/Cost liability",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
@@ -103,8 +103,8 @@ class CategoryAccount(metaclass=PoolMeta):
'account.account', "Account Stock",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
('type.statement', '=', 'balance'),
# ('type.stock', '=', True),
# ('type.statement', '=', 'balance'),
('company', '=', Eval('company', -1)),
])
account_stock_in = fields.Many2One(

View File

@@ -1,8 +1,11 @@
from trytond.pool import Pool
from . import automation,rules #, document
from . import automation,rules,freight_booking,cron #, document
def register():
Pool.register(
automation.AutomationDocument,
rules.AutomationRuleSet,
freight_booking.FreightBookingInfo,
cron.Cron,
cron.AutomationCron,
module='automation', type_='model')

View File

@@ -1,10 +1,15 @@
from trytond.model import ModelSQL, ModelView, fields, Workflow
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.wizard import Button
from trytond.transaction import Transaction
from sql import Table
from decimal import getcontext, Decimal, ROUND_HALF_UP
import requests
import io
import logging
import json
import re
logger = logging.getLogger(__name__)
@@ -17,6 +22,7 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
('invoice', 'Invoice'),
('statement_of_facts', 'Statement of Facts'),
('weight_report', 'Weight Report'),
('controller', 'Controller'),
('bol', 'Bill of Lading'),
('controller_invoice', 'Controller Invoice'),
], 'Type')
@@ -57,25 +63,53 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
def run_ocr(cls, docs):
for doc in docs:
try:
# Décoder le fichier depuis le champ Binary
file_data = doc.document.data or b""
logger.info(f"File size: {len(file_data)} bytes")
logger.info(f"First 20 bytes: {file_data[:20]}")
logger.info(f"Last 20 bytes: {file_data[-20:]}")
if doc.type == 'weight_report':
# Décoder le fichier depuis le champ Binary
file_data = doc.document.data or b""
logger.info(f"File size: {len(file_data)} bytes")
logger.info(f"First 20 bytes: {file_data[:20]}")
logger.info(f"Last 20 bytes: {file_data[-20:]}")
file_name = doc.document.name or "document"
file_name = doc.document.name or "document"
# Envoyer le fichier au service OCR
response = requests.post(
"http://automation-service:8006/ocr",
files={"file": (file_name, io.BytesIO(file_data))}
)
response.raise_for_status()
data = response.json()
logger.info("RUN_OCR_RESPONSE:%s",data)
doc.ocr_text = data.get("ocr_text", "")
doc.state = "ocr_done"
doc.notes = (doc.notes or "") + "OCR done\n"
# Envoyer le fichier au service OCR
response = requests.post(
"http://automation-service:8006/ocr",
files={"file": (file_name, io.BytesIO(file_data))}
)
response.raise_for_status()
data = response.json()
logger.info("RUN_OCR_RESPONSE:%s",data)
doc.ocr_text = data.get("ocr_text", "")
doc.state = "ocr_done"
doc.notes = (doc.notes or "") + "OCR done\n"
else:
doc.ocr_text = (doc.document.data or b"").decode('utf-8', errors='replace')
match = re.search(r"\bID\s*:\s*(\d+)", doc.ocr_text)
if match:
request_id = match.group(1)
match = re.search(r"\bBL\s*number\s*:\s*([A-Za-z0-9_-]+)", doc.ocr_text, re.IGNORECASE)
if match:
bl_number = match.group(1)
ShipmentIn = Pool().get('stock.shipment.in')
sh = ShipmentIn.search(['bl_number','=',bl_number])
if sh:
sh[0].returned_id = request_id
ShipmentIn.save(sh)
doc.notes = (doc.notes or "") + "Id returned: " + request_id
so_payload = {
"ServiceOrderKey": sh[0].service_order_key,
"ID_Number": request_id
}
response = requests.post(
"http://automation-service:8006/service-order-update",
json=so_payload,
timeout=10
)
response.raise_for_status()
doc.notes = (doc.notes or "") + " SO updated"
except Exception as e:
doc.state = "error"
@@ -177,6 +211,18 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
logger.error("Metadata processing error: %s", e)
doc.save()
def create_weight_report(self,wr_payload):
response = requests.post(
"http://automation-service:8006/weight-report",
json=wr_payload, # 👈 ICI la correction
timeout=10
)
response.raise_for_status()
return response.json()
# -------------------------------------------------------
# FULL PIPELINE
# -------------------------------------------------------
@@ -185,18 +231,66 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
def run_pipeline(cls, docs):
for doc in docs:
try:
if cls.rule_set.ocr_required:
cls.run_ocr([doc])
if cls.rule_set.structure_required and doc.state != "error":
cls.run_structure([doc])
if cls.rule_set.table_required and doc.state != "error":
cls.run_tables([doc])
if cls.rule_set.metadata_required and doc.state != "error":
cls.run_metadata([doc])
if doc.state != "error":
doc.state = "validated"
doc.notes = (doc.notes or "") + "Pipeline completed\n"
logger.info("DATA_TYPE:%s",type(doc.metadata_json))
metadata = json.loads(str(doc.metadata_json))
logger.info("JSON STRUCTURE:%s",metadata)
WeightReport = Pool().get('weight.report')
wr = WeightReport.create_from_json(metadata)
ShipmentIn = Pool().get('stock.shipment.in')
ShipmentWR = Pool().get('shipment.wr')
sh = ShipmentIn.search([('bl_number','ilike',wr.bl_no)])
if sh:
swr = ShipmentWR()
swr.shipment_in = sh[0]
swr.wr = wr
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"
# if cls.rule_set.ocr_required:[]
# cls.run_ocr([doc])
# if cls.rule_set.structure_required and doc.state != "error":
# cls.run_structure([doc])
# if cls.rule_set.table_required and doc.state != "error":
# cls.run_tables([doc])
# if cls.rule_set.metadata_required and doc.state != "error":
# cls.run_metadata([doc])
# if doc.state != "error":
# doc.state = "validated"
# doc.notes = (doc.notes or "") + "Pipeline completed\n"
except Exception as e:
logger.exception("PIPELINE FAILED") # 👈 TRACE COMPLETE
doc.state = "error"
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
doc.save()
raise
# except Exception as e:
# doc.state = "error"
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
doc.save()

View File

@@ -75,7 +75,7 @@
<record model="ir.model.button" id="auto_button1">
<field name="model">automation.document</field>
<field name="name">run_pipeline</field>
<field name="string">Run Full Pipeline</field>
<field name="string">Create Weight Report</field>
</record>
<record model="ir.model.button" id="auto_button2">
<field name="model">automation.document</field>

377
modules/automation/cron.py Normal file
View File

@@ -0,0 +1,377 @@
import requests
from decimal import getcontext, Decimal, ROUND_HALF_UP
from datetime import datetime, timedelta
from trytond.model import fields
from trytond.model import ModelSQL, ModelView
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
import logging
from sql import Table
import traceback
logger = logging.getLogger(__name__)
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.append(
('automation.cron|update_shipment', "Update Shipment from freight booking info")
)
class AutomationCron(ModelSQL, ModelView):
"Automation Cron"
__name__ = 'automation.cron'
frequency = fields.Selection([
('daily', "Daily"),
('weekly', "Weekly"),
('monthly', "Monthly"),
], "Frequency", required=True,
help="How frequently rates must be updated.")
last_update = fields.Date("Last Update", required=True)
@classmethod
def run(cls, crons):
cls.update_shipment()
@classmethod
def update_shipment(cls):
PoolObj = Pool()
ShipmentIn = PoolObj.get('stock.shipment.in')
Party = PoolObj.get('party.party')
Vessel = PoolObj.get('trade.vessel')
Location = PoolObj.get('stock.location')
# Table externe
t = Table('freight_booking_info')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.ShippingInstructionNumber,
t.ShippingInstructionDate,
t.ShippingInstructionQuantity,
t.ShippingInstructionQuantityUnit,
t.NumberOfContainers,
t.ContainerType,
t.Loading,
t.Destination,
t.BookingAgent,
t.Carrier,
t.Vessel,
t.BL_Number,
t.ETD_Date,
t.BL_Date,
t.ExpectedController,
t.Comments,
t.FintradeBookingKey,
))
rows = cursor.fetchall()
logger.info(f"Nombre total de lignes à traiter : {len(rows)}")
# ---- PREMIÈRE TRANSACTION : Création des objets de référence ----
with Transaction().new_transaction() as trans1:
try:
logger.info("Début de la création des objets de référence...")
parties_to_save = []
vessels_to_save = []
locations_to_save = []
parties_cache = {}
vessels_cache = {}
locations_cache = {}
# Collecter les données des objets de référence
for row in rows:
(
si_number, si_date, si_quantity, si_unit,
container_number, container_type,
loading_name, destination_name,
agent_name, carrier_name,
vessel_name, bl_number,
etd_date, bl_date, controller,
comments, fintrade_booking_key
) = row
# Fonction pour obtenir ou créer un Party
def get_or_create_party(name):
if not name:
return None
name_upper = str(name).strip().upper()
if name_upper in parties_cache:
return parties_cache[name_upper]
# Chercher d'abord dans la base
existing = Party.search([('name', '=', name_upper)], limit=1)
if existing:
parties_cache[name_upper] = existing[0]
return existing[0]
# Créer un nouveau
new_p = Party()
new_p.name = name_upper
parties_cache[name_upper] = new_p
parties_to_save.append(new_p)
return new_p
# Fonction pour obtenir ou créer un Vessel
def get_or_create_vessel(name):
if not name:
return None
name_upper = str(name).strip().upper()
if name_upper in vessels_cache:
return vessels_cache[name_upper]
existing = Vessel.search([('vessel_name', '=', name_upper)], limit=1)
if existing:
vessels_cache[name_upper] = existing[0]
return existing[0]
new_v = Vessel()
new_v.vessel_name = name_upper
vessels_cache[name_upper] = new_v
vessels_to_save.append(new_v)
return new_v
# Fonction pour obtenir ou créer une Location
def get_or_create_location(name, type_):
if not name:
return None
name_upper = str(name).strip().upper()
key = f"{name_upper}_{type_}"
if key in locations_cache:
return locations_cache[key]
existing = Location.search([
('name', '=', name_upper),
('type', '=', type_)
], limit=1)
if existing:
locations_cache[key] = existing[0]
return existing[0]
new_loc = Location()
new_loc.name = name_upper
new_loc.type = type_
locations_cache[key] = new_loc
locations_to_save.append(new_loc)
return new_loc
# Collecter les objets à créer
_ = get_or_create_party(carrier_name)
_ = get_or_create_party(agent_name)
_ = get_or_create_vessel(vessel_name)
_ = get_or_create_location(loading_name, 'supplier')
_ = get_or_create_location(destination_name, 'customer')
# Sauvegarder tous les objets de référence
if parties_to_save:
logger.info(f"Création de {len(parties_to_save)} parties...")
Party.save(parties_to_save)
if vessels_to_save:
logger.info(f"Création de {len(vessels_to_save)} vessels...")
Vessel.save(vessels_to_save)
if locations_to_save:
logger.info(f"Création de {len(locations_to_save)} locations...")
Location.save(locations_to_save)
trans1.commit()
logger.info("Première transaction commitée : objets de référence créés")
except Exception as e:
trans1.rollback()
logger.error(f"Erreur dans la création des objets de référence : {e}")
logger.error(traceback.format_exc())
raise
# ---- TRANSACTIONS INDIVIDUELLES pour chaque shipment ----
successful_shipments = 0
failed_shipments = []
# Recréer le curseur après la nouvelle transaction
cursor2 = Transaction().connection.cursor()
cursor2.execute(*t.select(
t.ShippingInstructionNumber,
t.ShippingInstructionDate,
t.ShippingInstructionQuantity,
t.ShippingInstructionQuantityUnit,
t.NumberOfContainers,
t.ContainerType,
t.Loading,
t.Destination,
t.BookingAgent,
t.Carrier,
t.Vessel,
t.BL_Number,
t.ETD_Date,
t.BL_Date,
t.ExpectedController,
t.Comments,
t.FintradeBookingKey,
))
rows2 = cursor2.fetchall()
for i, row in enumerate(rows2, 1):
(
si_number, si_date, si_quantity, si_unit,
container_number, container_type,
loading_name, destination_name,
agent_name, carrier_name,
vessel_name, bl_number,
etd_date, bl_date, controller,
comments, fintrade_booking_key
) = row
logger.info(f"Traitement shipment {i}/{len(rows2)} : SI {si_number}")
# ---- TRANSACTION INDIVIDUELLE pour ce shipment ----
try:
with Transaction().new_transaction() as trans_shipment:
logger.info(f"Début transaction pour SI {si_number}")
# Vérifier si le shipment existe déjà
existing_shipment = ShipmentIn.search([
('reference', '=', si_number)
], limit=1)
if existing_shipment:
logger.info(f"Shipment {si_number} existe déjà, ignoré")
trans_shipment.commit()
continue
# Récupérer les objets (maintenant ils existent dans la base)
carrier = None
if carrier_name:
carrier_list = Party.search([('name', '=', str(carrier_name).strip().upper())], limit=1)
if carrier_list:
carrier = carrier_list[0]
logger.info(f"Carrier trouvé pour {si_number}: {carrier.name}")
else:
logger.warning(f"Carrier NON TROUVÉ pour {si_number}: '{carrier_name}'")
agent = None
agent_list = Party.search([('name', '=', str(agent_name or 'TBN').strip().upper())], limit=1)
if agent_list:
agent = agent_list[0]
vessel = None
if vessel_name:
vessel_list = Vessel.search([('vessel_name', '=', str(vessel_name).strip().upper())], limit=1)
if vessel_list:
vessel = vessel_list[0]
loc_from = None
if loading_name:
loc_from_list = Location.search([
('name', '=', str(loading_name).strip().upper()),
('type', '=', 'supplier')
], limit=1)
if loc_from_list:
loc_from = loc_from_list[0]
loc_to = None
if destination_name:
loc_to_list = Location.search([
('name', '=', str(destination_name).strip().upper()),
('type', '=', 'customer')
], limit=1)
if loc_to_list:
loc_to = loc_to_list[0]
# Vérification critique du carrier
if not carrier:
error_msg = f"ERREUR CRITIQUE: Carrier manquant pour SI {si_number} (valeur: '{carrier_name}')"
logger.error(error_msg)
raise ValueError(error_msg)
# Créer le shipment
shipment = ShipmentIn()
shipment.reference = si_number
shipment.from_location = loc_from
shipment.to_location = loc_to
shipment.carrier = None #carrier
shipment.supplier = agent
shipment.agent = agent
shipment.vessel = vessel
shipment.cargo_mode = 'bulk'
shipment.bl_number = bl_number
shipment.bl_date = bl_date
shipment.etd = etd_date
shipment.etad = shipment.bl_date + timedelta(days=20)
# Sauvegarder ce shipment uniquement
ShipmentIn.save([shipment])
inv_date,inv_nb = shipment._create_lots_from_fintrade()
shipment.controller = shipment.get_controller()
shipment.controller_target = controller
shipment.create_fee(shipment.controller)
shipment.instructions = shipment.get_instructions_html(inv_date,inv_nb)
ShipmentIn.save([shipment])
trans_shipment.commit()
successful_shipments += 1
logger.info(f"✓ Shipment {si_number} créé avec succès")
except Exception as e:
# Cette transaction échoue mais les autres continuent
error_details = {
'si_number': si_number,
'carrier_name': carrier_name,
'error': str(e),
'traceback': traceback.format_exc()
}
failed_shipments.append(error_details)
logger.error(f"✗ ERREUR pour shipment {si_number}: {e}")
logger.error(f" Carrier: '{carrier_name}'")
logger.error(f" Agent: '{agent_name}'")
logger.error(f" Vessel: '{vessel_name}'")
logger.error(" Traceback complet:")
for line in traceback.format_exc().split('\n'):
if line.strip():
logger.error(f" {line}")
# ---- RÉSUMÉ FINAL ----
logger.info("=" * 60)
logger.info("RÉSUMÉ DE L'EXÉCUTION")
logger.info("=" * 60)
logger.info(f"Total de shipments à traiter : {len(rows2)}")
logger.info(f"Shipments créés avec succès : {successful_shipments}")
logger.info(f"Shipments en échec : {len(failed_shipments)}")
if failed_shipments:
logger.info("\nDétail des échecs :")
for i, error in enumerate(failed_shipments, 1):
logger.info(f" {i}. SI {error['si_number']}:")
logger.info(f" Carrier: '{error['carrier_name']}'")
logger.info(f" Erreur: {error['error']}")
# Log supplémentaire pour debug
logger.info("\nAnalyse des carriers problématiques :")
problematic_carriers = {}
for error in failed_shipments:
carrier = error['carrier_name']
if carrier in problematic_carriers:
problematic_carriers[carrier] += 1
else:
problematic_carriers[carrier] = 1
for carrier, count in problematic_carriers.items():
logger.info(f" Carrier '{carrier}' : {count} échec(s)")
# Vérifier si ce carrier existe dans la base
existing = Party.search([('name', '=', str(carrier).strip().upper())], limit=1)
if existing:
logger.info(f" → EXISTE DANS LA BASE (ID: {existing[0].id})")
else:
logger.info(f" → N'EXISTE PAS DANS LA BASE")
logger.info("=" * 60)

View File

@@ -0,0 +1,37 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="cron_view_list">
<field name="model">automation.cron</field>
<field name="type">tree</field>
<field name="name">cron_list</field>
</record>
<record model="ir.ui.view" id="cron_view_form">
<field name="model">automation.cron</field>
<field name="type">form</field>
<field name="name">cron_form</field>
</record>
<record model="ir.action.act_window" id="act_cron_form">
<field name="name">Update shipment from freight booking</field>
<field name="res_model">automation.cron</field>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="cron_view_list"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="cron_view_form"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.cron" id="cron_cron">
<field name="method">automation.cron|update_shipment</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">days</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,56 @@
from trytond.model import ModelSQL, ModelView, fields
from sql import Table
from sql.functions import CurrentTimestamp
from sql import Column, Literal
import logging
logger = logging.getLogger(__name__)
class FreightBookingInfo(ModelSQL, ModelView):
"Freight Booking"
__name__ = 'freight.booking.info'
booking_number = fields.Char("Booking Number")
agent = fields.Char("Agent")
controller = fields.Char("Customer")
origin = fields.Char("Origin")
destination = fields.Char("Destination")
etd = fields.Date("ETD")
bl_date = fields.Date("BL date")
bl_number = fields.Char("BL Nb")
carrier = fields.Char("Carrier")
vessel = fields.Char("Vessel")
container_count = fields.Float("Containers")
quantity = fields.Float("Gross Weight")
@classmethod
def table_query(cls):
t = Table('freight_booking_info')
query = t.select(
Literal(None).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Column(t, 'FintradeBookingKey').as_('id'),
Column(t, 'ShippingInstructionNumber').as_('booking_number'),
Column(t, 'BookingAgent').as_('agent'),
Column(t, 'ExpectedController').as_('controller'),
Column(t, 'Loading').as_('origin'),
Column(t, 'Destination').as_('destination'),
Column(t, 'ETD_Date').as_('etd'),
Column(t, 'BL_Date').as_('bl_date'),
Column(t, 'BL_Number').as_('bl_number'),
Column(t, 'Carrier').as_('carrier'),
Column(t, 'Vessel').as_('vessel'),
Column(t, 'NumberOfContainers').as_('container_count'),
Column(t, 'ShippingInstructionQuantity').as_('quantity'),
)
#logger.info("*****QUERY*****:%s",query)
return query
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [
('etd', 'DESC'),
]

View File

@@ -0,0 +1,25 @@
<tryton>
<data>
<record model="ir.ui.view" id="freight_booking_info_tree">
<field name="model">freight.booking.info</field>
<field name="type">tree</field>
<field name="name">freight_booking_info_tree</field>
</record>
<record model="ir.action.act_window" id="act_freight_booking_info">
<field name="name">Freight Bookings</field>
<field name="res_model">freight.booking.info</field>
</record>
<record model="ir.action.act_window.view" id="act_freight_booking_info_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="freight_booking_info_tree"/>
<field name="act_window" ref="act_freight_booking_info"/>
</record>
<menuitem
name="Freight Booking"
action="act_freight_booking_info"
parent="menu_automation"
sequence="10"
id="menu_freight_booking" />
</data>
</tryton>

View File

@@ -5,4 +5,6 @@ depends:
res
document_incoming
xml:
automation.xml
automation.xml
freight_booking.xml
cron.xml

View File

@@ -0,0 +1,13 @@
<tree>
<field name="booking_number"/>
<field name="agent"/>
<field name="controller"/>
<field name="origin"/>
<field name="destination"/>
<field name="etd"/>
<field name="bl_date"/>
<field name="bl_number"/>
<field name="carrier"/>
<field name="vessel"/>
<field name="container_count"/>
</tree>

View File

@@ -27,6 +27,14 @@ class Sale(metaclass=PoolMeta):
invoice.save()
return invoice
@property
def report_agent(self):
if self.agent:
return (self.agent.party.address_get(
type='delivery')).full_address
else:
return ''
@classmethod
@ModelView.button
@Workflow.transition('quotation')

View File

@@ -137,6 +137,13 @@ class Currency(
closer = date
return res
@classmethod
def get_by_name(cls, name):
currencies = cls.search([('symbol', '=', name)], limit=1)
if not currencies:
return None
return currencies[0]
@staticmethod
def _get_rate(currencies, tdate=None):
'''

View File

@@ -17,6 +17,8 @@ from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
from .exceptions import DocumentIncomingSplitError
import logging
logger = logging.getLogger(__name__)
if config.getboolean('document_incoming', 'filestore', default=True):
file_id = 'file_id'
@@ -179,30 +181,112 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
def _split_mime_types(cls):
return ['application/pdf']
# @classmethod
# def from_inbound_email(cls, email_, rule):
# message = email_.as_dict()
# attachments = message.get('attachments')
# active = False
# data = message.get('text', message.get('html'))
# logger.info("DATA_FROM_INBOUND_MAIL:%s",data)
# if isinstance(data, str):
# data = data.encode()
# body = message.get('text') or message.get('html') or ''
# if isinstance(body, str):
# body_bytes = body.encode('utf-8')
# else:
# body_bytes = body
# document = cls(
# active=active,
# name=message.get('subject', 'No Subject'),
# company=rule.document_incoming_company,
# data=data,
# type=rule.document_incoming_type if active else None,
# source='inbound_email',
# )
# children = []
# if attachments:
# for attachment in attachments:
# child = cls(
# name=attachment['filename'] or 'data.bin',
# company=rule.document_incoming_company,
# data=attachment['data'],
# type=rule.document_incoming_type,
# source='inbound_email')
# children.append(child)
# else:
# child = cls(
# name='mail_' + message.get('subject', 'No Subject') + '.txt',
# company=rule.document_incoming_company,
# data=body_bytes,
# type=rule.document_incoming_type,
# source='inbound_email',
# )
# children.append(child)
# document.children = children
# document.save()
# return document
@classmethod
def from_inbound_email(cls, email_, rule):
message = email_.as_dict()
active = not message.get('attachments')
def clean(value):
if not value:
return value
return (
value
.replace('\n', ' ')
.replace('\r', ' ')
.replace("'", '')
.replace('"', '')
.strip()
)
subject = clean(message.get('subject', 'No Subject'))
attachments = message.get('attachments')
active = False
data = message.get('text', message.get('html'))
logger.info("DATA_FROM_INBOUND_MAIL:%s", data)
if isinstance(data, str):
data = data.encode()
body = message.get('text') or message.get('html') or ''
if isinstance(body, str):
body_bytes = body.encode('utf-8')
else:
body_bytes = body
document = cls(
active=active,
name=message.get('subject', 'No Subject'),
name=subject,
company=rule.document_incoming_company,
data=data,
type=rule.document_incoming_type if active else None,
source='inbound_email',
)
)
children = []
for attachment in message.get('attachments', []):
if attachments:
for attachment in attachments:
filename = clean(attachment['filename'] or 'data.bin')
child = cls(
name=filename,
company=rule.document_incoming_company,
data=attachment['data'],
type=rule.document_incoming_type,
source='inbound_email')
children.append(child)
else:
child = cls(
name=attachment['filename'] or 'data.bin',
name='mail_' + subject + '.txt',
company=rule.document_incoming_company,
data=attachment['data'],
data=body_bytes,
type=rule.document_incoming_type,
source='inbound_email')
source='inbound_email',
)
children.append(child)
document.children = children
document.save()
return document
@@ -265,7 +349,6 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
default.setdefault('children')
return super().copy(documents, default=default)
def iter_pages(expression, size):
ranges = set()
for pages in expression.split(','):

View File

@@ -4,7 +4,8 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
import logging
logger = logging.getLogger(__name__)
class Rule(metaclass=PoolMeta):
__name__ = 'inbound.email.rule'
@@ -53,4 +54,4 @@ class Rule(metaclass=PoolMeta):
if (self.action == 'document.incoming|from_inbound_email'
and self.document_incoming_process):
document = email_.result
DocumentIncoming.process([document], with_children=True)
DocumentIncoming.process([document], with_children=True)

View File

@@ -6,6 +6,7 @@ from trytond.model import fields
from trytond.modules.document_incoming.exceptions import (
DocumentIncomingProcessError)
from trytond.pool import Pool, PoolMeta
import json
class IncomingConfiguration(metaclass=PoolMeta):
@@ -22,16 +23,15 @@ class Incoming(metaclass=PoolMeta):
super().__setup__()
cls.type.selection.append(
('weight_report', "Weight Report"))
cls.type.selection.append(
('controller', "Controller"))
@classmethod
def _get_results(cls):
return super()._get_results() | {'automation.document'}
def _process_weight_report(self):
pool = Pool()
WR = pool.get('automation.document')
# Configuration = pool.get('document.incoming.configuration')
# config = Configuration(1)
WR = Pool().get('automation.document')
wr = WR()
wr.document = self.id
wr.type = 'weight_report'
@@ -39,6 +39,19 @@ class Incoming(metaclass=PoolMeta):
WR.save([wr])
WR.run_ocr([wr])
WR.run_metadata([wr])
return wr
def _process_controller(self):
WR = Pool().get('automation.document')
wr = WR()
wr.document = self.id
wr.type = 'controller'
wr.state = 'draft'
WR.save([wr])
WR.run_ocr([wr])
# WR.run_metadata([wr])
return wr
# @property

View File

@@ -17,6 +17,9 @@ from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.url import http_host
import logging
logger = logging.getLogger(__name__)
if config.getboolean('inbound_email', 'filestore', default=True):
file_id = 'data_id'
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
@@ -74,6 +77,7 @@ class Inbox(ModelSQL, ModelView):
assert email_.inbox == self
for rule in self.rules:
if rule.match(email_.as_dict()):
logger.info("RULE_MATCHED:%s",rule)
email_.rule = rule
rule.run(email_)
return

View File

@@ -12,7 +12,7 @@ __all__ = ['IncotermMixin', 'IncotermAvailableMixin']
class IncotermMixin(Model):
incoterm = fields.Many2One(
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'),
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'), required=False,
ondelete='RESTRICT')
incoterm_location = fields.Many2One(
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),

View File

@@ -477,15 +477,16 @@ class Lot(ModelSQL, ModelView):
else:
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
def get_hist_quantity(self,seq):
def get_hist_quantity(self,state_id=0):
qt = Decimal(0)
gross_qt = Decimal(0)
if self.lot_state:
if self.lot_hist:
if seq != 0:
st = seq
if state_id != 0:
st = state_id
else:
st = self.lot_state.id
logger.info("GET_HIST_QT:%s",st)
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
qt = round(lot.quantity,5)
gross_qt = round(lot.gross_quantity,5)
@@ -499,24 +500,48 @@ class Lot(ModelSQL, ModelView):
physic_sum = Decimal(0)
for l in line.lots:
if l.lot_type == 'physic' :
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit)),5)
factor = None
rate = None
if l.lot_unit_line.category.id != l.line.unit.category.id:
factor = 1
rate = 1
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit, True, factor, rate)),5)
return line.quantity_theorical - physic_sum
def get_current_quantity(self,name=None):
# if self.lot_type == 'physic':
qt, gross_qt = self.get_hist_quantity(0)
qt, gross_qt = self.get_hist_quantity()
return qt
# else:
# return self.get_virtual_diff()
def get_current_quantity_converted(self,name=None):
def get_current_quantity_converted(self,state_id=0,unit=None):
Uom = Pool().get('product.uom')
unit = self.line.unit if self.line else self.sale_line.unit
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
if not unit:
unit = self.line.unit if self.line else self.sale_line.unit
qt, gross_qt = self.get_hist_quantity(state_id)
factor = None
rate = None
if self.lot_unit_line.category.id != unit.category.id:
factor = 1
rate = 1
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(qt), unit, True, factor, rate)),5)
def get_current_gross_quantity_converted(self,state_id=0,unit=None):
Uom = Pool().get('product.uom')
if not unit:
unit = self.line.unit if self.line else self.sale_line.unit
qt, gross_qt = self.get_hist_quantity(state_id)
factor = None
rate = None
if self.lot_unit_line.category.id != unit.category.id:
factor = 1
rate = 1
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(gross_qt), unit, True, factor, rate)),5)
def get_current_gross_quantity(self,name=None):
if self.lot_type == 'physic':
qt, gross_qt = self.get_hist_quantity(0)
qt, gross_qt = self.get_hist_quantity()
return gross_qt
else:
return None
@@ -526,6 +551,7 @@ class Lot(ModelSQL, ModelView):
lqh = LotQtHist()
lqh.quantity_type = qt_type
lqh.quantity = net
logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
lqh.gross_quantity = gross
lqh.lot = self
return lqh
@@ -542,6 +568,7 @@ class Lot(ModelSQL, ModelView):
if existing:
hist = existing[0]
hist.quantity = net
logger.info("SET_CURRENT_HIST:%s",gross)
hist.gross_quantity = gross
else:
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))

View File

@@ -44,7 +44,7 @@ class Price(
price_composite = fields.One2Many('price.composite','price',"Composites")
price_product = fields.One2Many('price.product', 'price', "Product")
price_ct_size = fields.Numeric("Ct size")
def get_qt(self,nb_ct,unit):
Uom = Pool().get('product.uom')
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
@@ -71,7 +71,6 @@ class Price(
def get_price(self,dt,unit,currency,last=False):
price = float(0)
PV = Pool().get('price.price_value')
logger.info("ASKED_PRICE_FOR:%s",dt)
if self.price_values:
dt = dt.strftime("%Y-%m-%d")
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
@@ -115,7 +114,6 @@ class Calendar(DeactivableMixin,ModelSQL,ModelView,MultiValueMixin):
dt = dt.strftime("%Y-%m-%d")
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
if cl:
#logger.info("ISQUOTE:%s",cl)
return False
else:
return True
@@ -136,3 +134,20 @@ class Product(ModelSQL,ModelView):
__name__ = 'price.product'
price = fields.Many2One('price.price',"Price index")
product = fields.Many2One('product.product',"Product")
attributes = fields.Many2One('product.attribute',"Attribute",domain=[
('sets', '=', Eval('attribute_set')),
],
states={
'readonly': ~Eval('attribute_set'),
},
depends=['product', 'attribute_set'])
attribute_set = fields.Function(
fields.Many2One('product.attribute.set', "Attribute Set"),
'on_change_with_attribute_set'
)
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
if self.product and self.product.template and self.product.template.attribute_set:
return self.product.template.attribute_set.id

View File

@@ -1,6 +1,6 @@
<form>
<label name="price"/>
<field name="price"/>
<label name="product"/>
<field name="product"/>
<label name="attributes"/>
<field name="attributes"/>
</form>

View File

@@ -1,4 +1,5 @@
<tree>
<field name="price"/>
<field name="product"/>
<field name="attributes"/>
</tree>

View File

@@ -609,6 +609,26 @@ class Product(
('template.code', operator, code_value, *extra),
]
@classmethod
def get_by_name(cls, name, type_='goods'):
pool = Pool()
Template = pool.get('product.template')
Uom = pool.get('product.uom')
templates = Template.search([('name', '=', name)], limit=1)
if templates:
return templates[0].products[0]
unit_uom, = Uom.search([('name', '=', 'Mt')], limit=1)
template, = Template.create([{
'name': name,
'type': type_,
'default_uom': unit_uom.id,
'cost_price_method': 'fixed',
}])
return template.products[0]
@staticmethod
def get_price_uom(products, name):
Uom = Pool().get('product.uom')

View File

@@ -92,6 +92,13 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
def default_digits():
return 2
@classmethod
def get_by_name(cls, name):
uom = cls.search([('symbol', '=', name)], limit=1)
if not uom:
return None
return uom[0]
@fields.depends('factor')
def on_change_factor(self):
if (self.factor or 0.0) == 0.0:

View File

@@ -22,6 +22,7 @@ class Month(ModelView, ModelSQL):
is_cotation = fields.Boolean("Cotation month")
beg_date = fields.Date("Date from")
end_date = fields.Date("Date end")
description = fields.Char("Description")
class ProductMonth(ModelView, ModelSQL):
"Product month"

105
modules/purchase/AGENTS.md Normal file
View File

@@ -0,0 +1,105 @@
# AGENTS.md - Module `purchase`
Ce guide complete le `AGENTS.md` racine.
Pour ce module, les regles locales ci-dessous priment.
## 1) Perimetre metier
Le module `purchase` gere le cycle d'achat fournisseur:
- commande d'achat (`purchase.purchase`, `purchase.line`)
- facturation fournisseur (`account.invoice` liee a l'achat)
- reception/retour de stock (`stock.move`, `stock.shipment.in`, `stock.shipment.in.return`)
- reporting achats (axes temporels, fournisseur, produit)
## 2) Fichiers pivots
- Logique coeur:
- `modules/purchase/purchase.py`
- Extensions metier connexes:
- `modules/purchase/product.py`
- `modules/purchase/stock.py`
- `modules/purchase/invoice.py`
- `modules/purchase/party.py`
- `modules/purchase/configuration.py`
- `modules/purchase/purchase_reporting.py`
- Vues et actions:
- `modules/purchase/purchase.xml`
- `modules/purchase/stock.xml`
- `modules/purchase/invoice.xml`
- `modules/purchase/purchase_reporting.xml`
- Manifest et dependances:
- `modules/purchase/tryton.cfg`
- Documentation metier:
- `modules/purchase/docs/business-rules.template.md` (template)
- `modules/purchase/docs/business-rules.md` (instance a remplir)
## 3) Etats et flux critiques a preserver
Workflow de commande (dans `purchase.py`):
- `draft -> quotation -> confirmed -> processing -> done`
- transitions de retour existent aussi (`cancelled`, retour a `draft`, etc.)
Invariants importants:
- `invoice_state` et `shipment_state` doivent rester coherents apres `process()`.
- `process()` orchestre facture + stock + recalcul d'etats, ne pas contourner sans raison.
- `delete()` exige une commande annulee.
- Les methodes `create_invoice()` et `create_move()` sont sensibles (gestion `lots` et `action`).
## 4) Couplages a surveiller
- Facture:
- `purchase.py` <-> `invoice.py`
- gestion des exceptions facture (`purchase_exception_state`)
- Stock:
- `purchase.py` <-> `stock.py`
- liens `moves`, expeditions entrantes, retours
- Produit/fournisseur/prix:
- `product.py` impacte prix d'achat, UoM, fournisseurs
- Tiers:
- `party.py` impacte adresses/parametres fournisseur et contraintes d'effacement
## 5) Convention de modification pour ce module
1. Modifier d'abord le coeur minimal dans `purchase.py` ou le fichier specialise adequat.
2. Mettre a jour XML uniquement si comportement UI/action change.
3. Si regle metier impactee, mettre a jour `docs/business-rules.md`.
4. Ajouter un test proche du flux reel (scenario `.rst` prioritaire si possible).
5. Verifier les impacts transverses facture/stock avant rendu.
## 6) Strategie de test recommandee
Priorite 1 (rapide):
- `modules/purchase/tests/test_module.py`
Priorite 2 (comportement metier):
- `modules/purchase/tests/test_scenario.py`
- Scenarios cibles selon la modif:
- `scenario_purchase.rst`
- `scenario_purchase_manual_invoice.rst`
- `scenario_purchase_line_cancelled.rst`
- `scenario_purchase_line_cancelled_on_shipment.rst`
- `scenario_purchase_return_wizard.rst`
- `scenario_purchase_reporting.rst`
Si la modif touche prix/UoM/fournisseur:
- ajouter un cas dans `test_module.py` ou un scenario dedie.
## 7) Cas qui exigent validation humaine
- Changement du workflow d'etats
- Changement des regles de creation facture/mouvement
- Changement de logique sur retours fournisseur
- Changement qui altere les ecritures comptables ou le statut de paiement
## 8) Definition of done (module `purchase`)
- Le flux metier cible fonctionne de bout en bout.
- Les etats `state`, `invoice_state`, `shipment_state` restent coherents.
- Les tests du module pertinents passent.
- Le patch est limite aux fichiers necessaires.

View File

@@ -0,0 +1,122 @@
# Business Rules Template - Purchase
Statut: `draft` | `reviewed` | `approved`
Version: `v0.1`
Derniere mise a jour: `YYYY-MM-DD`
Owner metier: `Nom / Equipe`
Owner technique: `Nom / Equipe`
## 1) Scope
- Domaine: `ex: achats fournisseur`
- Hors scope: `ex: achats intercompany`
- Modules impactes:
- `purchase`
- `stock` (si applicable)
- `account_invoice` (si applicable)
## 2) Glossaire
- `Purchase`: commande d'achat fournisseur.
- `Line`: ligne de commande.
- `Invoice State`: etat facture calcule.
- `Shipment State`: etat reception calcule.
- Ajouter ici les termes metier propres a ton contexte.
## 3) Regles metier (source de verite)
### BR-001 - [Titre court]
- Intent: `Pourquoi cette regle existe`
- Description:
- `Enonce clair et testable`
- Conditions d'entree:
- `Etat`
- `Type de ligne (goods/service)`
- `Contexte (societe, devise, fournisseur, lot, etc.)`
- Resultat attendu:
- `Etat/valeur/action attendue`
- Exceptions:
- `Cas ou la regle ne s'applique pas`
- Priorite:
- `bloquante | importante | informative`
- Source:
- `Ticket / spec / decision metier`
### BR-002 - [Titre court]
- Intent:
- Description:
- Conditions d'entree:
- Resultat attendu:
- Exceptions:
- Priorite:
- Source:
## 4) Matrice d'etats (optionnel mais recommande)
| Regle | Etat initial | Evenement | Etat attendu | Notes |
|---|---|---|---|---|
| BR-001 | `draft` | `quote` | `quotation` | |
| BR-002 | `quotation` | `confirm` | `confirmed/processing` | |
## 5) Exemples concrets
### Exemple E1 - Cas nominal
- Donnees:
- `fournisseur = X`
- `produit = Y`
- `quantite = 10`
- Attendu:
- `invoice_state = pending`
- `shipment_state = waiting`
### Exemple E2 - Cas limite
- Donnees:
- Attendu:
## 6) Impact code attendu
- Fichiers Python potentiellement concernes:
- `modules/purchase/purchase.py`
- `modules/purchase/stock.py`
- `modules/purchase/invoice.py`
- `modules/purchase/product.py`
- Fichiers XML potentiellement concernes:
- `modules/purchase/purchase.xml`
- `modules/purchase/stock.xml`
- `modules/purchase/invoice.xml`
## 7) Strategie de tests
- Unitaires:
- `modules/purchase/tests/test_module.py`
- Scenarios:
- `modules/purchase/tests/scenario_purchase.rst`
- `modules/purchase/tests/scenario_purchase_manual_invoice.rst`
- `modules/purchase/tests/scenario_purchase_return_wizard.rst`
Pour chaque regle BR-xxx, lister le test associe:
| Regle | Test existant | Nouveau test a ajouter | Statut |
|---|---|---|---|
| BR-001 | `...` | `...` | `todo` |
## 8) Compatibilite et migration
- Effet retroactif sur commandes existantes: `oui/non`
- Migration necessaire: `oui/non`
- Plan de rollback:
- `comment revenir en arriere sans corruption metier`
## 9) Validation
- Valide par metier:
- `Nom` - `date`
- Valide par technique:
- `Nom` - `date`
- Decision finale:
- `approved / rejected / needs update`

View File

@@ -89,14 +89,14 @@ class Purchase(
number = fields.Char("Number", readonly=True)
reference = fields.Char("Reference")
description = fields.Char('Description', size=None, states=_states)
purchase_date = fields.Date('Purchase Date',
purchase_date = fields.Date('Purchase Date', required=True,
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
})
payment_term = fields.Many2One(
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
'account.invoice.payment_term', "Payment Term", required=True, ondelete='RESTRICT',
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
})
@@ -389,6 +389,11 @@ class Purchase(
def default_state():
return 'draft'
@classmethod
def default_purchase_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_currency(cls, **pattern):
pool = Pool()
@@ -462,6 +467,8 @@ class Purchase(
self.tol_min = self.party.tol_min
if self.party.tol_max:
self.tol_max = self.party.tol_max
if self.party.origin:
self.product_origin = self.party.origin
if self.party.wb:
self.wb = self.party.wb
if self.party.association:
@@ -734,6 +741,7 @@ class Purchase(
@classmethod
def copy(cls, purchases, default=None):
Date = Pool().get('ir.date')
if default is None:
default = {}
else:
@@ -742,7 +750,7 @@ class Purchase(
default.setdefault('invoice_state', 'none')
default.setdefault('invoices_ignored', None)
default.setdefault('shipment_state', 'none')
default.setdefault('purchase_date', None)
default.setdefault('purchase_date', Date.today())
default.setdefault('quoted_by')
default.setdefault('confirmed_by')
default.setdefault('untaxed_amount_cache')
@@ -1021,9 +1029,15 @@ class Purchase(
pool = Pool()
Invoice = pool.get('account.invoice')
Invoice.save(invoices.values())
Invoice.save(invoices.values())
for purchase, invoice in invoices.items():
#check if forex
forex_rate = invoice.get_forex()
if forex_rate:
invoice.selection_rate = 'forex'
invoice.rate = invoice.on_change_with_rate()
Invoice.save([invoice])
purchase.copy_resources_to(invoice)
if len(invoices)==1:
if prepayment:
@@ -1215,7 +1229,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
()),
If(Eval('type') != 'line',
('id', '=', None),
()),
())
],
states={
'invisible': Eval('type') != 'line',
@@ -1857,77 +1871,93 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
else:
lots_to_invoice = self.lots
for l in lots_to_invoice:
#if l.lot_type == 'physic':
invoice_line = InvoiceLine()
invoice_line.type = self.type
invoice_line.currency = self.currency
invoice_line.company = self.company
invoice_line.description = self.description
invoice_line.note = self.note
invoice_line.origin = self
qt, gross_qt = l.get_hist_quantity(0)
quantity = float(qt)
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
if self.unit:
quantity = self.unit.round(quantity)
invoice_line.unit_price = l.get_lot_price()
invoice_line.product = l.lot_product
invoice_line.quantity = quantity
if not invoice_line.quantity:
return []
invoice_line.unit = self.unit
invoice_line.taxes = self.taxes
if self.company.purchase_taxes_expense:
invoice_line.taxes_deductible_rate = 0
elif self.product:
invoice_line.taxes_deductible_rate = (
self.product.supplier_taxes_deductible_rate_used)
invoice_line.invoice_type = 'in'
if self.product:
invoice_line.account = self.product.account_stock_in_used
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_product_missing_account_expense',
purchase=self.purchase.rec_name,
product=self.product.rec_name))
else:
invoice_line.account = account_config.get_multivalue(
'default_category_account_expense', company=self.company.id)
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_missing_account_expense',
purchase=self.purchase.rec_name))
if action == 'prov':
invoice_line.description = 'Pro forma'
elif action == 'final':
invoice_line.description = 'Final'
elif action == 'service':
invoice_line.description = 'Service'
#invoice_line.stock_moves = self._get_invoice_line_moves()
#invoice_line.stock_moves = [l.get_current_supplier_move()]
invoice_line.lot = l.id
if self.product.type == 'service':
invoice_line.unit_price = self.unit_price
invoice_line.product = self.product
invoice_line.stock_moves = []
Fee = Pool().get('fee.fee')
fee = Fee.search(['purchase','=',self.purchase.id])
if fee:
invoice_line.fee = fee[0]
lines.append(invoice_line)
logger.info("GETINVLINE:%s",self.product.type)
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
if l.invoice_line_prov and self.product.type != 'service':
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
'invoice': None,
'quantity': -l.invoice_line_prov.quantity,
'unit_price': l.invoice_line_prov.unit_price,
'party': l.invoice_line_prov.invoice.party,
'origin': str(self),
})
lines.append(invoice_line_)
if l.lot_type == 'physic':
invoice_line = InvoiceLine()
invoice_line.type = self.type
invoice_line.currency = self.currency
invoice_line.company = self.company
invoice_line.description = self.description
invoice_line.note = self.note
invoice_line.origin = self
qt, gross_qt = l.get_hist_quantity(0)
quantity = float(qt)
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
if self.unit:
quantity = self.unit.round(quantity)
invoice_line.unit_price = l.get_lot_price()
invoice_line.product = l.lot_product
invoice_line.quantity = quantity
logger.info("GETINVOICELINE_QT:%s",quantity)
if not invoice_line.quantity:
return []
invoice_line.unit = self.unit
invoice_line.taxes = self.taxes
if self.company.purchase_taxes_expense:
invoice_line.taxes_deductible_rate = 0
elif self.product:
invoice_line.taxes_deductible_rate = (
self.product.supplier_taxes_deductible_rate_used)
invoice_line.invoice_type = 'in'
if self.product:
if self.product.type == 'service' and not self.product.landed_cost:
invoice_line.account = self.product.account_stock_in_used
else:
invoice_line.account = self.product.account_stock_in_used
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_product_missing_account_expense',
purchase=self.purchase.rec_name,
product=self.product.rec_name))
else:
invoice_line.account = account_config.get_multivalue(
'default_category_account_expense', company=self.company.id)
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_missing_account_expense',
purchase=self.purchase.rec_name))
if action == 'prov':
invoice_line.description = 'Pro forma'
elif action == 'final':
invoice_line.description = 'Final'
elif action == 'service':
invoice_line.description = 'Service'
#invoice_line.stock_moves = self._get_invoice_line_moves()
#invoice_line.stock_moves = [l.get_current_supplier_move()]
invoice_line.lot = l.id
if self.product.type == 'service':
invoice_line.unit_price = self.unit_price
invoice_line.product = self.product
invoice_line.stock_moves = []
Fee = Pool().get('fee.fee')
fee = Fee.search(['purchase','=',self.purchase.id])
if fee:
invoice_line.fee = fee[0]
if fee[0].mode == 'lumpsum':
invoice_line.quantity = 1
elif fee[0].mode == 'ppack':
invoice_line.quantity = fee[0].quantity
else:
state_id = 0
LotQtType = Pool().get('lot.qt.type')
lqt = LotQtType.search([('name','=','BL')])
if lqt:
state_id = lqt[0].id
invoice_line.quantity = fee[0].get_fee_lots_qt(state_id)
lines.append(invoice_line)
logger.info("GETINVLINE:%s",self.product.type)
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
if l.invoice_line_prov and self.product.type != 'service':
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
'invoice': None,
'quantity': -l.invoice_line_prov.quantity,
'unit_price': l.invoice_line_prov.unit_price,
'party': l.invoice_line_prov.invoice.party,
'origin': str(self),
})
lines.append(invoice_line_)
return lines
def _get_invoice_line_quantity(self):
@@ -2046,11 +2076,13 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
to_location = self.purchase.to_location
move.from_location = from_location
if to_location.type != 'customer':
move.to_location = Location.get_transit_id()
else:
move.to_location = to_location
logger.info("FROM_LOCATION:%s",self.purchase.from_location)
logger.info("TO_LOCATION:%s",self.purchase.to_location)
if to_location:
if to_location.type != 'customer':
move.to_location = Location.get_transit_id()
else:
move.to_location = to_location
unit_price = l.get_lot_price()
# if l.invoice_line_prov != None :

View File

@@ -17,9 +17,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="payment_term"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<label name="certif"/>
<field name="certif"/>
</group>
<group col="2" colspan="2" id="hd" yfill="1">
<field name="viewer" widget="html_viewer" height="300" width="600"/>

View File

@@ -16,13 +16,21 @@ this repository contains the full copyright notices and license terms. -->
<label name="product_supplier"/>
<field name="product_supplier"/>
<newline/>
<label name="attributes_name"/>
<field name="attributes_name"/>
<label name="concentration"/>
<field name="concentration"/>
<newline/>
<label name="del_period"/>
<field name="del_period"/>
<label name="period_at"/>
<field name="period_at"/>
<newline/>
<label id="delivery_date" string="Delivery Date:"/>
<group id="delivery_date" col="-1">
<field name="delivery_date" xexpand="0"/>
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
</group>
<label name="del_period"/>
<field name="del_period"/>
<newline/>
<label name="from_del"/>
<field name="from_del"/>
@@ -40,6 +48,10 @@ this repository contains the full copyright notices and license terms. -->
<separator name="description" colspan="4"/>
<field name="description" colspan="4"/>
</page>
<page string="Attributes" id="att">
<label name="attributes"/>
<field name="attributes"/>
</page>
<page string="Taxes" id="taxes">
<field name="taxes" colspan="4"/>
</page>

View File

@@ -28,7 +28,12 @@ from . import (
open_position,
credit_risk,
valuation,
)
dimension,
weight_report,
backtoback,
service,
invoice,
)
def register():
Pool.register(
@@ -70,9 +75,11 @@ def register():
dashboard.Incoming,
dashboard.BotAction,
dashboard.News,
dashboard.Demos,
party.Party,
dashboard.Demos,
party.Party,
party.PartyExecution,
party.PartyExecutionSla,
party.PartyExecutionPlace,
payment_term.PaymentTerm,
payment_term.PaymentTermLine,
purchase.Purchase,
@@ -111,10 +118,12 @@ def register():
forex.PForex,
forex.ForexBI,
purchase.PnlBI,
purchase.PositionBI,
stock.Move,
stock.Location,
stock.InvoiceLine,
stock.ShipmentIn,
stock.ShipmentWR,
stock.ShipmentInternal,
stock.ShipmentOut,
stock.StatementOfFacts,
@@ -158,14 +167,38 @@ def register():
purchase.ContractDocumentType,
purchase.DocTemplate,
purchase.DocTypeTemplate,
purchase.Mtm,
module='purchase', type_='model')
purchase.PurchaseStrategy,
purchase.PriceComposition,
purchase.QualityAnalysis,
purchase.Assay,
purchase.AssayLine,
purchase.AssayElement,
purchase.AssayUnit,
purchase.PayableRule,
purchase.PenaltyRule,
purchase.PenaltyRuleTier,
purchase.ConcentrateTerm,
backtoback.Backtoback,
dimension.AnalyticDimension,
dimension.AnalyticDimensionValue,
dimension.AnalyticDimensionAssignment,
weight_report.WeightReport,
module='purchase', type_='model')
Pool.register(
invoice.Invoice,
invoice.InvoiceLine,
module='account_invoice', type_='model')
Pool.register(
forex.Forex,
forex.ForexCoverFees,
forex.ForexCategory,
pricing.Component,
pricing.Mtm,
pricing.MtmStrategy,
pricing.MtmScenario,
pricing.MtmSnapshot,
pricing.PriceMatrix,
pricing.PriceMatrixLine,
pricing.Estimated,
pricing.Pricing,
pricing.Period,
@@ -182,6 +215,7 @@ def register():
sale.Derivative,
sale.Valuation,
sale.ValuationLine,
sale.ValuationDyn,
sale.ValuationReport,
sale.Fee,
sale.Lot,
@@ -193,8 +227,11 @@ def register():
forex.SForex,
forex.ForexCoverPhysicalSale,
sale.ContractDocumentType,
sale.Mtm,
sale.SaleStrategy,
sale.OpenPosition,
sale.Backtoback,
sale.AnalyticDimensionAssignment,
sale.PriceComposition,
module='sale', type_='model')
Pool.register(
lot.LotShipping,
@@ -220,6 +257,7 @@ def register():
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
purchase.PositionReport,
derivative.DerivativeMatchWizard,
module='purchase', type_='wizard')
Pool.register(

View File

@@ -0,0 +1,22 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
import datetime
import logging
class Backtoback(ModelSQL, ModelView):
'Back To Back'
__name__ = 'back.to.back'
reference = fields.Char("Reference")
purchase = fields.One2Many('purchase.purchase','btb', "Purchase")

View File

@@ -0,0 +1,65 @@
<tryton>
<data>
<record model="ir.ui.icon" id="btb_icon">
<field name="name">tradon-btb</field>
<field name="path">icons/tradon-btb.svg</field>
</record>
<record model="ir.ui.view" id="btb_view_form">
<field name="model">back.to.back</field>
<field name="type">form</field>
<field name="name">btb_form</field>
</record>
<record model="ir.ui.view" id="purchase_btb_view_form">
<field name="model">purchase.purchase</field>
<field name="type">form</field>
<field name="name">purchase_btb_form</field>
</record>
<record model="ir.ui.view" id="purchase_line_btb_view_form">
<field name="model">purchase.line</field>
<field name="type">form</field>
<field name="name">purchase_line_btb_form</field>
</record>
<record model="ir.ui.view" id="sale_line_btb_view_form">
<field name="model">sale.line</field>
<field name="type">form</field>
<field name="name">sale_line_btb_form</field>
</record>
<record model="ir.ui.view" id="btb_view_list">
<field name="model">back.to.back</field>
<field name="type">tree</field>
<field name="name">btb_tree</field>
</record>
<record model="ir.action.act_window" id="act_btb_form">
<field name="name">Back to back</field>
<field name="res_model">back.to.back</field>
</record>
<record model="ir.action.act_window.view" id="act_btb_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="btb_view_list"/>
<field name="act_window" ref="act_btb_form"/>
</record>
<record model="ir.action.act_window.view" id="act_btb_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="btb_view_form"/>
<field name="act_window" ref="act_btb_form"/>
</record>
<menuitem
name="Back to back"
sequence="99"
id="menu_btb_main"
icon="tradon-btb" />
<menuitem
name="Back to back"
action="act_btb_form"
parent="menu_btb_main"
sequence="10"
id="menu_btb" />
</data>
</tryton>

View File

@@ -235,15 +235,66 @@ class Dashboard(ModelSQL, ModelView):
return round(1/f1,6), round(1/f2,6) if f2 else None, round(1/f3,6) if f3 else None, round(1/f4,6) if f4 else None, round(1/f5,6) if f5 else None, d1, d2, d3, d4, d5
def get_tremor(self,name):
Date = Pool().get('ir.date')
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
Shipment = Pool().get('stock.shipment.in')
DocumentIncoming = Pool().get('document.incoming')
Fee = Pool().get('fee.fee')
WR = Pool().get('weight.report')
if config.automation:
shipment = Shipment.search([('state','!=','received')])
shipment_trend = [sh for sh in shipment if sh.create_date == Date.today()]
controller = Shipment.search([('controller','!=',None)])
controller_trend = [co for co in controller if co.create_date == Date.today()]
instruction = Shipment.search([('result','!=',None)])
instruction_trend = [si for si in instruction if si.create_date == Date.today()]
id_received = Shipment.search([('returned_id','!=',None)])
id_received_trend = [i for i in id_received if i.create_date == Date.today()]
wr = WR.search([('id','>',0)])
wr_trend = [w for w in wr if w.create_date == Date.today()]
so = Fee.search(['id','=',25])
so_trend = [s for s in so if s.create_date == Date.today()]
di = DocumentIncoming.search(['id','>',0])
di_trend = [d for d in di if d.create_date == Date.today()]
return (
config.dashboard +
"/dashboard/index.html?shipment="
+ str(len(shipment))
+ "&shipment_trend="
+ str(len(shipment_trend))
+ "&controller="
+ str(len(controller))
+ "&controller_trend="
+ str(len(controller_trend))
+ "&instruction="
+ str(len(instruction))
+ "&instruction_trend="
+ str(len(instruction_trend))
+ "&wr="
+ str(len(wr))
+ "&wr_trend="
+ str(len(wr_trend))
+ "&so="
+ str(len(so))
+ "&so_trend="
+ str(len(so_trend))
+ "&di="
+ str(len(di))
+ "&di_trend="
+ str(len(di_trend))
+ "&id_received="
+ str(len(id_received))
+ "&id_received_trend="
+ str(len(id_received_trend)))
f1,f2,f3,f4,f5,d1,d2,d3,d4,d5 = self.get_last_five_fx_rates()
Valuation = Pool().get('valuation.valuation')
total_t, total_t1, variation = Valuation.get_totals()
pnl_amount = "{:,.0f}".format(round(total_t,0))
last_total,last_variation = Valuation.get_totals()
pnl_amount = "{:,.0f}".format(round(last_total,0))
pnl_variation = 0
if total_t1:
pnl_variation = "{:,.2f}".format(round((total_t/total_t1 - 1)*100,0))
if last_total and last_variation:
pnl_variation = "{:,.2f}".format(round((last_variation/last_total)*100,0))
Open = Pool().get('open.position')
opens = Open.search(['id','>',0])
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
@@ -271,7 +322,7 @@ class Dashboard(ModelSQL, ModelView):
val_s = len(val)
conf = Sale.search(['state','=','confirmed'])
conf_s = len(conf)
Shipment = Pool().get('stock.shipment.in')
draft = Shipment.search(['state','=','draft'])
shipment_d = len(draft)
val = Shipment.search(['state','=','started'])
@@ -464,7 +515,7 @@ class Dashboard(ModelSQL, ModelView):
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
]
demos = Demos.search([('active', '=', True)])
demos = Demos.search([('active', '=', True)],order=[('id', 'DESC')])
for n in demos:
icon = n.icon or "📰"
category = n.category or "General"
@@ -717,6 +768,7 @@ class BotWizard(Wizard):
l.lot_quantity = l.lot_qt
l.lot_gross_quantity = l.lot_qt
l.lot_premium = Decimal(0)
l.lot_chunk_key = None
lot_id = LotQt.add_physical_lots(lqt,[l])
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
Dashboard.save([d])

View File

@@ -0,0 +1,79 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
import datetime
import logging
class AnalyticDimension(ModelSQL, ModelView):
'Analytic Dimension'
__name__ = 'analytic.dimension'
name = fields.Char('Name', required=True)
code = fields.Char('Code', required=True)
active = fields.Boolean('Active')
class AnalyticDimensionValue(ModelSQL, ModelView):
'Analytic Dimension Value'
__name__ = 'analytic.dimension.value'
dimension = fields.Many2One(
'analytic.dimension',
'Dimension',
required=True,
ondelete='CASCADE'
)
name = fields.Char('Name', required=True)
code = fields.Char('Code')
parent = fields.Many2One(
'analytic.dimension.value',
'Parent',
domain=[
('dimension', '=', Eval('dimension')),
],
depends=['dimension']
)
children = fields.One2Many(
'analytic.dimension.value',
'parent',
'Children'
)
active = fields.Boolean('Active')
class AnalyticDimensionAssignment(ModelSQL, ModelView):
'Analytic Dimension Assignment'
__name__ = 'analytic.dimension.assignment'
dimension = fields.Many2One(
'analytic.dimension',
'Dimension',
required=True
)
value = fields.Many2One(
'analytic.dimension.value',
'Value',
required=True,
domain=[
('dimension', '=', Eval('dimension')),
],
depends=['dimension']
)
purchase = fields.Many2One(
'purchase.purchase',
'Purchase',
ondelete='CASCADE'
)

View File

@@ -0,0 +1,34 @@
<tryton>
<data>
<record model="ir.ui.view" id="dimension_view_form">
<field name="model">analytic.dimension</field>
<field name="type">form</field>
<field name="name">dimension_form</field>
</record>
<record model="ir.ui.view" id="dimension_view_list">
<field name="model">analytic.dimension</field>
<field name="type">tree</field>
<field name="name">dimension_tree</field>
</record>
<record model="ir.ui.view" id="dimension_value_view_form">
<field name="model">analytic.dimension.value</field>
<field name="type">form</field>
<field name="name">dimension_value_form</field>
</record>
<record model="ir.ui.view" id="dimension_value_view_list">
<field name="model">analytic.dimension.value</field>
<field name="type">tree</field>
<field name="name">dimension_value_tree</field>
</record>
<record model="ir.ui.view" id="dimension_ass_view_form">
<field name="model">analytic.dimension.assignment</field>
<field name="type">form</field>
<field name="name">dimension_ass_form</field>
</record>
<record model="ir.ui.view" id="dimension_ass_view_list">
<field name="model">analytic.dimension.assignment</field>
<field name="type">tree</field>
<field name="name">dimension_ass_tree</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,157 @@
# Business Rules - Purchase Trade
Statut: `draft`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
Owner metier: `a completer`
Owner technique: `a completer`
## 1) Scope
- Domaine: `purchase_trade`
- Hors scope:
- Modules impactes:
- `purchase_trade`
- `lot`
## 2) Glossaire
- `Purchase Line`: ligne d'achat.
- `quantity_theorical`: quantite theorique contractuelle de la ligne.
- `Virtual Lot`: lot unique de type `virtual` rattache a une `purchase.line`.
- `lot.qt`: table des quantites ouvertes, matchées ou shippées par lot.
- `lot.qt ouvert`: enregistrement `lot.qt` avec `lot_p = virtual lot`, `lot_s = None` et sans shipment.
## 3) Regles metier
### BR-PT-001 - Ajustement de la quantite theorique apres creation du contrat
- Intent: conserver la coherence entre la quantite theorique de la ligne d'achat, le lot virtuel associe et les quantites ouvertes stockees dans `lot.qt`.
- Description:
- Quand `purchase.line.quantity_theorical` est modifiee apres creation du contrat, le systeme doit recalculer le delta entre l'ancienne et la nouvelle valeur.
- La regle s'applique au lot unique de type `virtual` rattache a la `purchase.line`.
- Conditions d'entree:
- Une `purchase.line` existe deja.
- Son champ `quantity_theorical` est modifie via `write`.
- Un lot `virtual` est rattache a la ligne.
- Resultat attendu:
- Si `delta > 0`:
- augmenter la quantite courante du lot `virtual` via `set_current_quantity` pour conserver l'historique `lot.qt.hist`
- augmenter le `lot.qt` ouvert existant
- si aucun `lot.qt` ouvert n'existe, en creer un nouveau avec le delta
- Si `delta < 0`:
- diminuer le `lot.qt` ouvert uniquement si la quantite ouverte disponible est suffisante
- diminuer la quantite courante du lot `virtual` du meme delta
- si aucun `lot.qt` ouvert n'existe ou si sa quantite est insuffisante, bloquer avec l'erreur `Please unlink or unmatch lot`
- Definition du `lot.qt` ouvert:
- `lot_p = virtual lot`
- `lot_s = None`
- `lot_shipment_in = None`
- `lot_shipment_internal = None`
- `lot_shipment_out = None`
- Exceptions:
- si aucun lot `virtual` n'est trouve sur la ligne, la regle ne fait rien
- Priorite:
- `bloquante`
- Source:
- `Decision metier documentee dans les commentaires de purchase_trade.purchase.Line.write`
### BR-PT-002 - Le lot physique est le pont metier entre purchase, sale et shipment
- Intent: disposer d'un chemin unique et stable pour retrouver les informations logistiques et de facturation reliees a un contrat d'achat ou de vente.
- Description:
- Le lot physique (`lot_type = physic`) porte simultanement le lien vers:
- la `purchase.line` via `lot.line`
- la `sale.line` via `lot.sale_line`
- le shipment via `lot.lot_shipment_in` / `lot.lot_shipment_internal` / `lot.lot_shipment_out`
- Pour toute logique qui doit naviguer entre achat, vente, shipment et facture, il faut privilegier ce lot physique comme source de verite.
- Resultat attendu:
- depuis une facture d'achat:
- remonter a la `purchase.line`
- puis au lot physique de la ligne
- puis au shipment et aux donnees logistiques associees
- depuis une facture de vente:
- remonter a la `sale.line`
- puis au lot physique matchant qui porte aussi la `purchase.line`
- puis au shipment et aux donnees logistiques associees
- Cas d'usage typiques:
- recuperer `bl_date`, `bl_number`, `controller`, `from_location`, `to_location`
- retrouver une facture provisoire liee au lot
- retrouver des fees rattaches au shipment
- Priorite:
- `structurante`
### BR-PT-003 - Le freight amount des templates facture vient du fee de shipment
- Intent: afficher dans les documents facture la vraie valeur de fret maritime rattachee au shipment du lot physique.
- Description:
- Le `FREIGHT VALUE` d'une facture ne doit pas etre pris sur la facture elle-meme.
- Il doit etre calcule a partir du `fee.fee` rattache au shipment (`shipment_in`) du lot physique relie a la facture.
- Regle de navigation:
- retrouver le lot physique pertinent depuis la facture
- retrouver son shipment
- chercher le `fee.fee` avec:
- `shipment_in = shipment.id`
- `product.name = 'Maritime freight'`
- utiliser `fee.get_amount()` comme montant de fret
- Portee:
- s'applique aussi bien aux factures d'achat qu'aux factures de vente
- cote vente, la remontee doit passer par le lot physique qui fait le lien entre `purchase.line` et `sale.line`
- Priorite:
- `importante`
## 4) Exemples concrets
### Exemple E1 - Augmentation simple
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 120`
- `lot.qt ouvert = 40`
- Attendu:
- lot `virtual` augmente de `20`
- `lot.qt ouvert` passe de `40` a `60`
### Exemple E2 - Augmentation sans lot.qt ouvert
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 110`
- aucun `lot.qt` ouvert
- Attendu:
- lot `virtual` augmente de `10`
- creation d'un `lot.qt` ouvert a `10`
### Exemple E3 - Diminution possible
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 90`
- `lot.qt ouvert = 25`
- Attendu:
- lot `virtual` diminue de `10`
- `lot.qt ouvert` passe de `25` a `15`
### Exemple E4 - Diminution impossible
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 80`
- `lot.qt ouvert = 5`
- Attendu:
- blocage avec `Please unlink or unmatch lot`
## 5) Impact code attendu
- Fichiers Python concernes:
- `modules/purchase_trade/purchase.py`
- `modules/purchase_trade/lot.py`
## 6) Strategie de tests
Pour cette regle, couvrir au minimum:
- augmentation avec `lot.qt` ouvert existant
- augmentation sans `lot.qt` ouvert
- diminution possible
- diminution impossible avec erreur

View File

@@ -0,0 +1,149 @@
# Template Rules - Purchase Trade
Statut: `draft`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
## 1) Scope
- Domaine: `templates Relatorio .fodt`
- Modules concernes:
- `purchase_trade`
- `sale`
- `account_invoice`
## 2) Objectif
- Eviter les erreurs de parsing Relatorio/Genshi lors de la generation des documents.
- Standardiser la maniere d'alimenter les templates metier a partir du code Python.
## 3) Regles pratiques
### TR-001 - Toujours partir du template standard voisin
- Avant de modifier un template metier (`invoice_ict.fodt`, `sale_ict.fodt`, etc.), comparer avec le template standard du module source:
- `modules/account_invoice/invoice.fodt`
- `modules/sale/sale.fodt`
- Reprendre en priorite la syntaxe Relatorio deja validee dans ces templates.
### TR-002 - Eviter les expressions Genshi trop complexes dans le `.fodt`
- Preferer des proprietes Python simples exposees par le modele.
- Le template doit consommer au maximum des champs ou proprietes du type:
- `record.report_address`
- `record.report_price`
- `record.report_payment_date`
- Si un template a besoin de donnees issues d'un autre modele lie, creer un petit pont Python.
### TR-003 - Regles de syntaxe XML/Relatorio dans les placeholders
- Dans un `text:placeholder`, utiliser:
- `&quot;...&quot;` pour les guillemets doubles
- `&apos;...&apos;` pour les apostrophes
- Eviter les formes avec antislashs:
- interdit: `\'\'`
- interdit: `\'value\'`
- Exemples corrects:
- `&lt;replace text:p=&quot;set_lang(invoice.party.lang)&quot;&gt;`
- `&lt;if test=&quot;invoice.report_payment_description&quot;&gt;`
- `&lt;tax.description or &apos;&apos;&gt;`
### TR-004 - Pour une facture issue d'une vente, preferer un pont `account.invoice -> sale`
- Si le template facture doit reutiliser la logique de la pro forma vente, ne pas dupliquer les calculs directement dans le `.fodt`.
- Ajouter plutot dans `purchase_trade` une extension `account.invoice` avec des proprietes `report_*` qui relaient vers `invoice.sales[0]`.
- Exemple de proprietes utiles:
- `report_address`
- `report_contract_number`
- `report_shipment`
- `report_product_description`
- `report_crop_name`
- `report_attributes_name`
- `report_price`
- `report_payment_date`
- `report_nb_bale`
- `report_gross`
- `report_net`
- `report_lbs`
### TR-005 - Reutiliser les proprietes existantes du module `purchase_trade.sale`
- Avant d'ajouter une nouvelle logique pour un template vente ou facture issue d'une vente, verifier si une propriete existe deja sur `sale.sale`.
- Proprietes deja utiles:
- `report_terms`
- `report_gross`
- `report_net`
- `report_qt`
- `report_nb_bale`
- `report_deal`
- `report_packing`
- `report_price`
- `report_delivery`
- `report_payment_date`
- `report_shipment`
### TR-006 - Penser au cache des reports facture avant d'accuser le `.fodt`
- Les actions de report `account.invoice` peuvent partager le meme moteur de rendu.
- Dans `modules/account_invoice/invoice.py`, le champ `invoice_report_cache` peut reutiliser un document deja genere.
- Symptome typique:
- plusieurs actions differentes (`Provisional Invoice`, `Final Invoice`, `Prepayment`, etc.) semblent ouvrir le meme template ou le meme rendu
- Reflexe a avoir:
- verifier si le probleme vient du cache avant de modifier le `.fodt`
- pour un report alternatif, ne pas reutiliser le cache du report standard `account_invoice/invoice.fodt`
- si besoin, bypasser la lecture/ecriture du cache pour les templates alternatifs
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation
- Pour remonter d'une facture vers des donnees logistiques ou metier, ne pas dupliquer de chemins differents selon achat/vente.
- Regle pratique:
- partir de la ligne metier (`purchase.line` ou `sale.line`)
- retrouver le lot physique associe
- utiliser ce lot comme pont vers le shipment et les autres objets lies
- Ce chemin doit etre privilegie pour exposer des proprietes `report_*` comme:
- `report_bl_date`
- `report_loading_port`
- `report_discharge_port`
- `report_controller_name`
- `report_si_number`
- `report_proforma_invoice_number`
- `report_proforma_invoice_date`
### TR-008 - Le freight amount d'un template facture vient du fee de shipment
- Ne pas lire le fret directement sur `account.invoice`.
- Pour les templates `invoice_ict*`, le `FREIGHT VALUE` doit etre expose par une propriete Python du type `invoice.report_freight_amount`.
- La logique attendue est:
- retrouver le lot physique pertinent
- retrouver son shipment
- chercher le `fee.fee` du shipment avec `product.name = 'Maritime freight'`
- utiliser `fee.get_amount()`
- Si le fee a sa propre devise, preferer aussi exposer le symbole de devise depuis le fee plutot que depuis la facture.
## 4) Workflow recommande pour corriger un template en erreur
1. Identifier le placeholder exact qui provoque l'erreur Relatorio.
2. Comparer sa syntaxe avec le template standard equivalent.
3. Remplacer les guillemets/quotes non valides par `&quot;` / `&apos;`.
4. Si l'expression devient trop longue, la deplacer dans une propriete Python `report_*`.
5. Ne modifier que les placeholders necessaires.
6. Regenerer le document pour verifier la prochaine erreur eventuelle.
7. Si plusieurs actions affichent le meme rendu, verifier ensuite le cache `invoice_report_cache`.
## 5) Cas documentes dans ce repo
### Invoice ICT
- Fichier: `modules/account_invoice/invoice_ict.fodt`
- Strategie retenue:
- aligner la syntaxe sur `modules/account_invoice/invoice.fodt`
- reutiliser au maximum les proprietes metier deja exposees
- exposer dans `modules/purchase_trade/invoice.py` des proprietes de pont `account.invoice -> sale/purchase`
- pour les donnees shipment et freight, passer par le lot physique comme pont achat/vente
### Sale ICT
- Fichier: `modules/sale/sale_ict.fodt`
- Usage:
- reference principale pour les champs metier proches d'une pro forma / facture commerciale
- source de verite pratique pour les placeholders `report_*` issus de `purchase_trade.sale`

View File

@@ -6,7 +6,7 @@ from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
@@ -18,6 +18,8 @@ import datetime
import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.account.exceptions import PeriodNotFoundError
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
logger = logging.getLogger(__name__)
@@ -55,16 +57,28 @@ class Fee(ModelSQL,ModelView):
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('rate', '% rate'),
('pcost', '% cost price'),
('ppack', 'Per packing'),
], 'Mode', required=True)
inherit_qt = fields.Boolean("Inh Qt")
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
unit = fields.Many2One('product.uom',"Unit",domain=[
If(Eval('mode') == 'ppack',
('category', '=', Eval('packing_category')),
()),
],
states={
'readonly': (Bool(Eval('mode') != 'ppack') & Bool(Eval('mode') != 'perqt')),
},
depends=['mode', 'packing_category'])
packing_category = fields.Function(fields.Many2One('product.uom.category',"Packing Category"),'on_change_with_packing_category')
inherit_shipment = fields.Boolean("Inh Sh",states={
'invisible': (Eval('shipment_in')),
})
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
qt_state = fields.Many2One('lot.qt.type',"Qt State")
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
@@ -80,9 +94,94 @@ class Fee(ModelSQL,ModelView):
weight_type = fields.Selection([
('net', 'Net'),
('brut', 'Brut'),
('brut', 'Gross'),
], string='W. type')
fee_date = fields.Date("Date")
@classmethod
def default_fee_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_qt_state(cls):
LotQtType = Pool().get('lot.qt.type')
lqt = LotQtType.search([('name','=','BL')])
if lqt:
return lqt[0].id
@fields.depends('mode','unit')
def on_change_with_packing_category(self, name=None):
UnitCategory = Pool().get('product.uom.category')
packing = UnitCategory.search(['name','=','Packing'])
if packing:
return packing[0]
@fields.depends('line','sale_line','shipment_in','lots','price','unit','auto_calculation','mode','_parent_line.unit','_parent_line.lots','_parent_sale_line.unit','_parent_sale_line.lots','_parent_shipment_in.id')
def on_change_with_quantity(self, name=None):
qt = None
unit = None
line = self.line
logger.info("ON_CHANGE_WITH_LINE:%s",line)
if not line:
line = self.sale_line
if line:
if line.lots:
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots])
qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots])
unit = line.lots[0].lot_unit
logger.info("ON_CHANGE_WITH_QT0:%s",qt)
logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in)
if self.shipment_in:
Lot = Pool().get('lot.lot')
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
logger.info("ON_CHANGE_WITH_LOTS:%s",lots)
if lots:
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots])
qt_ = sum([e.get_current_quantity_converted(0) for e in lots])
unit = lots[0].lot_unit
if not qt:
logger.info("ON_CHANGE_WITH_QT1:%s",qt)
LotQt = Pool().get('lot.qt')
if self.shipment_in:
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
qt = Decimal(lqts[0].lot_quantity)
qt_ = qt
unit = lqts[0].lot_unit
logger.info("ON_CHANGE_WITH_QT2:%s",qt)
if self.mode != 'ppack':
return qt
else:
if self.auto_calculation:
logger.info("AUTOCALCULATION:%s",qt)
logger.info("AUTOCALCULATION2:%s",qt_)
logger.info("AUTOCALCULATION3:%s",Decimal(unit.factor))
logger.info("AUTOCALCULATION4:%s",Decimal(self.unit.factor))
return (qt_ * Decimal(unit.factor) / Decimal(self.unit.factor)).to_integral_value(rounding=ROUND_UP)
@fields.depends('price','mode','_parent_line.lots','_parent_sale_line.lots','shipment_in')
def on_change_with_unit(self, name=None):
if self.mode != 'ppack' and self.mode != 'perqt':
line = self.line
if not line:
line = self.sale_line
if line:
if line.lots:
if len(line.lots) == 1:
return line.lots[0].lot_unit_line
else:
return line.lots[1].lot_unit_line
if self.shipment_in:
Lot = Pool().get('lot.lot')
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
logger.info("ON_CHANGE_WITH_UNIT:%s",lots)
if lots:
return lots[0].lot_unit_line
else:
return self.unit
def get_lots(self, name):
logger.info("GET_LOTS_LINE:%s",self.line)
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
@@ -119,6 +218,23 @@ class Fee(ModelSQL,ModelView):
if ml:
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
def get_non_cog(self,lot):
MoveLine = Pool().get('account.move.line')
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
Uom = Pool().get('product.uom')
ml = MoveLine.search([
('lot', '=', lot.id),
('fee', '=', self.id),
('account', '=', self.product.account_stock_in_used.id),
])
logger.info("GET_NON_COG_FEE:%s",ml)
if ml:
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
@classmethod
def __setup__(cls):
super().__setup__()
@@ -137,13 +253,14 @@ class Fee(ModelSQL,ModelView):
def default_p_r(cls):
return 'pay'
def get_unit(self, name):
Lot = Pool().get('lot.lot')
if self.lots:
if self.lots[0].line:
return self.lots[0].line.unit
if self.lots[0].sale_line:
return self.lots[0].sale_line.unit
def get_unit(self, name=None):
FeeLots = Pool().get('fee.lots')
fl = FeeLots.search(['fee','=',self.id])
if fl:
if fl[0].lot.line:
return fl[0].lot.line.unit
if fl[0].lot.sale_line:
return fl[0].lot.sale_line.unit
@classmethod
@ModelView.button
@@ -174,7 +291,11 @@ class Fee(ModelSQL,ModelView):
return round(self.price / self.quantity,4)
elif self.mode == 'perqt':
return self.price
elif self.mode == 'pprice':
elif self.mode == 'ppack':
unit = self.get_unit()
if unit and self.unit:
return round(self.price / Decimal(self.unit.factor) * Decimal(unit.factor),4)
elif self.mode == 'pprice' or self.mode == 'pcost':
if self.line and self.price:
return round(self.price * Decimal(self.line.unit_price) / 100,4)
if self.sale_line and self.price:
@@ -194,8 +315,8 @@ class Fee(ModelSQL,ModelView):
def get_landed_status(self,name):
if self.product:
return self.product.landed_cost
return self.product.template.landed_cost
def get_quantity(self,name=None):
qt = self.get_fee_lots_qt()
if qt:
@@ -206,6 +327,7 @@ class Fee(ModelSQL,ModelView):
return Decimal(lqts[0].lot_quantity)
def get_amount(self,name=None):
Date = Pool().get('ir.date')
sign = Decimal(1)
if self.price:
# if self.p_r:
@@ -213,13 +335,54 @@ class Fee(ModelSQL,ModelView):
# sign = -1
if self.mode == 'lumpsum':
return self.price * sign
elif self.mode == 'ppack':
return round(self.price * self.quantity,2)
elif self.mode == 'rate':
#take period with estimated trigger date
if self.line:
if self.line.estimated_date:
beg_date = self.fee_date if self.fee_date else Date.today()
est_lines = [dd for dd in self.line.estimated_date if dd.trigger == 'bldate']
est_line = est_lines[0] if est_lines else None
if est_line and est_line.estimated_date:
est_date = est_line.estimated_date + datetime.timedelta(
days=est_line.fin_int_delta or 0
)
if est_date and beg_date:
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price/100,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
return round(factor * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
if self.sale_line:
if self.sale_line.sale.payment_term:
beg_date = self.fee_date if self.fee_date else Date.today()
est_date = self.sale_line.sale.payment_term.lines[0].get_date(beg_date,self.sale_line)
logger.info("EST_DATE:%s",est_date)
if est_date and beg_date:
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price/100,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
logger.info("FACTOR:%s",factor)
return round(factor * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
elif self.mode == 'perqt':
if self.shipment_in:
StockMove = Pool().get('stock.move')
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
if sm:
unique_lots = {e.lot for e in sm if e.lot}
return round(self.price * Decimal(sum([e.get_current_quantity_converted() for e in unique_lots])) * sign,2)
return round(self.price * Decimal(sum([e.get_current_quantity_converted(0,self.unit) for e in unique_lots])) * sign,2)
LotQt = Pool().get('lot.qt')
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
@@ -262,12 +425,12 @@ class Fee(ModelSQL,ModelView):
return super().copy(fees, default=default)
def get_fee_lots_qt(self):
def get_fee_lots_qt(self,state_id=0):
qt = Decimal(0)
FeeLots = Pool().get('fee.lots')
fee_lots = FeeLots.search([('fee', '=', self.id)])
if fee_lots:
qt = sum([e.lot.get_current_quantity_converted() for e in fee_lots])
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
logger.info("GET_FEE_LOTS_QT:%s",qt)
return qt
@@ -277,10 +440,18 @@ class Fee(ModelSQL,ModelView):
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
if self.price != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.price
if self.quantity != self.purchase.lines[0].quantity:
self.purchase.lines[0].quantity = self.quantity
if self.mode == 'lumpsum':
if self.amount != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.amount
elif self.mode == 'ppack':
if self.amount != self.purchase.lines[0].amount:
self.purchase.lines[0].unit_price = self.price
self.purchase.lines[0].quantity = self.quantity
else:
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.get_price_per_qt()
if self.quantity != self.purchase.lines[0].quantity:
self.purchase.lines[0].quantity = self.quantity
if self.product != self.purchase.lines[0].product:
self.purchase.lines[0].product = self.product
PurchaseLine.save([self.purchase.lines[0]])
@@ -297,46 +468,47 @@ class Fee(ModelSQL,ModelView):
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
records = super(Fee, cls).create(vlist)
fees = super(Fee, cls).create(vlist)
qt_sh = Decimal(0)
qt_line = Decimal(0)
unit = None
for record in records:
for fee in fees:
FeeLots = Pool().get('fee.lots')
Lots = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
if record.line:
for l in record.line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.line = l.line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.line.unit
if record.sale_line:
for l in record.sale_line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.sale_line = l.sale_line.id
FeeLots.save([fl])
if record.shipment_in:
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
if fee.line:
for l in fee.line.lots:
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
fl = FeeLots()
fl.fee = fee.id
fl.lot = l.id
fl.line = l.line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.line.unit
if fee.sale_line:
for l in fee.sale_line.lots:
if (l.lot_type == 'virtual' and len(fee.sale_line.lots)==1) or (l.lot_type == 'physic' and len(fee.sale_line.lots)>1):
fl = FeeLots()
fl.fee = fee.id
fl.lot = l.id
fl.sale_line = l.sale_line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.sale_line.unit
if fee.shipment_in:
if fee.shipment_in.state == 'draft'or fee.shipment_in.state == 'started':
lots = Lots.search(['lot_shipment_in','=',fee.shipment_in.id])
if lots:
for l in lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.fee = fee.id
fl.lot = l.id
FeeLots.save([fl])
qt_sh += l.get_current_quantity_converted()
unit = l.line.unit
else:
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
if lqts:
for l in lqts:
qt_sh += l.lot_p.get_current_quantity_converted()
@@ -344,46 +516,154 @@ class Fee(ModelSQL,ModelView):
else:
raise UserError("You cannot add fee on received shipment!")
type = record.type
type = fee.type
if type == 'ordered':
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine()
pl.product = record.product
if record.line:
pl.product = fee.product
if fee.line or fee.sale_line:
pl.quantity = round(qt_line,5)
if record.shipment_in:
if fee.shipment_in:
pl.quantity = round(qt_sh,5)
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
pl.unit = unit
pl.fee_ = record.id
if record.price:
pl.unit_price = round(Decimal(record.price),4)
pl.fee_ = fee.id
if fee.price:
fee_price = fee.get_price_per_qt()
logger.info("GET_FEE_PRICE_PER_QT:%s",fee_price)
pl.unit_price = round(Decimal(fee_price),4)
if fee.mode == 'lumpsum':
pl.quantity = 1
pl.unit_price = round(Decimal(fee.amount),4)
elif fee.mode == 'ppack':
pl.unit_price = fee.price
p = Purchase()
p.lines = [pl]
p.party = record.supplier
p.party = fee.supplier
if p.party.addresses:
p.invoice_address = p.party.addresses[0]
p.currency = record.currency
p.currency = fee.currency
p.line_type = 'service'
p.from_location = fee.shipment_in.from_location if fee.shipment_in else (fee.line.purchase.from_location if fee.line else fee.sale_line.sale.from_location)
p.to_location = fee.shipment_in.to_location if fee.shipment_in else (fee.line.purchase.to_location if fee.line else fee.sale_line.sale.to_location)
if fee.shipment_in and fee.shipment_in.lotqt:
p.payment_term = fee.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
elif fee.line:
p.payment_term = fee.line.purchase.payment_term
elif fee.sale_line:
p.payment_term = fee.sale_line.sale.payment_term
Purchase.save([p])
#if reception of moves done we need to generate accrual for fee
StockMove = Pool().get('stock.move')
feelots = FeeLots.search(['fee','=',record.id])
for lot in feelots:
move = lot.get_received_move()
if move:
Warning = Pool().get('res.user.warning')
warning_name = Warning.format("Lot ever received", [])
if Warning.check(warning_name):
raise Warning(warning_name,
"By clicking yes, an accrual for this fee will be created")
AccountMove = Pool().get('account.move')
account_move = move._get_account_stock_move_fee(record.id)
AccountMove.save([account_move])
if not fee.sale_line:
feelots = FeeLots.search(['fee','=',fee.id])
for fl in feelots:
if fee.product.template.landed_cost:
move = fl.lot.get_received_move()
if move:
Warning = Pool().get('res.user.warning')
warning_name = Warning.format("Lot ever received", [])
if Warning.check(warning_name):
raise UserWarning(warning_name,
"By clicking yes, an accrual for this fee will be created")
AccountMove = Pool().get('account.move')
account_move = move._get_account_stock_move_fee(fee)
AccountMove.save([account_move])
else:
AccountMove = Pool().get('account.move')
account_move = fee._get_account_move_fee(fl.lot)
AccountMove.save([account_move])
return records
return fees
def _get_account_move_fee(self,lot,in_out='in',amt = None):
pool = Pool()
AccountMove = pool.get('account.move')
Date = pool.get('ir.date')
Period = pool.get('account.period')
AccountConfiguration = pool.get('account.configuration')
if self.product.type != 'service':
return
today = Date.today()
company = lot.line.purchase.company if lot.line else lot.sale_line.sale.company
for date in [today]:
try:
period = Period.find(company, date=date, test_state=False)
except PeriodNotFoundError:
if date < today:
return
continue
break
else:
return
if period.state != 'open':
date = today
period = Period.find(company, date=date)
AccountMoveLine = pool.get('account.move.line')
Currency = pool.get('currency.currency')
move_line = AccountMoveLine()
move_line.lot = lot
move_line.fee = self
move_line.origin = None
move_line_ = AccountMoveLine()
move_line_.lot = lot
move_line_.fee = self
move_line_.origin = None
amount = amt if amt else self.amount
if self.currency != company.currency:
with Transaction().set_context(date=today):
amount_converted = amount
amount = Currency.compute(self.currency,
amount, company.currency)
move_line.second_currency = self.currency
if self.p_r == 'pay':
move_line.debit = amount
move_line.credit = Decimal(0)
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
if hasattr(move_line, 'second_currency') and move_line.second_currency:
move_line.amount_second_currency = amount_converted
move_line_.debit = Decimal(0)
move_line_.credit = amount
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
move_line_.amount_second_currency = -amount_converted
else:
move_line.debit = Decimal(0)
move_line.credit = amount
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
if hasattr(move_line, 'second_currency') and move_line.second_currency:
move_line.amount_second_currency = -amount_converted
move_line_.debit = amount
move_line_.credit = Decimal(0)
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
move_line_.amount_second_currency = amount_converted
logger.info("FEE_MOVELINES_1:%s",move_line)
logger.info("FEE_MOVELINES_2:%s",move_line_)
AccountJournal = Pool().get('account.journal')
journal = AccountJournal.search(['type','=','expense'])
if journal:
journal = journal[0]
description = None
description = 'Fee'
return AccountMove(
journal=journal,
period=period,
date=date,
origin=None,
description=description,
lines=[move_line,move_line_],
)
class FeeLots(ModelSQL,ModelView):
"Fee lots"

View File

@@ -0,0 +1,141 @@
from decimal import Decimal, getcontext
from datetime import date
from calendar import isleap
getcontext().prec = 28
class DayCount:
@staticmethod
def year_fraction(start_date, end_date, convention):
if end_date <= start_date:
return Decimal('0')
if convention == 'ACT/360':
return Decimal((end_date - start_date).days) / Decimal(360)
elif convention in ('ACT/365', 'ACT/365F'):
return Decimal((end_date - start_date).days) / Decimal(365)
elif convention == 'ACT/ACT_ISDA':
return DayCount._act_act_isda(start_date, end_date)
elif convention == '30/360_US':
return DayCount._30_360_us(start_date, end_date)
elif convention == '30E/360':
return DayCount._30e_360(start_date, end_date)
elif convention == '30E/360_ISDA':
return DayCount._30e_360_isda(start_date, end_date)
else:
raise ValueError(f"Unsupported convention {convention}")
# ---------- IMPLEMENTATIONS ----------
@staticmethod
def _act_act_isda(start_date, end_date):
total = Decimal('0')
current = start_date
while current < end_date:
year_end = date(current.year, 12, 31)
period_end = min(year_end.replace(day=31) +
(date(current.year + 1, 1, 1) - year_end),
end_date)
days_in_period = (period_end - current).days
days_in_year = 366 if isleap(current.year) else 365
total += Decimal(days_in_period) / Decimal(days_in_year)
current = period_end
return total
@staticmethod
def _30_360_us(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
if d1.day in (30, 31) and d2.day == 31:
d2_day = 30
else:
d2_day = d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360(d1, d2):
d1_day = min(d1.day, 30)
d2_day = min(d2.day, 30)
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360_isda(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
d2_day = 30 if d2.day == 31 else d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
class InterestCalculator:
@staticmethod
def calculate(
start_date,
end_date,
rate,
rate_type='annual', # 'annual' or 'monthly'
convention='ACT/360',
compounding='simple', # simple, annual, monthly, continuous
):
"""
Retourne le facteur d'intérêt (pas le montant).
"""
if not start_date or not end_date:
return Decimal('0')
if end_date <= start_date:
return Decimal('0')
rate = Decimal(str(rate))
# Conversion en taux annuel si besoin
if rate_type == 'monthly':
annual_rate = rate * Decimal(12)
else:
annual_rate = rate
yf = DayCount.year_fraction(start_date, end_date, convention)
if compounding == 'simple':
return annual_rate * yf
elif compounding == 'annual':
return (Decimal(1) + annual_rate) ** yf - Decimal(1)
elif compounding == 'monthly':
monthly_rate = annual_rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
elif compounding == 'continuous':
from math import exp
return Decimal(exp(float(annual_rate * yf))) - Decimal(1)
else:
raise ValueError("Unsupported compounding mode")

View File

@@ -0,0 +1,259 @@
from decimal import Decimal, getcontext
from datetime import datetime, date
from calendar import isleap
from typing import Callable, Dict
import uuid
getcontext().prec = 28
# {
# "computation_type": "INTEREST_ACCRUAL",
# "input": {
# "start_date": "2026-01-01",
# "end_date": "2026-06-30",
# "notional": "1000000",
# "rate": {
# "value": "0.08",
# "type": "ANNUAL"
# },
# "day_count_convention": "ACT/360",
# "compounding_method": "SIMPLE"
# }
# }
# result = FinancialComputationService.execute(payload)
# interest = Decimal(result["result"]["interest_amount"])
# interest = currency.round(interest)
# ============================================================
# VERSIONING
# ============================================================
ENGINE_VERSION = "1.0.0"
# ============================================================
# REGISTRY (PLUGIN SYSTEM)
# ============================================================
DAY_COUNT_REGISTRY: Dict[str, Callable] = {}
COMPOUNDING_REGISTRY: Dict[str, Callable] = {}
def register_day_count(name: str):
def decorator(func):
DAY_COUNT_REGISTRY[name] = func
return func
return decorator
def register_compounding(name: str):
def decorator(func):
COMPOUNDING_REGISTRY[name] = func
return func
return decorator
# ============================================================
# DOMAIN DAY COUNT CONVENTIONS
# ============================================================
@register_day_count("ACT/360")
def act_360(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(360)
@register_day_count("ACT/365F")
def act_365f(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(365)
@register_day_count("ACT/ACT_ISDA")
def act_act_isda(start: date, end: date) -> Decimal:
total = Decimal("0")
current = start
while current < end:
year_end = date(current.year, 12, 31)
next_year = date(current.year + 1, 1, 1)
period_end = min(next_year, end)
days = (period_end - current).days
year_days = 366 if isleap(current.year) else 365
total += Decimal(days) / Decimal(year_days)
current = period_end
return total
@register_day_count("30E/360")
def thirty_e_360(start: date, end: date) -> Decimal:
d1 = min(start.day, 30)
d2 = min(end.day, 30)
days = (
(end.year - start.year) * 360 +
(end.month - start.month) * 30 +
(d2 - d1)
)
return Decimal(days) / Decimal(360)
# ============================================================
# DOMAIN COMPOUNDING STRATEGIES
# ============================================================
@register_compounding("SIMPLE")
def simple(rate: Decimal, yf: Decimal) -> Decimal:
return rate * yf
@register_compounding("ANNUAL")
def annual(rate: Decimal, yf: Decimal) -> Decimal:
return (Decimal(1) + rate) ** yf - Decimal(1)
@register_compounding("MONTHLY")
def monthly(rate: Decimal, yf: Decimal) -> Decimal:
monthly_rate = rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
@register_compounding("CONTINUOUS")
def continuous(rate: Decimal, yf: Decimal) -> Decimal:
from math import exp
return Decimal(exp(float(rate * yf))) - Decimal(1)
# ============================================================
# DOMAIN INTEREST COMPUTATION OBJECT
# ============================================================
class InterestComputation:
def __init__(
self,
start_date: date,
end_date: date,
notional: Decimal,
rate_value: Decimal,
rate_type: str,
day_count: str,
compounding: str,
):
self.start_date = start_date
self.end_date = end_date
self.notional = notional
self.rate_value = rate_value
self.rate_type = rate_type
self.day_count = day_count
self.compounding = compounding
def compute(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
if self.day_count not in DAY_COUNT_REGISTRY:
raise ValueError("Unsupported day count convention")
if self.compounding not in COMPOUNDING_REGISTRY:
raise ValueError("Unsupported compounding method")
yf = DAY_COUNT_REGISTRY[self.day_count](
self.start_date,
self.end_date
)
# Normalize rate to annual
if self.rate_type == "MONTHLY":
annual_rate = self.rate_value * Decimal(12)
else:
annual_rate = self.rate_value
factor = COMPOUNDING_REGISTRY[self.compounding](
annual_rate,
yf
)
interest_amount = self.notional * factor
return {
"year_fraction": yf,
"interest_factor": factor,
"interest_amount": interest_amount,
}
# ============================================================
# APPLICATION LAYER JSON SERVICE (Camunda Ready)
# ============================================================
class FinancialComputationService:
@staticmethod
def execute(payload: dict) -> dict:
"""
Stateless JSON entrypoint.
Compatible Camunda / REST / Tryton bridge.
"""
try:
request_id = str(uuid.uuid4())
input_data = payload["input"]
start_date = datetime.strptime(
input_data["start_date"], "%Y-%m-%d"
).date()
end_date = datetime.strptime(
input_data["end_date"], "%Y-%m-%d"
).date()
notional = Decimal(input_data["notional"])
rate_value = Decimal(input_data["rate"]["value"])
rate_type = input_data["rate"]["type"].upper()
day_count = input_data["day_count_convention"]
compounding = input_data["compounding_method"]
computation = InterestComputation(
start_date=start_date,
end_date=end_date,
notional=notional,
rate_value=rate_value,
rate_type=rate_type,
day_count=day_count,
compounding=compounding,
)
result = computation.compute()
return {
"metadata": {
"engine_version": ENGINE_VERSION,
"request_id": request_id,
"timestamp": datetime.utcnow().isoformat() + "Z"
},
"result": {
"year_fraction": str(result["year_fraction"]),
"interest_factor": str(result["interest_factor"]),
"interest_amount": str(result["interest_amount"]),
},
"explainability": {
"formula": "Interest = Notional × Factor",
"factor_definition": f"{compounding} compounding applied to annualized rate",
"day_count_used": day_count
},
"status": "SUCCESS"
}
except Exception as e:
return {
"status": "ERROR",
"message": str(e),
"engine_version": ENGINE_VERSION
}

View File

@@ -15,5 +15,7 @@ class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
dashboard = fields.Char("Dashboard connexion")
dark = fields.Boolean("Dark mode")
pnl_id = fields.Integer("Pnl ID")
position_id = fields.Integer("Position ID")
forex_id = fields.Integer("Forex ID")
payload = fields.Char("Metabase payload")
payload = fields.Char("Metabase payload")
automation = fields.Boolean("Automation")

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<!-- Barre centrale (contrat / position) -->
<rect x="11" y="4" width="2" height="16" fill="#267F82"/>
<!-- Flèche gauche (achat) -->
<path d="M9 7 L5 12 L9 17 L9 14 L11 14 L11 10 L9 10 Z"
fill="#267F82"/>
<!-- Flèche droite (vente) -->
<path d="M15 7 L19 12 L15 17 L15 14 L13 14 L13 10 L15 10 Z"
fill="#267F82"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="
M3 18 V7
L8 12
L12 8
L16 12
L21 7
V18
H18 V11
L12 15
L6 11
V18
Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="
M4 18 V6
H7 L12 13 L17 6
H20 V18
H17 V10.5
L12 16
L7 10.5
V18
Z
" fill="#267F82"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,430 @@
from decimal import Decimal
from trytond.pool import Pool, PoolMeta
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
def _get_report_invoice_line(self):
for line in self.lines or []:
if getattr(line, 'type', None) == 'line':
return line
return self.lines[0] if self.lines else None
def _get_report_purchase(self):
purchases = list(self.purchases or [])
return purchases[0] if purchases else None
def _get_report_sale(self):
# Bridge invoice templates to the originating sale so FODT files can
# reuse stable sale.report_* properties instead of complex expressions.
sales = list(self.sales or [])
return sales[0] if sales else None
def _get_report_trade(self):
return self._get_report_sale() or self._get_report_purchase()
def _get_report_purchase_line(self):
purchase = self._get_report_purchase()
if purchase and purchase.lines:
return purchase.lines[0]
def _get_report_sale_line(self):
sale = self._get_report_sale()
if sale and sale.lines:
return sale.lines[0]
def _get_report_trade_line(self):
return self._get_report_sale_line() or self._get_report_purchase_line()
def _get_report_lot(self):
line = self._get_report_trade_line()
if line and line.lots:
for lot in line.lots:
if lot.lot_type == 'physic':
return lot
return line.lots[0]
def _get_report_freight_fee(self):
pool = Pool()
Fee = pool.get('fee.fee')
shipment = self._get_report_shipment()
if not shipment:
return None
fees = Fee.search([
('shipment_in', '=', shipment.id),
('product.name', '=', 'Maritime freight'),
], limit=1)
return fees[0] if fees else None
def _get_report_shipment(self):
lot = self._get_report_lot()
if not lot:
return None
return (
getattr(lot, 'lot_shipment_in', None)
or getattr(lot, 'lot_shipment_out', None)
or getattr(lot, 'lot_shipment_internal', None)
)
@property
def report_address(self):
trade = self._get_report_trade()
if trade and trade.report_address:
return trade.report_address
if self.invoice_address and self.invoice_address.full_address:
return self.invoice_address.full_address
return ''
@property
def report_contract_number(self):
trade = self._get_report_trade()
if trade and trade.full_number:
return trade.full_number
return self.origins or ''
@property
def report_shipment(self):
trade = self._get_report_trade()
if trade and trade.report_shipment:
return trade.report_shipment
return self.description or ''
@property
def report_trader_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'trader', None):
return trade.trader.initial or ''
return ''
@property
def report_origin(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'product_origin', None):
return trade.product_origin or ''
return ''
@property
def report_operator_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'operator', None):
return trade.operator.initial or ''
return ''
@property
def report_product_description(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.description or ''
return ''
@property
def report_description_upper(self):
if self.lines:
return (self.lines[0].description or '').upper()
return ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
line = self._get_report_trade_line()
if line:
return getattr(line, 'attributes_name', '') or ''
return ''
@property
def report_price(self):
trade = self._get_report_trade()
if trade and trade.report_price:
return trade.report_price
return ''
@property
def report_rate_currency_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_currency_upper
return ''
@property
def report_rate_value(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_value
return ''
@property
def report_rate_unit_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_unit_upper
return ''
@property
def report_rate_price_words(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_price_words
return self.report_price or ''
@property
def report_rate_pricing_text(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_pricing_text
return ''
@property
def report_payment_date(self):
trade = self._get_report_trade()
if trade and trade.report_payment_date:
return trade.report_payment_date
return ''
@property
def report_payment_description(self):
trade = self._get_report_trade()
if trade and trade.payment_term:
return trade.payment_term.description or ''
if self.payment_term:
return self.payment_term.description or ''
return ''
@property
def report_nb_bale(self):
sale = self._get_report_sale()
if sale and sale.report_nb_bale:
return sale.report_nb_bale
line = self._get_report_trade_line()
if line and line.lots:
nb_bale = sum(
lot.lot_qt for lot in line.lots if lot.lot_type == 'physic'
)
return 'NB BALES: ' + str(int(nb_bale))
return ''
@property
def report_gross(self):
sale = self._get_report_sale()
if sale and sale.report_gross != '':
return sale.report_gross
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_gross_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
return ''
@property
def report_net(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_net', '') != '':
return trade.report_net
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
if self.lines:
return self.lines[0].quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
@property
def report_bl_date(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_date
@property
def report_bl_nb(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_number
@property
def report_vessel(self):
shipment = self._get_report_shipment()
if shipment and shipment.vessel:
return shipment.vessel.vessel_name
@property
def report_loading_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.from_location:
return shipment.from_location.rec_name
return ''
@property
def report_discharge_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.to_location:
return shipment.to_location.rec_name
return ''
@property
def report_incoterm(self):
trade = self._get_report_trade()
if not trade:
return ''
incoterm = trade.incoterm.code if getattr(trade, 'incoterm', None) else ''
location = (
trade.incoterm_location.party_name
if getattr(trade, 'incoterm_location', None) else ''
)
if incoterm and location:
return f"{incoterm} {location}"
return incoterm or location
@property
def report_proforma_invoice_number(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.number or ''
return ''
@property
def report_proforma_invoice_date(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.invoice_date
@property
def report_controller_name(self):
shipment = self._get_report_shipment()
if shipment and shipment.controller:
return shipment.controller.rec_name
return ''
@property
def report_si_number(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.number or ''
return ''
@property
def report_freight_amount(self):
fee = self._get_report_freight_fee()
if fee:
return fee.get_amount()
return ''
@property
def report_freight_currency_symbol(self):
fee = self._get_report_freight_fee()
if fee and fee.currency:
return fee.currency.symbol or ''
if self.currency:
return self.currency.symbol or ''
return 'USD'
class InvoiceLine(metaclass=PoolMeta):
__name__ = 'account.invoice.line'
def _get_report_trade(self):
origin = getattr(self, 'origin', None)
if not origin:
return None
return getattr(origin, 'sale', None) or getattr(origin, 'purchase', None)
def _get_report_trade_line(self):
return getattr(self, 'origin', None)
@property
def report_product_description(self):
if self.product:
return self.product.description or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.description or ''
return ''
@property
def report_description_upper(self):
return (self.description or '').upper()
@property
def report_rate_currency_upper(self):
origin = self._get_report_trade_line()
currency = getattr(origin, 'linked_currency', None) or self.currency
if currency and currency.rec_name:
return currency.rec_name.upper()
return ''
@property
def report_rate_value(self):
return self.unit_price if self.unit_price is not None else ''
@property
def report_rate_unit_upper(self):
origin = self._get_report_trade_line()
unit = getattr(origin, 'linked_unit', None) or self.unit
if unit and unit.rec_name:
return unit.rec_name.upper()
return ''
@property
def report_rate_price_words(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_price', None):
return trade.report_price
return ''
@property
def report_rate_pricing_text(self):
origin = self._get_report_trade_line()
return getattr(origin, 'get_pricing_text', '') or ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
origin = getattr(self, 'origin', None)
if origin:
return getattr(origin, 'attributes_name', '') or ''
return ''
@property
def report_net(self):
if self.type == 'line':
return self.quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)

View File

@@ -20,6 +20,7 @@ import datetime
import json
import logging
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.service import ContractFactory
logger = logging.getLogger(__name__)
@@ -47,7 +48,7 @@ class LotMove(ModelSQL,ModelView):
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
line = fields.Many2One('purchase.line',"Purchase")
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
lot_move = fields.One2Many('lot.move','lot',"Move")
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
@@ -58,6 +59,7 @@ class Lot(metaclass=PoolMeta):
delta_pr = fields.Numeric("Delta Pr")
delta_amt = fields.Numeric("Delta Amt")
warrant_nb = fields.Char("Warrant Nb")
lot_chunk_key = fields.Integer("Chunk key")
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
pivot = fields.Function(
@@ -201,7 +203,7 @@ class Lot(metaclass=PoolMeta):
)
pivot_data['options'] = {
"rows": ["lot","ct type","event_date","event","move","curr","rate"],
"rows": ["lot","ct type","event_date","event","move","Curr","rate"],
"cols": ["account"],
"aggregatorName": "Sum",
"vals": ["amount"]
@@ -583,7 +585,7 @@ class Lot(metaclass=PoolMeta):
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
for m in lm:
if m.move.from_location.type == 'supplier' and m.move.state == 'done':
return m
return m.move
return None
def GetShipment(self,type):
@@ -1095,6 +1097,7 @@ class LotQt(
newlot.lot_shipment_internal = self.lot_shipment_internal
newlot.lot_shipment_out = self.lot_shipment_out
newlot.lot_product = self.lot_p.line.product
newlot.lot_chunk_key = l.lot_chunk_key
if self.lot_s:
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
newlot.lot_type = 'physic'
@@ -1197,12 +1200,12 @@ class LotQt(
# Pnl.save(pnl_lines)
#Open position update
if pl.quantity_theorical:
OpenPosition = Pool().get('open.position')
OpenPosition.create_from_purchase_line(pl)
# if pl.quantity_theorical:
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_purchase_line(pl)
@classmethod
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None):
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None,finished=False):
pool = Pool()
LotQt = pool.get('lot.qt')
lqt = LotQt.__table__()
@@ -1250,8 +1253,12 @@ class LotQt(
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
if ps == 'P':
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
if not finished:
wh &= (pl.finished == False)
elif ps == 'S':
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
if not finished:
wh &= (sl.finished == False)
if purchase:
wh &= (pu.id == purchase)
if sale:
@@ -1841,7 +1848,8 @@ class LotReport(
supplier = context.get('supplier')
#asof = context.get('asof')
#todate = context.get('todate')
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin)
finished = context.get('finished')
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin,finished)
return query
@classmethod
@@ -1931,6 +1939,12 @@ class LotContext(ModelView):
('pnl', 'Pnl'),
],'Mode')
finished = fields.Boolean("Display finished")
@classmethod
def default_finished(cls):
return False
@classmethod
def default_asof(cls):
pool = Pool()
@@ -2011,17 +2025,24 @@ class LotShipping(Wizard):
if r.r_lot_shipment_in:
raise UserError("Please unlink before linking to a new shipment !")
else:
shipped_quantity = Decimal(r.r_lot_quantity)
shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
logger.info("LotShipping:%s",shipped_quantity)
shipment_origin = None
if self.ship.quantity:
shipped_quantity = self.ship.quantity
if shipped_quantity == 0:
shipped_quantity = Decimal(r.r_lot_matched)
shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
if self.ship.shipment == 'in':
if not self.ship.shipment_in:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
elif self.ship.shipment == 'out':
if not self.ship.shipment_out:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
elif self.ship.shipment == 'int':
if not self.ship.shipment_internal:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
if r.id < 10000000 :
l = Lot(r.id)
@@ -2048,10 +2069,13 @@ class LotShipping(Wizard):
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
l.lot_av = 'reserved'
Lot.save([l])
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
Lot.save([l])
else:
lqt = LotQt(r.id - 10000000)
#Increase forecasted virtual part shipped
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
logger.info("LotShipping2:%s",shipped_quantity)
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
#Decrease forecasted virtual part non shipped
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
@@ -2455,6 +2479,7 @@ class LotAddLine(ModelView):
lot_gross_quantity = fields.Numeric("Gross weight")
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
lot_premium = fields.Numeric("Premium")
lot_chunk_key = fields.Integer("Chunk key")
# @fields.depends('lot_qt')
# def on_change_with_lot_quantity(self):
@@ -2617,6 +2642,17 @@ class LotInvoice(Wizard):
invoicing = StateTransition()
message = StateView(
'purchase.create_prepayment.message',
'purchase_trade.create_prepayment_message_form',
[
Button('OK', 'end', 'tryton-ok'),
Button('See Invoice', 'see_invoice', 'tryton-go-next'),
]
)
see_invoice = StateAction('account_invoice.act_invoice_form')
def transition_start(self):
return 'inv'
@@ -2663,7 +2699,7 @@ class LotInvoice(Wizard):
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
val['lot_unit'] = lot.lot_unit_line.id
val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
unit = val['lot_unit']
val['lot_currency'] = lot.lot_price_ct_symbol
lot_p.append(val)
@@ -2679,6 +2715,7 @@ class LotInvoice(Wizard):
val_s['lot_diff_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
val_s['lot_diff_amount'] = val_s['lot_amount'] - Decimal(lot.sale_invoice_line_prov.amount)
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
lot_s.append(val_s)
if line:
if line.fees:
@@ -2760,12 +2797,27 @@ class LotInvoice(Wizard):
continue
lots.append(lot)
invoice_line = None
if self.inv.type == 'purchase':
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.invoice_line else r.r_lot_p.invoice_line_prov
else:
if sale:
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
return 'end'
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.sale_invoice_line else r.r_lot_p.sale_invoice_line_prov
self.message.invoice = invoice_line.invoice
return 'message'
def default_message(self, fields):
return {
'message': 'The invoice has been successfully created.',
}
def do_see_invoice(self, action):
action['views'].reverse() # pour ouvrir en form directement
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
return action, {'res_id':self.message.invoice.id}
def end(self):
return 'reload'
@@ -3142,136 +3194,13 @@ class CreateContracts(Wizard):
}
def transition_creating(self):
SaleLine = Pool().get('sale.line')
Sale = Pool().get('sale.sale')
PurchaseLine = Pool().get('purchase.line')
Purchase = Pool().get('purchase.purchase')
LotQt = Pool().get('lot.qt')
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
Lot = Pool().get('lot.lot')
Date = Pool().get('ir.date')
self.sale_lines = []
type = self.ct.type
base_contract = self.ct.lot.sale_line.sale if type == 'Purchase' else self.ct.lot.line.purchase
for c in self.ct.contracts:
contract = Purchase() if type == 'Purchase' else Sale()
contract_line = PurchaseLine() if type == 'Purchase' else SaleLine()
parts = c.currency_unit.split("_")
if int(parts[0]) != 0:
contract.currency = int(parts[0])
else:
contract.currency = 1
contract.party = c.party
contract.crop = c.crop
contract.tol_min = c.tol_min
contract.tol_max = c.tol_max
if type == 'Purchase':
contract.purchase_date = Date.today()
else:
contract.sale_date = Date.today()
contract.reference = c.reference
if base_contract.from_location and base_contract.to_location:
if type == 'Purchase':
contract.to_location = base_contract.from_location
else:
contract.from_location = base_contract.to_location
if base_contract.from_location.type == 'supplier' and base_contract.to_location.type == 'customer':
contract.from_location = base_contract.from_location
contract.to_location = base_contract.to_location
if c.party.wb:
contract.wb = c.party.wb
if c.party.association:
contract.association = c.party.association
if type == 'Purchase':
if c.party.supplier_payment_term:
contract.payment_term = c.party.supplier_payment_term
else:
if c.party.customer_payment_term:
contract.payment_term = c.party.customer_payment_term
contract.incoterm = c.incoterm
if c.party.addresses:
contract.invoice_address = c.party.addresses[0]
if type == 'Sale':
contract.shipment_address = c.party.addresses[0]
contract.__class__.save([contract])
contract_line.quantity = c.quantity
contract_line.quantity_theorical = c.quantity
contract_line.product = self.ct.product
contract_line.price_type = c.price_type
contract_line.unit = self.ct.unit
if type == 'Purchase':
contract_line.purchase = contract.id
else:
contract_line.sale = contract.id
contract_line.created_by_code = self.ct.matched
contract_line.premium = Decimal(0)
if int(parts[0]) == 0:
contract_line.enable_linked_currency = True
contract_line.linked_currency = 1
contract_line.linked_unit = int(parts[1])
contract_line.linked_price = c.price
contract_line.unit_price = contract_line.get_price_linked_currency()
else:
contract_line.unit_price = c.price if c.price else Decimal(0)
contract_line.del_period = c.del_period
contract_line.from_del = c.from_del
contract_line.to_del = c.to_del
contract_line.__class__.save([contract_line])
logger.info("CREATE_ID:%s",contract.id)
logger.info("CREATE_LINE_ID:%s",contract_line.id)
if self.ct.matched:
lot = Lot()
if type == 'Purchase':
lot.line = contract_line.id
else:
lot.sale_line = contract_line.id
lot.lot_qt = None
lot.lot_unit = None
lot.lot_unit_line = contract_line.unit
lot.lot_quantity = round(contract_line.quantity,5)
lot.lot_gross_quantity = None
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = contract_line.product
lqtt = LotQtType.search([('sequence','=',1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = round(lot.lot_quantity,5)
lqh.gross_quantity = round(lot.lot_quantity,5)
lot.lot_hist = [lqh]
Lot.save([lot])
vlot = self.ct.lot
shipment_origin = None
if self.ct.shipment_in:
shipment_origin = 'stock.shipment.in,' + str(self.ct.shipment_in.id)
elif self.ct.shipment_internal:
shipment_origin = 'stock.shipment.internal,' + str(self.ct.shipment_internal.id)
elif self.ct.shipment_out:
shipment_origin = 'stock.shipment.out,' + str(self.ct.shipment_out.id)
qt = c.quantity
if type == 'Purchase':
if not lot.updateVirtualPart(qt,shipment_origin,vlot):
lot.createVirtualPart(qt,shipment_origin,vlot)
#Decrease forecasted virtual part non matched
lot.updateVirtualPart(-qt,shipment_origin,vlot,'only sale')
else:
if not vlot.updateVirtualPart(qt,shipment_origin,lot):
vlot.createVirtualPart(qt,shipment_origin,lot)
#Decrease forecasted virtual part non matched
vlot.updateVirtualPart(-qt,shipment_origin,None)
ContractFactory.create_contracts(
self.ct.contracts,
type_=self.ct.type,
ct=self.ct,
)
return 'end'
# def do_matching(self, action):
# return action, {
# 'ids': self.sale_lines,
# 'model': str(self.ct.lot.id),
# }
def end(self):
return 'reload'
@@ -3301,7 +3230,6 @@ class ContractsStart(ModelView):
def default_matched(cls):
return True
class ContractDetail(ModelView):
"Contract Detail"
@@ -3309,26 +3237,29 @@ class ContractDetail(ModelView):
category = fields.Integer("Category")
cd = fields.Many2One('contracts.start',"Contracts")
party = fields.Many2One('party.party',"Party",domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
currency = fields.Many2One('currency.currency',"Currency")
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm")
quantity = fields.Numeric("Quantity",digits=(1,5))
unit = fields.Many2One('product.uom',"Unit")
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
currency = fields.Many2One('currency.currency',"Currency", required=True)
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
unit = fields.Many2One('product.uom',"Unit", required=True)
qt_unit = fields.Many2One('product.uom',"Unit")
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
tol_min = fields.Numeric("Tol - in %", required=True)
tol_max = fields.Numeric("Tol + in %", required=True)
crop = fields.Many2One('purchase.crop',"Crop")
del_period = fields.Many2One('product.month',"Delivery Period")
from_del = fields.Date("From")
to_del = fields.Date("To")
price = fields.Numeric("Price",digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
price_type = price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
], 'Price type')
], 'Price type', required=True)
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
reference = fields.Char("Reference")
from_location = fields.Many2One('stock.location',"From location")
to_location = fields.Many2One('stock.location',"To location")
payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
@classmethod
def default_category(cls):
@@ -3385,7 +3316,7 @@ class ContractDetail(ModelView):
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
return lqt.lot_p.line.purchase.crop.id
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
return lqt.lot_s.line.sale.crop.id
return lqt.lot_s.sale_line.sale.crop.id
@classmethod
def default_currency(cls):

View File

@@ -0,0 +1,152 @@
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
UNITS = (
"ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN ELEVEN TWELVE "
"THIRTEEN FOURTEEN FIFTEEN SIXTEEN SEVENTEEN EIGHTEEN NINETEEN"
).split()
TENS = "ZERO TEN TWENTY THIRTY FORTY FIFTY SIXTY SEVENTY EIGHTY NINETY".split()
def format_date_en(d):
if not d:
return ''
day = d.day
# Gestion des suffixes ordinaux
if 10 <= day % 100 <= 20:
suffix = 'TH'
else:
suffix = {1: 'ST', 2: 'ND', 3: 'RD'}.get(day % 10, 'TH')
return f"{day}{suffix} {d.strftime('%B').upper()} {d.year}"
def _under_thousand(n):
words = []
hundreds = n // 100
remainder = n % 100
if hundreds:
words.append(UNITS[hundreds])
words.append("HUNDRED")
if remainder:
words.append("AND")
if remainder:
if remainder < 20:
words.append(UNITS[remainder])
else:
words.append(TENS[remainder // 10])
if remainder % 10:
words.append(UNITS[remainder % 10])
return " ".join(words)
def integer_to_words(n):
if n == 0:
return "ZERO"
parts = []
millions = n // 1_000_000
thousands = (n // 1_000) % 1_000
remainder = n % 1_000
if millions:
parts.append(_under_thousand(millions))
parts.append("MILLION")
if thousands:
parts.append(_under_thousand(thousands))
parts.append("THOUSAND")
if remainder:
parts.append(_under_thousand(remainder))
return " ".join(parts)
# ==============================
# 💰 MONETARY
# ==============================
def amount_to_currency_words(amount,
major_singular="DOLLAR",
major_plural="DOLLARS",
minor_singular="CENT",
minor_plural="CENTS"):
"""
Example:
1.20 → ONE DOLLAR AND TWENTY CENTS
2.00 → TWO DOLLARS
"""
amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
integer_part = int(amount)
decimal_part = int((amount - integer_part) * 100)
words = []
# Major unit
major_words = integer_to_words(integer_part)
words.append(major_words)
if integer_part == 1:
words.append(major_singular)
else:
words.append(major_plural)
# Minor unit
if decimal_part:
words.append("AND")
minor_words = integer_to_words(decimal_part)
words.append(minor_words)
if decimal_part == 1:
words.append(minor_singular)
else:
words.append(minor_plural)
return " ".join(words)
# ==============================
# ⚖️ QUANTITY WITH UNIT
# ==============================
def quantity_to_words(quantity,
unit_singular="METRIC TON",
unit_plural="METRIC TONS"):
"""
Example:
1 → ONE METRIC TON
23 → TWENTY THREE METRIC TONS
1.5 → ONE POINT FIVE METRIC TONS
"""
quantity = Decimal(str(quantity)).normalize()
if quantity == quantity.to_integral():
integer_part = int(quantity)
words = integer_to_words(integer_part)
if integer_part == 1:
unit = unit_singular
else:
unit = unit_plural
return f"{words} {unit}"
else:
# lecture décimale simple pour quantités
integer_part = int(quantity)
decimal_str = str(quantity).split(".")[1]
words = integer_to_words(integer_part)
decimal_words = " ".join(UNITS[int(d)] for d in decimal_str)
return f"{words} POINT {decimal_words} {unit_plural}"

View File

@@ -1,10 +1,11 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
from trytond.pool import PoolMeta, Pool
from trytond.exceptions import UserError
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
__all__ = ['Party']
__metaclass__ = PoolMeta
from trytond.transaction import Transaction
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql import Table
from trytond.pyson import Bool, Eval, Id, If
class PartyExecution(ModelSQL,ModelView):
"Party Execution"
@@ -13,7 +14,47 @@ class PartyExecution(ModelSQL,ModelView):
party = fields.Many2One('party.party',"Party")
area = fields.Many2One('country.region',"Area")
percent = fields.Numeric("% targeted")
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
def get_percent(self,name):
return 2
class PartyExecutionSla(ModelSQL,ModelView):
"Party Execution Sla"
__name__ = 'party.execution.sla'
party = fields.Many2One('party.party',"Party")
reference = fields.Char("Reference")
product = fields.Many2One('product.product',"Product")
date_from = fields.Date("From")
date_to = fields.Date("To")
places = fields.One2Many('party.execution.place','pes',"")
class PartyExecutionPlace(ModelSQL,ModelView):
"Party Sla Place"
__name__ = 'party.execution.place'
pes = fields.Many2One('party.execution.sla',"Sla")
location = fields.Many2One('stock.location',"Location")
cost = fields.Numeric("Cost",digits=(16,4))
mode = fields.Selection([
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('rate', '% rate'),
('pcost', '% cost price'),
('ppack', 'Per packing'),
], 'Mode', required=True)
currency = fields.Many2One('currency.currency',"Currency")
unit = fields.Many2One('product.uom',"Unit",domain=[
If(Eval('mode') == 'ppack',
('category', '=', 8),
()),
],
states={
'readonly': Eval('mode') != 'ppack',
})
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
@@ -21,4 +62,53 @@ class Party(metaclass=PoolMeta):
tol_max = fields.Numeric("Tol + in %")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
origin =fields.Char("Origin")
execution = fields.One2Many('party.execution','party',"")
sla = fields.One2Many('party.execution.sla','party', "Sla")
initial = fields.Char("Initials")
def IsAvailableForControl(self,sh):
return True
def get_sla_cost(self,location):
if self.sla:
for sla in self.sla:
SlaPlace = Pool().get('party.execution.place')
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
if sp:
return sp[0].cost,sp[0].mode,sp[0].currency,sp[0].unit
def get_alf(self):
if self.name == 'CARGO CONTROL':
return 105
t = Table('alf')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.ALF_CODE,
where=t.SHORT_NAME.ilike(f'%{self.name}%')
))
rows = cursor.fetchall()
if rows:
return int(rows[0][0])
@classmethod
def getPartyByName(cls, party, category=None):
party = party.upper()
p = cls.search([('name', '=', party)], limit=1)
if p:
return p[0]
else:
p = cls()
p.name = party
cls.save([p])
if category:
Category = Pool().get('party.category')
cat = Category.search(['name','=',category])
if cat:
PartyCategory = Pool().get('party.party-party.category')
pc = PartyCategory()
pc.party = p.id
pc.category = cat[0].id
PartyCategory.save([pc])
return p

View File

@@ -5,10 +5,25 @@
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>
<record model="ir.ui.view" id="party_exec_view_form">
<record model="ir.ui.view" id="party_exec_view_list">
<field name="model">party.execution</field>
<field name="type">tree</field>
<field name="name">party_exec_tree</field>
</record>
<record model="ir.ui.view" id="party_exec_sla_view_form">
<field name="model">party.execution.sla</field>
<field name="type">form</field>
<field name="name">party_exec_sla_form</field>
</record>
<record model="ir.ui.view" id="party_exec_sla_view_list">
<field name="model">party.execution.sla</field>
<field name="type">tree</field>
<field name="name">party_exec_sla_tree</field>
</record>
<record model="ir.ui.view" id="party_exec_place_view_form">
<field name="model">party.execution.place</field>
<field name="type">tree</field>
<field name="name">party_exec_place_tree</field>
</record>
</data>
</tryton>

View File

@@ -21,6 +21,7 @@ class PaymentTermLine(metaclass=PoolMeta):
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
term_type = fields.Selection([
(None, ''),
('advance', 'Advance'),
('cad', 'CAD'),
('open', 'Open'),
@@ -30,18 +31,21 @@ class PaymentTermLine(metaclass=PoolMeta):
trigger_offset = fields.Integer('Trigger Offset')
offset_unit = fields.Selection([
(None, ''),
('calendar', 'Calendar Days'),
('business', 'Business Days'),
], 'Offset Unit')
eom_flag = fields.Boolean('EOM Flag')
eom_mode = fields.Selection([
(None, ''),
('standard', 'Standard'),
('before', 'Before EOM'),
('after', 'After EOM'),
], 'EOM Mode')
risk_classification = fields.Selection([
(None, ''),
('fully_secured', 'Fully Secured'),
('partially_secured', 'Partially Secured'),
('unsecured', 'Unsecured'),

View File

@@ -50,37 +50,243 @@ DAYS = [
('sunday', 'Sunday'),
]
class Estimated(ModelSQL, ModelView):
"Estimated date"
__name__ = 'pricing.estimated'
trigger = fields.Selection(TRIGGERS,"Trigger")
estimated_date = fields.Date("Estimated date")
fin_int_delta = fields.Integer("Financing interests delta")
class MtmScenario(ModelSQL, ModelView):
"MtM Scenario"
__name__ = 'mtm.scenario'
name = fields.Char("Scenario", required=True)
valuation_date = fields.Date("Valuation Date", required=True)
use_last_price = fields.Boolean("Use Last Available Price")
calendar = fields.Many2One(
'price.calendar', "Calendar"
)
class MtmStrategy(ModelSQL, ModelView):
"Mark to Market Strategy"
__name__ = 'mtm.strategy'
name = fields.Char("Name", required=True)
active = fields.Boolean("Active")
scenario = fields.Many2One(
'mtm.scenario', "Scenario", required=True
)
currency = fields.Many2One(
'currency.currency', "Valuation Currency"
)
components = fields.One2Many(
'pricing.component', 'strategy', "Components"
)
@classmethod
def default_active(cls):
return True
def get_mtm(self,line,qty):
pool = Pool()
Currency = pool.get('currency.currency')
total = Decimal(0)
scenario = self.scenario
dt = scenario.valuation_date
for comp in self.components:
value = Decimal(0)
if comp.price_source_type == 'curve' and comp.price_index:
value = Decimal(
comp.price_index.get_price(
dt,
line.unit,
self.currency,
last=scenario.use_last_price
)
)
elif comp.price_source_type == 'matrix' and comp.price_matrix:
value = self._get_matrix_price(comp, line, dt)
if comp.ratio:
value *= Decimal(comp.ratio)
total += value * qty
return Decimal(str(total)).quantize(Decimal("0.01"))
def _get_matrix_price(self, comp, line, dt):
MatrixLine = Pool().get('price.matrix.line')
domain = [
('matrix', '=', comp.price_matrix.id),
]
if line:
domain += [
('origin', '=', line.purchase.from_location),
('destination', '=', line.purchase.to_location),
]
lines = MatrixLine.search(domain)
if lines:
return Decimal(lines[0].price_value)
return Decimal(0)
def run_daily_mtm():
Strategy = Pool().get('mtm.strategy')
Snapshot = Pool().get('mtm.snapshot')
for strat in Strategy.search([('active', '=', True)]):
amount = strat.compute_mtm()
Snapshot.create([{
'strategy': strat.id,
'valuation_date': strat.scenario.valuation_date,
'amount': amount,
'currency': strat.currency.id,
}])
class Mtm(ModelSQL, ModelView):
"Mtm"
"MtM Component"
__name__ = 'mtm.component'
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
price_index = fields.Many2One('price.price',"Curve")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=True, ondelete='CASCADE'
)
def get_cur(self,name):
name = fields.Char("Component", required=True)
component_type = fields.Selection([
('commodity', 'Commodity'),
('freight', 'Freight'),
('quality', 'Quality'),
('fx', 'FX'),
('storage', 'Storage'),
('other', 'Other'),
], "Type", required=True)
fix_type = fields.Many2One('price.fixtype', "Fixation Type")
price_source_type = fields.Selection([
('curve', 'Curve'),
('matrix', 'Matrix'),
('manual', 'Manual'),
], "Price Source", required=True)
price_index = fields.Many2One('price.price', "Price Curve")
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
ratio = fields.Numeric("Ratio / %", digits=(16, 6))
manual_price = fields.Numeric(
"Manual Price",
digits=(16, 6),
help="Price set manually if price_source_type is 'manual'"
)
currency = fields.Many2One('currency.currency', "Currency")
def get_cur(self, name=None):
if self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.price_currency
return self.price_index.price_currency
if self.price_matrix:
return self.price_matrix.currency
return None
@fields.depends('price_index','price_matrix')
def on_change_with_currency(self):
return self.get_cur()
class PriceMatrix(ModelSQL, ModelView):
"Price Matrix"
__name__ = 'price.matrix'
name = fields.Char("Name", required=True)
matrix_type = fields.Selection([
('freight', 'Freight'),
('location', 'Location Spread'),
('quality', 'Quality'),
('storage', 'Storage'),
('other', 'Other'),
], "Matrix Type", required=True)
unit = fields.Many2One('product.uom', "Unit")
currency = fields.Many2One('currency.currency', "Currency")
calendar = fields.Many2One(
'price.calendar', "Calendar"
)
valid_from = fields.Date("Valid From")
valid_to = fields.Date("Valid To")
lines = fields.One2Many(
'price.matrix.line', 'matrix', "Lines"
)
class PriceMatrixLine(ModelSQL, ModelView):
"Price Matrix Line"
__name__ = 'price.matrix.line'
matrix = fields.Many2One(
'price.matrix', "Matrix",
required=True, ondelete='CASCADE'
)
origin = fields.Many2One('stock.location', "Origin")
destination = fields.Many2One('stock.location', "Destination")
product = fields.Many2One('product.product', "Product")
quality = fields.Many2One('product.category', "Quality")
price_value = fields.Numeric("Price", digits=(16, 6))
class MtmSnapshot(ModelSQL, ModelView):
"MtM Snapshot"
__name__ = 'mtm.snapshot'
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=True, ondelete='CASCADE'
)
valuation_date = fields.Date("Valuation Date", required=True)
amount = fields.Numeric("MtM Amount", digits=(16, 6))
currency = fields.Many2One('currency.currency', "Currency")
created_at = fields.DateTime("Created At")
class Component(ModelSQL, ModelView):
"Component"
__name__ = 'pricing.component'
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=False, ondelete='CASCADE'
)
price_source_type = fields.Selection([
('curve', 'Curve'),
('matrix', 'Matrix'),
# ('manual', 'Manual'),
], "Price Source", required=True)
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
ratio = fields.Numeric("%",digits=(16,7))
price_index = fields.Many2One('price.price',"Curve")
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
auto = fields.Boolean("Auto")
fallback = fields.Boolean("Fallback")
@@ -194,6 +400,7 @@ class Trigger(ModelSQL,ModelView):
'readonly': Eval('pricing_period') != None,
})
average = fields.Boolean("Avg")
last = fields.Boolean("Last")
application_period = fields.Many2One('pricing.period',"Application period")
from_a = fields.Date("From",
states={
@@ -217,14 +424,11 @@ class Trigger(ModelSQL,ModelView):
pp = PP(self.application_period)
CO = Pool().get('pricing.component')
co = CO(self.component)
logger.info("DELDATEEST_:%s",co)
if co.line:
d = co.getEstimatedTriggerPurchase(pp.trigger)
else:
d = co.getEstimatedTriggerSale(pp.trigger)
logger.info("DELDATEEST:%s",d)
date_from,date_to,dates = pp.getDates(d)
logger.info("DELDATEEST2:%s",dates)
return date_from,date_to,d,pp.include,dates
def getApplicationListDates(self, cal):
@@ -288,7 +492,7 @@ class Trigger(ModelSQL,ModelView):
pi = PI(pc.price_index)
val = {}
val['date'] = current_date
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency)
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency,self.last)
val['avg'] = val['price']
val['avg_minus_1'] = val['price']
val['isAvg'] = self.average
@@ -330,8 +534,6 @@ class Period(ModelSQL,ModelView):
date_from = None
date_to = None
dates = []
logger.info("GETDATES:%s",t)
logger.info("GETDATES:%s",self.every)
if t:
if self.every:
if t:
@@ -348,21 +550,18 @@ class Period(ModelSQL,ModelView):
while current.month == t.month:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation > 0:
days_to_add = (weekday_target - t.weekday()) % 7
current = t + datetime.timedelta(days=days_to_add)
while len(dates) < self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation < 0:
days_to_sub = (t.weekday() - weekday_target) % 7
current = t - datetime.timedelta(days=days_to_sub)
while len(dates) < -self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current -= datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
else:
if self.startday == 'before':

View File

@@ -3,6 +3,11 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.icon" id="mtm_icon">
<field name="name">tradon-mtm</field>
<field name="path">icons/tradon-mtm.svg</field>
</record>
<record model="ir.ui.view" id="summary_view_tree_sequence">
<field name="model">sale.pricing.summary</field>
<field name="type">tree</field>
@@ -104,5 +109,84 @@ this repository contains the full copyright notices and license terms. -->
<field name="type">form</field>
<field name="name">period_form</field>
</record>
<record model="ir.ui.view" id="mtm_scenario_view_form">
<field name="model">mtm.scenario</field>
<field name="type">form</field>
<field name="name">mtm_scenario_form</field>
</record>
<record model="ir.ui.view" id="mtm_scenario_view_list">
<field name="model">mtm.scenario</field>
<field name="type">tree</field>
<field name="name">mtm_scenario_tree</field>
</record>
<record model="ir.ui.view" id="mtm_strategy_view_form">
<field name="model">mtm.strategy</field>
<field name="type">form</field>
<field name="name">mtm_strategy_form</field>
</record>
<record model="ir.ui.view" id="mtm_strategy_view_list">
<field name="model">mtm.strategy</field>
<field name="type">tree</field>
<field name="name">mtm_strategy_tree</field>
</record>
<record model="ir.ui.view" id="price_matrix_view_form">
<field name="model">price.matrix</field>
<field name="type">form</field>
<field name="name">price_matrix_form</field>
</record>
<record model="ir.ui.view" id="price_matrix_view_list">
<field name="model">price.matrix</field>
<field name="type">tree</field>
<field name="name">price_matrix_tree</field>
</record>
<record model="ir.ui.view" id="price_matrix_line_view_list">
<field name="model">price.matrix.line</field>
<field name="type">tree</field>
<field name="name">price_matrix_line_tree</field>
</record>
<!-- <record model="ir.ui.view" id="price_matrix_line_view_form">
<field name="model">price.matrix.line</field>
<field name="type">form</field>
<field name="name">price_matrix_line_form</field>
</record>
<record model="ir.ui.view" id="mtm_snapshot_view_form">
<field name="model">mtm.snapshot</field>
<field name="type">form</field>
<field name="name">mtm_snapshot_form</field>
</record>
<record model="ir.ui.view" id="mtm_snapshot_view_list">
<field name="model">mtm.snapshot</field>
<field name="type">tree</field>
<field name="name">mtm_snapshot_tree</field>
</record> -->
<record model="ir.action.act_window" id="act_strategy_form">
<field name="name">Strategy</field>
<field name="res_model">mtm.strategy</field>
</record>
<record model="ir.action.act_window.view" id="act_strategy_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="mtm_strategy_view_list"/>
<field name="act_window" ref="act_strategy_form"/>
</record>
<record model="ir.action.act_window.view" id="act_strategy_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="mtm_strategy_view_form"/>
<field name="act_window" ref="act_strategy_form"/>
</record>
<menuitem
name="Mtm"
sequence="99"
id="menu_mtm"
icon="tradon-mtm" />
<menuitem
name="Strategy"
action="act_strategy_form"
parent="menu_mtm"
sequence="10"
id="menu_strategy" />
</data>
</tryton>

File diff suppressed because it is too large Load Diff

View File

@@ -126,6 +126,25 @@ this repository contains the full copyright notices and license terms. -->
<field name="wiz_name">pnl.report</field>
</record>
<record model="ir.ui.view" id="position_bi_view_graph">
<field name="model">position.bi</field>
<field name="type">form</field>
<field name="name">position_bi_graph</field>
</record>
<record model="ir.action.act_window" id="act_position_bi">
<field name="name">Position BI</field>
<field name="res_model">position.bi</field>
</record>
<record model="ir.action.act_window.view" id="act_position_bi_view">
<field name="sequence" eval="30"/>
<field name="view" ref="position_bi_view_graph"/>
<field name="act_window" ref="act_position_bi"/>
</record>
<record model="ir.action.wizard" id="act_position_report">
<field name="name">Position report</field>
<field name="wiz_name">position.report</field>
</record>
<record model="ir.ui.view" id="mtm_view_form">
<field name="model">mtm.component</field>
<field name="type">form</field>
@@ -137,6 +156,77 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">mtm_tree</field>
</record>
<record model="ir.ui.view" id="price_composition_view_tree">
<field name="model">price.composition</field>
<field name="type">tree</field>
<field name="name">price_composition_tree</field>
</record>
<record model="ir.ui.view" id="quality_analysis_view_tree">
<field name="model">quality.analysis</field>
<field name="type">tree</field>
<field name="name">quality_analysis_tree</field>
</record>
<record model="ir.ui.view" id="quality_analysis_view_form">
<field name="model">quality.analysis</field>
<field name="type">form</field>
<field name="name">quality_analysis_form</field>
</record>
<record model="ir.ui.view" id="assay_view_tree">
<field name="model">assay.assay</field>
<field name="type">tree</field>
<field name="name">assay_tree</field>
</record>
<record model="ir.ui.view" id="assay_view_form">
<field name="model">assay.assay</field>
<field name="type">form</field>
<field name="name">assay_form</field>
</record>
<record model="ir.ui.view" id="assay_line_view_tree">
<field name="model">assay.line</field>
<field name="type">tree</field>
<field name="name">assay_line_tree</field>
</record>
<record model="ir.ui.view" id="assay_element_view_form">
<field name="model">assay.element</field>
<field name="type">form</field>
<field name="name">assay_element_form</field>
</record>
<record model="ir.ui.view" id="concentrate_view_tree">
<field name="model">concentrate.term</field>
<field name="type">tree</field>
<field name="name">concentrate_tree</field>
</record>
<record model="ir.ui.view" id="concentrate_view_form">
<field name="model">concentrate.term</field>
<field name="type">form</field>
<field name="name">concentrate_form</field>
</record>
<record model="ir.ui.view" id="payable_rule_view_form">
<field name="model">payable.rule</field>
<field name="type">form</field>
<field name="name">payable_rule_form</field>
</record>
<record model="ir.ui.view" id="penalty_rule_view_form">
<field name="model">penalty.rule</field>
<field name="type">form</field>
<field name="name">penalty_rule_form</field>
</record>
<record model="ir.ui.view" id="penalty_rule_view_tree">
<field name="model">penalty.rule</field>
<field name="type">tree</field>
<field name="name">penalty_rule_tree</field>
</record>
<record model="ir.ui.view" id="penalty_rule_tier_view_tree">
<field name="model">penalty.rule.tier</field>
<field name="type">tree</field>
<field name="name">penalty_rule_tier_tree</field>
</record>
<menuitem
name="Pnl Report"
parent="purchase_trade.menu_global_reporting"
@@ -144,6 +234,13 @@ this repository contains the full copyright notices and license terms. -->
sequence="110"
id="menu_pnl_bi"/>
<menuitem
name="Position Report"
parent="purchase_trade.menu_global_reporting"
action="act_position_bi"
sequence="120"
id="menu_position_bi"/>
<menuitem
parent="purchase_trade.menu_global_reporting"
sequence="100"

View File

@@ -4,6 +4,7 @@ from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.i18n import gettext
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
@@ -15,6 +16,7 @@ import datetime
import logging
import json
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
logger = logging.getLogger(__name__)
@@ -32,6 +34,11 @@ class ContractDocumentType(metaclass=PoolMeta):
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
sale = fields.Many2One('sale.sale', "Sale")
class AnalyticDimensionAssignment(metaclass=PoolMeta):
'Analytic Dimension Assignment'
__name__ = 'analytic.dimension.assignment'
sale = fields.Many2One('sale.sale', "Sale")
class Estimated(metaclass=PoolMeta):
"Estimated date"
__name__ = 'pricing.estimated'
@@ -45,6 +52,12 @@ class FeeLots(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Line")
class Backtoback(metaclass=PoolMeta):
'Back To Back'
__name__ = 'back.to.back'
sale = fields.One2Many('sale.sale','btb', "Sale")
class OpenPosition(metaclass=PoolMeta):
"Open position"
__name__ = 'open.position'
@@ -52,10 +65,11 @@ class OpenPosition(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Sale Line")
client = fields.Many2One('party.party',"Client")
class Mtm(metaclass=PoolMeta):
"Mtm"
__name__ = 'mtm.component'
sale_line = fields.Many2One('sale.line',"Line")
class SaleStrategy(ModelSQL):
"Sale - Document Type"
__name__ = 'sale.strategy'
sale_line = fields.Many2One('sale.line', 'Sale Line')
strategy = fields.Many2One('mtm.strategy', "Strategy")
class Component(metaclass=PoolMeta):
"Component"
@@ -172,7 +186,7 @@ class Summary(ModelSQL,ModelView):
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
sale_line = fields.Many2One('sale.line',"Sale")
sale_line = fields.Many2One('sale.line',"Sale",ondelete='CASCADE')
lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt')
lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt')
@@ -203,34 +217,236 @@ class Lot(metaclass=PoolMeta):
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
from_location = fields.Many2One('stock.location', 'From location',domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location',domain=[('type', "!=", 'supplier')])
btb = fields.Many2One('back.to.back',"Back to back")
bank_accounts = fields.Function(
fields.Many2Many('bank.account', None, None, "Bank Accounts"),
'on_change_with_bank_accounts')
bank_account = fields.Many2One(
'bank.account', "Bank Account",
domain=[('id', 'in', Eval('bank_accounts', []))],
depends=['bank_accounts'])
from_location = fields.Many2One('stock.location', 'From location', required=True,domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')])
shipment_out = fields.Many2One('stock.shipment.out','Sales')
pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
#pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
pnl = fields.One2Many('valuation.valuation.dyn', 'r_sale', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
pnl_ = fields.One2Many('valuation.valuation.line', 'sale', 'Pnl',states={'invisible': Eval('group_pnl'),})
group_pnl = fields.Boolean("Group Pnl")
derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification")
# weight_basis = fields.Selection([
# (None, ''),
# ('ncsw', 'NCSW'),
# ('nlw', 'NLW'),
# ], 'Weight basis')
certif = fields.Many2One('purchase.certification',"Certification")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
crop = fields.Many2One('purchase.crop',"Crop")
tol_min = fields.Numeric("Tol - in %", required=True)
tol_max = fields.Numeric("Tol + in %", required=True)
tol_min_qt = fields.Numeric("Tol -")
tol_max_qt = fields.Numeric("Tol +")
certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),})
wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True)
association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),})
crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
viewer = fields.Function(fields.Text(""),'get_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
analytic_dimensions = fields.One2Many(
'analytic.dimension.assignment',
'sale',
'Analytic Dimensions'
)
trader = fields.Many2One('party.party',"Trader")
operator = fields.Many2One('party.party',"Operator")
our_reference = fields.Char("Our Reference")
company_visible = fields.Function(
fields.Boolean("Visible"), 'on_change_with_company_visible')
lc_date = fields.Date("LC date")
product_origin = fields.Char("Origin")
@fields.depends('company', '_parent_company.party')
def on_change_with_company_visible(self, name=None):
return bool(
self.company and self.company.party
and self.company.party.name == 'MELYA')
def _get_default_bank_account(self):
if not self.party or not self.party.bank_accounts:
return None
party_bank_accounts = list(self.party.bank_accounts)
if self.currency:
for account in party_bank_accounts:
if account.currency == self.currency:
return account
return party_bank_accounts[0]
@fields.depends('party', '_parent_party.bank_accounts')
def on_change_with_bank_accounts(self, name=None):
if self.party and self.party.bank_accounts:
return [account.id for account in self.party.bank_accounts]
return []
@fields.depends(
'company', 'party', 'invoice_party', 'shipment_party', 'warehouse',
'payment_term', 'lines', 'bank_account', '_parent_party.bank_accounts')
def on_change_party(self):
super().on_change_party()
self.bank_account = self._get_default_bank_account()
@fields.depends('party', 'currency', '_parent_party.bank_accounts')
def on_change_currency(self):
self.bank_account = self._get_default_bank_account()
@classmethod
def default_wb(cls):
WB = Pool().get('purchase.weight.basis')
wb = WB.search(['id','>',0])
if wb:
return wb[0].id
@classmethod
def default_certif(cls):
Certification = Pool().get('purchase.certification')
certification = Certification.search(['id','>',0])
if certification:
return certification[0].id
@classmethod
def default_association(cls):
Association = Pool().get('purchase.association')
association = Association.search(['id','>',0])
if association:
return association[0].id
@classmethod
def default_tol_min(cls):
return 0
@classmethod
def default_tol_max(cls):
return 0
@property
def report_terms(self):
if self.lines:
return self.lines[0].note
else:
return ''
@property
def report_gross(self):
if self.lines:
return sum([l.get_current_gross_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_net(self):
if self.lines:
return sum([l.get_current_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_qt(self):
if self.lines:
return quantity_to_words(self.lines[0].quantity)
else:
return ''
@property
def report_nb_bale(self):
text_bale = 'NB BALES: '
nb_bale = 0
if self.lines:
for line in self.lines:
if line.lots:
nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
return text_bale + str(int(nb_bale))
@property
def report_deal(self):
if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
else:
''
@property
def report_packing(self):
nb_packing = 0
unit = ''
if self.lines:
for line in self.lines:
if line.lots:
nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
if len(line.lots)>1:
unit = line.lots[1].lot_unit.name
return str(int(nb_packing)) + unit
@property
def report_price(self):
if self.lines:
if self.lines[0].price_type == 'priced':
if self.lines[0].linked_price:
return amount_to_currency_words(self.lines[0].linked_price,'USC','USC')
else:
return amount_to_currency_words(self.lines[0].unit_price)
elif self.lines[0].price_type == 'basis':
return amount_to_currency_words(self.lines[0].unit_price) + ' ' + self.lines[0].get_pricing_text()
else:
return ''
@property
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_payment_date(self):
if self.lines:
if self.lc_date:
return format_date_en(self.lc_date)
Date = Pool().get('ir.date')
payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
if payment_date:
payment_date = format_date_en(payment_date)
return payment_date
@property
def report_shipment(self):
if self.lines:
if len(self.lines[0].lots)>1:
shipment = self.lines[0].lots[1].lot_shipment_in
lot = self.lines[0].lots[1].lot_name
if shipment:
info = ''
if shipment.bl_number:
info += ' B/L ' + shipment.bl_number
if shipment.supplier:
info += ' BY ' + shipment.supplier.name
if shipment.vessel:
info += ' (' + shipment.vessel.vessel_name + ')'
if shipment.container and shipment.container[0].container_no:
id = 1
for cont in shipment.container:
if id == 1:
info += ' Container(s)'
if cont.container_no:
info += ' ' + cont.container_no
else:
info += ' unnamed'
id += 1
# info += ' (LOT ' + lot + ')'
if shipment.note:
info += ' ' + shipment.note
return info
else:
return ''
@classmethod
def default_viewer(cls):
country_start = "Zobiland"
@@ -316,19 +532,20 @@ class Sale(metaclass=PoolMeta):
for sale in sales:
for line in sale.lines:
if not line.quantity_theorical and line.quantity > 0:
line.quantity_theorical = line.quantity
line.quantity_theorical = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))
Line.save([line])
if line.lots:
line_p = line.lots[0].line
line_p = line.get_matched_lines()#line.lots[0].line
if line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
Pnl.generate(line_p)
for l in line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
Pnl.generate(l.lot_p.line)
if line.quantity_theorical:
OpenPosition = Pool().get('open.position')
OpenPosition.create_from_sale_line(line)
# if line.quantity_theorical:
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_sale_line(line)
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
@@ -341,6 +558,11 @@ class Sale(metaclass=PoolMeta):
for d in line.derivatives:
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
Line.save([line])
class PriceComposition(metaclass=PoolMeta):
__name__ = 'price.composition'
sale_line = fields.Many2One('sale.line',"Sale line")
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
@@ -362,8 +584,18 @@ class SaleLine(metaclass=PoolMeta):
}),'get_progress')
from_del = fields.Date("From")
to_del = fields.Date("To")
period_at = fields.Selection([
(None, ''),
('laycan', 'Laycan'),
('loading', 'Loading'),
('discharge', 'Discharge'),
('crossing_border', 'Crossing Border'),
('title_transfer', 'Title transfer'),
('arrival', 'Arrival'),
],"Period at")
concentration = fields.Numeric("Concentration")
price_components = fields.One2Many('pricing.component','sale_line',"Components")
mtm = fields.One2Many('mtm.component','sale_line',"Mtm")
mtm = fields.Many2Many('sale.strategy', 'sale_line', 'strategy', 'Mtm Strategy')
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
@@ -374,6 +606,12 @@ class SaleLine(metaclass=PoolMeta):
tol_max = fields.Numeric("Tol + in %",states={
'readonly': (Eval('inherit_tol')),
})
tol_min_qt = fields.Numeric("Tol -",states={
'readonly': (Eval('inherit_tol')),
})
tol_max_qt = fields.Numeric("Tol +",states={
'readonly': (Eval('inherit_tol')),
})
inherit_tol = fields.Boolean("Inherit tolerance")
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
@@ -396,6 +634,76 @@ class SaleLine(metaclass=PoolMeta):
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
attributes = fields.Dict(
'product.attribute', 'Attributes',
domain=[
('sets', '=', Eval('attribute_set')),
],
states={
'readonly': ~Eval('attribute_set'),
},
depends=['product', 'attribute_set'],
help="Add attributes to the variant."
)
attribute_set = fields.Function(
fields.Many2One('product.attribute.set', "Attribute Set"),
'on_change_with_attribute_set'
)
attributes_name = fields.Function(
fields.Char("Attributes Name"),
'on_change_with_attributes_name'
)
finished = fields.Boolean("Mark as finished")
pricing_rule = fields.Text("Pricing description")
price_composition = fields.One2Many('price.composition','sale_line',"Price composition")
@classmethod
def default_finished(cls):
return False
@property
def report_fixing_rule(self):
pricing_rule = ''
if self.pricing_rule:
pricing_rule = self.pricing_rule
return pricing_rule
@property
def get_pricing_text(self):
pricing_text = ''
if self.price_components:
for pc in self.price_components:
if pc.price_index:
pricing_text += 'ON ' + pc.price_index.price_desc + ' ' + (pc.price_index.price_period.description if pc.price_index.price_period else '')
return pricing_text
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
if self.product and self.product.template and self.product.template.attribute_set:
return self.product.template.attribute_set.id
@fields.depends('product', 'attributes')
def on_change_with_attributes_name(self, name=None):
if not self.product or not self.product.attribute_set or not self.attributes:
return
def key(attribute):
return getattr(attribute, 'sequence', attribute.name)
values = []
for attribute in sorted(self.product.attribute_set.attributes, key=key):
if attribute.name in self.attributes:
value = self.attributes[attribute.name]
values.append(gettext(
'product_attribute.msg_label_value',
label=attribute.string,
value=attribute.format(value)
))
return " | ".join(filter(None, values))
@classmethod
def default_price_type(cls):
return 'priced'
@@ -412,10 +720,20 @@ class SaleLine(metaclass=PoolMeta):
def default_inherit_cer(cls):
return True
# @fields.depends('quantity')
# def on_change_with_quantity_theorical(self):
# if not self.quantity_theorical:
# return self.quantity
def get_matched_lines(self):
if self.lots:
LotQt = Pool().get('lot.qt')
return LotQt.search([('lot_s','=',self.lots[0].id),('lot_p','>',0)])
def get_date(self,trigger_event):
trigger_date = None
if self.estimated_date:
logger.info("ESTIMATED_DATE:%s",self.estimated_date)
trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == trigger_event]
logger.info("TRIGGER_DATE:%s",trigger_date)
logger.info("TRIGGER_EVENT:%s",trigger_event)
trigger_date = trigger_date[0] if trigger_date else None
return trigger_date
def get_tol_min(self,name):
if self.inherit_tol:
@@ -652,9 +970,9 @@ class SaleLine(metaclass=PoolMeta):
valuations = Valuation.search([('lot','in',line.lots)])
if valuations:
Valuation.delete(valuations)
op = OpenPosition.search(['sale_line','=',line.id])
if op:
OpenPosition.delete(op)
# op = OpenPosition.search(['sale_line','=',line.id])
# if op:
# OpenPosition.delete(op)
super(SaleLine, cls).delete(lines)
@@ -691,7 +1009,7 @@ class SaleLine(metaclass=PoolMeta):
lot.sale_line = line.id
lot.lot_qt = line.quantity
lot.lot_unit_line = line.unit
lot.lot_quantity = line.quantity
lot.lot_quantity = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))#round(line.quantity,5)
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
@@ -855,18 +1173,22 @@ class ValuationDyn(metaclass=PoolMeta):
Max(val.sale).as_('r_sale'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Literal(None).as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Literal(None).as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Literal(None).as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.base_amount).as_('r_base_amount'),
Sum(val.rate).as_('r_rate'),
Sum(val.mtm).as_('r_mtm'),
Max(val.strategy).as_('r_strategy'),
Max(val.lot).as_('r_lot'),
Max(val.sale_line).as_('r_sale_line'),
where=wh,
group_by=[val.purchase,val.sale])

View File

@@ -52,5 +52,11 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_sale_allocations_wizard"/>
</record>
<record model="ir.ui.view" id="sale_btb_view_form">
<field name="model">sale.sale</field>
<field name="type">form</field>
<field name="name">sale_btb_form</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
from decimal import Decimal
import logging
from trytond.pool import Pool
from trytond.transaction import Transaction
logger = logging.getLogger(__name__)
class ContractFactory:
"""
Factory métier pour créer des Purchase depuis Sale
ou des Sale depuis Purchase.
Compatible :
- Wizard (n contrats)
- Appel direct depuis un modèle (1 contrat)
"""
@classmethod
def create_contracts(cls, contracts, *, type_, ct):
"""
:param contracts: iterable de contracts (wizard lines)
:param type_: 'Purchase' ou 'Sale'
:param ct: objet contenant le contexte (lot, product, unit, matched...)
:return: liste des contracts créés
"""
pool = Pool()
Sale = pool.get('sale.sale')
Purchase = pool.get('purchase.purchase')
SaleLine = pool.get('sale.line')
PurchaseLine = pool.get('purchase.line')
Date = pool.get('ir.date')
created = []
base_contract = (
ct.lot.sale_line.sale
if type_ == 'Purchase'
else ct.lot.line.purchase
)
for c in contracts:
contract = Purchase() if type_ == 'Purchase' else Sale()
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
# ---------- CONTRACT ----------
parts = c.currency_unit.split("_")
contract.currency = int(parts[0]) or 1
contract.party = c.party
contract.crop = c.crop
contract.tol_min = c.tol_min
contract.tol_max = c.tol_max
contract.payment_term = c.payment_term
contract.reference = c.reference
contract.from_location = c.from_location
contract.to_location = c.to_location
context = Transaction().context
contract.company = context.get('company') if context else None
if type_ == 'Purchase':
contract.purchase_date = Date.today()
else:
contract.sale_date = Date.today()
cls._apply_locations(contract, base_contract, type_)
cls._apply_party_data(contract, c.party, type_)
cls._apply_payment_term(contract, c.party, type_)
if type_ == 'Sale':
contract.product_origin = getattr(base_contract, 'product_origin', None)
contract.incoterm = c.incoterm
if c.party.addresses:
contract.invoice_address = c.party.addresses[0]
if type_ == 'Sale':
contract.shipment_address = c.party.addresses[0]
contract.save()
# ---------- LINE ----------
line.quantity = c.quantity
line.quantity_theorical = c.quantity
line.product = ct.product
line.unit = ct.unit
line.price_type = c.price_type
line.created_by_code = ct.matched
line.premium = Decimal(0)
if type_ == 'Purchase':
line.purchase = contract.id
else:
line.sale = contract.id
cls._apply_price(line, c, parts)
line.del_period = c.del_period
line.from_del = c.from_del
line.to_del = c.to_del
line.save()
logger.info("CREATE_ID:%s", contract.id)
logger.info("CREATE_LINE_ID:%s", line.id)
if ct.matched:
cls._create_lot(line, c, ct, type_)
created.append(contract)
return created
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
@staticmethod
def _apply_locations(contract, base, type_):
if not (base.from_location and base.to_location):
return
if type_ == 'Purchase':
contract.to_location = base.from_location
else:
contract.from_location = base.to_location
if (base.from_location.type == 'supplier'
and base.to_location.type == 'customer'):
contract.from_location = base.from_location
contract.to_location = base.to_location
@staticmethod
def _apply_party_data(contract, party, type_):
if party.wb:
contract.wb = party.wb
if party.association:
contract.association = party.association
@staticmethod
def _apply_payment_term(contract, party, type_):
if type_ == 'Purchase' and party.supplier_payment_term:
contract.payment_term = party.supplier_payment_term
elif type_ == 'Sale' and party.customer_payment_term:
contract.payment_term = party.customer_payment_term
@staticmethod
def _apply_price(line, c, parts):
if int(parts[0]) == 0:
line.enable_linked_currency = True
line.linked_currency = 1
line.linked_unit = int(parts[1])
line.linked_price = c.price
line.unit_price = line.get_price_linked_currency()
else:
line.unit_price = c.price if c.price else Decimal(0)
# -------------------------------------------------------------------------
# LOT / MATCHING (repris tel quel du wizard)
# -------------------------------------------------------------------------
@classmethod
def _create_lot(cls, line, c, ct, type_):
pool = Pool()
Lot = pool.get('lot.lot')
LotQtHist = pool.get('lot.qt.hist')
LotQtType = pool.get('lot.qt.type')
lot = Lot()
if type_ == 'Purchase':
lot.line = line.id
else:
lot.sale_line = line.id
lot.lot_qt = None
lot.lot_unit = None
lot.lot_unit_line = line.unit
lot.lot_quantity = round(line.quantity, 5)
lot.lot_gross_quantity = None
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
lqtt = LotQtType.search([('sequence', '=', 1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = round(lot.lot_quantity, 5)
lqh.gross_quantity = round(lot.lot_quantity, 5)
lot.lot_hist = [lqh]
lot.save()
vlot = ct.lot
shipment_origin = cls._get_shipment_origin(ct)
qt = c.quantity
if type_ == 'Purchase':
if not lot.updateVirtualPart(qt, shipment_origin, vlot):
lot.createVirtualPart(qt, shipment_origin, vlot)
# Decrease forecasted virtual part non matched
lot.updateVirtualPart(-qt, shipment_origin, vlot, 'only sale')
else:
if not vlot.updateVirtualPart(qt, shipment_origin, lot):
vlot.createVirtualPart(qt, shipment_origin, lot)
# Decrease forecasted virtual part non matched
vlot.updateVirtualPart(-qt, shipment_origin, None)
@staticmethod
def _get_shipment_origin(ct):
if ct.shipment_in:
return 'stock.shipment.in,%s' % ct.shipment_in.id
if ct.shipment_internal:
return 'stock.shipment.internal,%s' % ct.shipment_internal.id
if ct.shipment_out:
return 'stock.shipment.out,%s' % ct.shipment_out.id
return None

View File

@@ -67,10 +67,10 @@ setup(name=name,
+ ['trytond.modules.purchase_trade.%s' % p
for p in find_packages()]
),
package_data={
'trytond.modules.purchase_trade': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
},
package_data={
'trytond.modules.purchase_trade': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -120,4 +120,4 @@ setup(name=name,
[trytond.modules]
purchase_trade = trytond.modules.purchase_trade
""",
)
)

View File

@@ -16,24 +16,49 @@ from itertools import chain, groupby
from operator import itemgetter
import datetime
from collections import defaultdict
from sql import Table
from trytond.modules.purchase_trade.service import ContractFactory
import requests
import io
import base64
import logging
import json
import re
import html
logger = logging.getLogger(__name__)
class Location(metaclass=PoolMeta):
__name__ = 'stock.location'
def get_places(self):
t = Table('places')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.PLACE_KEY,
where=t.PLACE_NAME.ilike(f'%{self.name}%')
))
rows = cursor.fetchall()
if rows:
return int(rows[0][0])
@classmethod
def getLocationByName(cls, location, type):
location = location.upper()
loc = cls.search([('name', '=', location),('type', '=', type)], limit=1)
if loc:
return loc[0].id
else:
loc = cls()
loc.name = location
loc.type = type
cls.save([loc])
return loc
@classmethod
def get_transit_id(cls):
transit = cls.search([('name', '=', 'Transit')], limit=1)
if transit:
return transit[0].id
else:
transit = cls()
transit.name = 'Transit'
transit.type = 'storage'
cls.save([transit])
return transit.id
return cls.getLocationByName('TRANSIT','storage')
def is_transit(self):
if self.name == 'Transit':
@@ -356,6 +381,12 @@ class ShipmentContainer(ModelSQL, ModelView):
seal_no = fields.Char('Seal Number')
is_reefer = fields.Boolean('Reefer')
class ShipmentWR(ModelSQL,ModelView):
"Shipment WR"
__name__ = "shipment.wr"
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
wr = fields.Many2One('weight.report',"WR")
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
@@ -408,12 +439,24 @@ class ShipmentIn(metaclass=PoolMeta):
'shipment',
'Container'
)
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
controller = fields.Many2One('party.party',"Controller")
controller_target = fields.Char("Targeted controller")
send_instruction = fields.Boolean("Send instruction")
instructions = fields.Text("Instructions")
add_bl = fields.Boolean("Add BL")
add_invoice = fields.Boolean("Add invoice")
returned_id = fields.Char("Returned ID")
result = fields.Char("Result",readonly=True)
agent = fields.Many2One('party.party',"Booking Agent")
service_order_key = fields.Integer("Service Order Key")
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'compute': {},
'send': {},
})
def get_vessel_type(self,name=None):
@@ -426,6 +469,356 @@ class ShipmentIn(metaclass=PoolMeta):
else:
return str(self.id)
def create_fee(self,controller):
Fee = Pool().get('fee.fee')
Product = Pool().get('product.product')
fee = Fee()
fee.shipment_in = self.id
fee.supplier = controller
fee.type = 'budgeted'
fee.p_r = 'pay'
price,mode,curr,unit = controller.get_sla_cost(self.to_location)
if price and mode and curr and unit:
fee.mode = mode
fee.currency = curr
fee.unit = unit
fee.quantity = self.get_bales() or 1
fee.product = Product.get_by_name('Reweighing')
fee.price = price
Fee.save([fee])
def get_controller(self):
ControllerCategory = Pool().get('party.category')
PartyCategory = Pool().get('party.party-party.category')
cc = ControllerCategory.search(['name','=','CONTROLLER'])
if cc:
cc = cc[0]
controllers = PartyCategory.search(['category','=',cc.id])
for c in controllers:
if c.party.IsAvailableForControl(self):
return c.party
def get_instructions_html(self,inv_date,inv_nb):
vessel = self.vessel.vessel_name if self.vessel else ""
lines = [
"<p>Hi,</p>",
"<p>Please find details below for the requested control</p>",
]
lines.append(
"<p>"
f"<strong>BL number:</strong> {self.bl_number} | "
f"<strong>Vessel:</strong> {vessel} | "
f"<strong>ETA:</strong> {self.etad}"
"</p>"
)
if self.incoming_moves:
tot_net = sum([m.lot.get_current_quantity() for m in self.incoming_moves])
tot_gross = sum([m.lot.get_current_gross_quantity() for m in self.incoming_moves])
tot_bale = sum([m.lot.lot_qt for m in self.incoming_moves])
customer = self.incoming_moves[0].lot.sale_line.sale.party.name if self.incoming_moves[0].lot.sale_line else ""
unit = self.incoming_moves[0].lot.lot_unit_line.symbol
lines.append("<p>"
f"<strong>Customer:</strong> {customer} | "
f"<strong>Invoice Nb:</strong> {inv_nb} | "
f"<strong>Invoice Date:</strong> {inv_date}"
"</p>"
)
lines.append(
"<p>"
f"<strong>Nb Bales:</strong> {tot_bale} | "
f"<strong>Net Qt:</strong> {tot_net} {unit} | "
f"<strong>Gross Qt:</strong> {tot_gross} {unit}"
"</p>"
)
return "".join(lines)
# def get_instructions(self):
# lines = [
# "Hi,",
# "",
# "Please find details below for the requested control",
# f"BL number: {self.bl_number}",
# ""
# ]
# if self.incoming_moves:
# for m in self.incoming_moves:
# if m.lot:
# lines.append(
# f"Lot nb: {m.lot.lot_name} | "
# f"Net Qt: {m.lot.get_current_quantity()} {m.lot.lot_unit.symbol} | "
# f"Gross Qt: {m.lot.get_current_gross_quantity()} {m.lot.lot_unit.symbol}"
# )
# return "\n".join(lines)
def _create_lots_from_fintrade(self):
t = Table('freight_booking_lots')
cursor = Transaction().connection.cursor()
query = t.select(
t.BOOKING_NUMBER,
t.LOT_NUMBER,
t.LOT_NBR_BALES,
t.LOT_GROSS_WEIGHT,
t.LOT_NET_WEIGHT,
t.LOT_UOM,
t.LOT_QUALITY,
t.CUSTOMER,
t.SELL_PRICE_CURRENCY,
t.SELL_PRICE_UNIT,
t.SELL_PRICE,
t.SALE_INVOICE,
t.SELL_INV_AMOUNT,
t.SALE_INVOICE_DATE,
t.SELL_PREMIUM,
t.SALE_CONTRACT_NUMBER,
t.SALE_DECLARATION_KEY,
t.SHIPMENT_CHUNK_KEY,
where=(t.BOOKING_NUMBER == int(self.reference))
)
cursor.execute(*query)
rows = cursor.fetchall()
logger.info("ROWS:%s",rows)
inv_date = None
inv_nb = None
if rows:
sale_line = None
for row in rows:
logger.info("ROW:%s",row)
#Purchase & Sale creation
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
LotAdd = Pool().get('lot.add.line')
Currency = Pool().get('currency.currency')
Product = Pool().get('product.product')
Party = Pool().get('party.party')
Uom = Pool().get('product.uom')
Sale = Pool().get('sale.sale')
SaleLine = Pool().get('sale.line')
dec_key = str(row[16]).strip()
chunk_key = str(row[17]).strip()
lot_unit = str(row[5]).strip().lower()
product = str(row[6]).strip().upper()
lot_net_weight = Decimal(row[4])
logger.info("LOT_NET_WEIGHT:%s",lot_net_weight)
lot_gross_weight = Decimal(row[3])
lot_bales = Decimal(row[2])
lot_number = row[1]
customer = str(row[7]).strip().upper()
sell_price_currency = str(row[8]).strip().upper()
sell_price_unit = str(row[9]).strip().lower()
inv_date = str(row[13]).strip()
inv_nb = str(row[11]).strip()
sell_price = Decimal(row[10])
premium = Decimal(row[14])
reference = Decimal(row[15])
logger.info("DECLARATION_KEY:%s",dec_key)
declaration = SaleLine.search(['note','=',dec_key])
if declaration:
sale_line = declaration[0]
logger.info("WITH_DEC:%s",sale_line)
vlot = sale_line.lots[0]
lqt = LotQt.search([('lot_s','=',vlot.id)])
if lqt:
for lq in lqt:
if lq.lot_p:
logger.info("VLOT_P:%s",lq.lot_p)
sale_line.quantity_theorical += round(lot_net_weight,2)
SaleLine.save([sale_line])
lq.lot_p.updateVirtualPart(round(lot_net_weight,2),self,lq.lot_s)
vlot.set_current_quantity(round(lot_net_weight,2),round(lot_gross_weight,2),1)
Lot.save([vlot])
else:
sale = Sale()
sale_line = SaleLine()
sale.party = Party.getPartyByName(customer,'CLIENT')
logger.info("SALE_PARTY:%s",sale.party)
sale.reference = reference
sale.from_location = self.from_location
sale.to_location = self.to_location
sale.company = 6
sale.payment_term = 2
if sale.party.addresses:
sale.invoice_address = sale.party.addresses[0]
sale.shipment_address = sale.party.addresses[0]
if sell_price_currency == 'USC':
sale.currency = Currency.get_by_name('USD')
sale_line.enable_linked_currency = True
sale_line.linked_currency = 1
sale_line.linked_unit = Uom.get_by_name(sell_price_unit)
sale_line.linked_price = round(sell_price,4)
sale_line.unit_price = sale_line.get_price_linked_currency()
else:
sale.currency = Currency.get_by_name(sell_price_currency)
sale_line.unit_price = round(sell_price,4)
sale_line.unit = Uom.get_by_name(sell_price_unit)
sale_line.premium = premium
Sale.save([sale])
sale_line.sale = sale.id
sale_line.quantity = round(lot_net_weight,2)
sale_line.quantity_theorical = round(lot_net_weight,2)
sale_line.product = Product.get_by_name('BRAZIL COTTON')
logger.info("PRODUCT:%s",sale_line.product)
sale_line.unit = Uom.get_by_name(lot_unit)
sale_line.price_type = 'priced'
sale_line.created_by_code = False
sale_line.note = dec_key
SaleLine.save([sale_line])
#need to link the virtual part to the shipment
lqt = LotQt.search([('lot_s','=',sale_line.lots[0])])
if lqt:
lqt[0].lot_shipment_in = self
LotQt.save(lqt)
logger.info("SALE_LINKED_TO_SHIPMENT:%s",self)
ContractStart = Pool().get('contracts.start')
ContractDetail = Pool().get('contract.detail')
ct = ContractStart()
d = ContractDetail()
ct.type = 'Purchase'
ct.matched = True
ct.shipment_in = self
ct.lot = sale_line.lots[0]
ct.product = sale_line.product
ct.unit = sale_line.unit
d.party = Party.getPartyByName('FAIRCOT')
if sale_line.enable_linked_currency:
d.currency_unit = str(sale_line.linked_currency.id) + '_' + str(sale_line.linked_unit.id)
else:
d.currency_unit = str(sale.currency.id) + '_' + str(sale_line.unit.id)
d.quantity = sale_line.quantity
d.unit = sale_line.unit
d.price = sale_line.unit_price
d.price_type = 'priced'
d.crop = None
d.tol_min = 0
d.tol_max = 0
d.incoterm = None
d.reference = str(sale.id)
d.from_location = sale.from_location
d.to_location = sale.to_location
d.del_period = None
d.from_del = None
d.to_del = None
d.payment_term = sale.payment_term
ct.contracts = [d]
ContractFactory.create_contracts(
ct.contracts,
type_=ct.type,
ct=ct,
)
#Lots creation
vlot = sale_line.lots[0]
lqt = LotQt.search([('lot_s','=',vlot.id),('lot_p','>',0)])
if lqt and vlot.lot_quantity > 0:
lqt = lqt[0]
l = LotAdd()
l.lot_qt = lot_bales
l.lot_unit = Uom.get_by_name('bale')
l.lot_unit_line = Uom.get_by_name(lot_unit)
l.lot_quantity = round(lot_net_weight,2)
l.lot_gross_quantity = round(lot_gross_weight,2)
l.lot_premium = premium
l.lot_chunk_key = int(chunk_key)
logger.info("ADD_LOT:%s",int(chunk_key))
LotQt.add_physical_lots(lqt,[l])
return inv_date,inv_nb
def html_to_text(self,html_content):
text = re.sub(r"<br\s*/?>", "\n", html_content, flags=re.IGNORECASE)
text = re.sub(r"</p\s*>", "\n\n", text, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
return html.unescape(text).strip()
def create_service_order(self,so_payload):
response = requests.post(
"http://automation-service:8006/service-order",
json=so_payload,
timeout=10
)
response.raise_for_status()
return response.json()
@classmethod
@ModelView.button
def send(cls, shipments):
Date = Pool().get('ir.date')
Attachment = Pool().get('ir.attachment')
for sh in shipments:
sh.result = "Email not sent"
attachment = []
if sh.add_bl:
attachments = Attachment.search([
('resource', '=', 'stock.shipment.in,' + str(sh.id)),
])
if attachments:
content_b64 = base64.b64encode(attachments[0].data).decode('ascii')
attachment = [
{
"filename": attachments[0].name,
"content": content_b64,
"content_type": "application/pdf"
}
]
if sh.controller:
Contact = Pool().get('party.contact_mechanism')
contact = Contact.search(['party','=',sh.controller.id])
if contact:
payload = {
"to": [contact[0].value],
"subject": "Request for control",
"body": sh.html_to_text(sh.instructions),
"attachments": attachment,
"meta": {
"shipment": sh.bl_number,
"controller": sh.controller.id
}
}
response = requests.post(
"http://automation-service:8006/mail",
json=payload,
timeout=10
)
response.raise_for_status()
data = response.json()
logger.info("SEND_FROM_SHIPMENT:%s",data)
now = datetime.datetime.now()
sh.result = f"Email sent on {now.strftime('%d/%m/%Y %H:%M')}"
sh.save()
if sh.fees:
fee = sh.fees[0]
so_payload = {
"ControllerAlfCode": sh.controller.get_alf(),
"CurrKey": '3',
"Point1PlaceKey": sh.from_location.get_places(),
"Point2PlaceKey": sh.to_location.get_places(),
"OrderReference": sh.reference,
"FeeTotalCost": float(fee.amount),
"FeeUnitPrice": float(fee.price),
"ContractNumbers": sh.number,
"OrderQuantityGW": float(sh.get_quantity()) if sh.get_quantity() else float(1),
"NumberOfPackingBales": int(fee.quantity) if fee.quantity else int(1),
"ChunkKeyList": sh.get_chunk_key()
}
logger.info("PAYLOAD:%s",so_payload)
data = sh.create_service_order(so_payload)
logger.info("SO_NUMBER:%s",data.get('service_order_number'))
sh.result += f" / SO Nb {data.get('service_order_number')}"
sh.service_order_key = int(data.get('service_order_key'))
sh.save()
@classmethod
@ModelView.button
def compute(cls, shipments):
@@ -470,9 +863,19 @@ class ShipmentIn(metaclass=PoolMeta):
def default_dashboard(cls):
return 1
def get_chunk_key(self):
keys = [m.lot.lot_chunk_key for m in self.incoming_moves if m.lot]
return ",".join(map(str, keys)) if keys else None
def get_quantity(self,name=None):
if self.incoming_moves:
return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])
def get_bales(self,name=None):
Lot = Pool().get('lot.lot')
lots = Lot.search(['lot_shipment_in','=',self.id])
if lots:
return sum([l.lot_qt for l in lots])
def get_unit(self,name=None):
if self.incoming_moves:
@@ -587,7 +990,7 @@ class ShipmentIn(metaclass=PoolMeta):
#update line valuation
Pnl = Pool().get('valuation.valuation')
for lot in lots:
Pnl.generate(lot.line)
Pnl.generate(lot.line if lot.line else lot.sale_line)
if sh.sof:
for sof in sh.sof:
if sof.chart:

View File

@@ -38,6 +38,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">stock.shipment.container</field>
<field name="type">tree</field>
<field name="name">shipment_container_tree</field>
</record>
<record model="ir.ui.view" id="shipment_wr_view_tree">
<field name="model">shipment.wr</field>
<field name="type">tree</field>
<field name="name">shipment_wr_tree</field>
</record>
<record model="ir.action.wizard" id="act_vf">

View File

@@ -1,11 +1,12 @@
[tryton]
version=7.2.7
depends:
ir
purchase
sale
stock
res
depends:
ir
purchase
sale
account_invoice
stock
res
lot
document_incoming
incoterm
@@ -28,5 +29,8 @@ xml:
party.xml
forex.xml
global_reporting.xml
derivative.xml
valuation.xml
derivative.xml
valuation.xml
weight_report.xml
dimension.xml
backtoback.xml

View File

@@ -43,14 +43,15 @@ class ValuationBase(ModelSQL):
counterparty = fields.Many2One('party.party',"Counterparty")
product = fields.Many2One('product.product',"Product")
state = fields.Char("State")
price = fields.Numeric("Price",digits='unit')
price = fields.Numeric("Price",digits=(16,4))
currency = fields.Many2One('currency.currency',"Cur")
quantity = fields.Numeric("Quantity",digits='unit')
quantity = fields.Numeric("Quantity",digits=(16,5))
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits='unit')
mtm = fields.Numeric("Mtm",digits='unit')
amount = fields.Numeric("Amount",digits=(16,2))
mtm = fields.Numeric("Mtm",digits=(16,2))
strategy = fields.Many2One('mtm.strategy',"Strategy")
lot = fields.Many2One('lot.lot',"Lot")
base_amount = fields.Numeric("Base Amount",digits='unit')
base_amount = fields.Numeric("Base Amount",digits=(16,2))
rate = fields.Numeric("Rate", digits=(16,6))
@classmethod
@@ -83,9 +84,17 @@ class ValuationBase(ModelSQL):
qty = lot.get_current_quantity_converted()
price = pc.price
logger.info("TERMS:%s",line.terms)
if line.terms:
c = [t for t in line.terms if t.component == pc.price_component]
logger.info("COMPONENTS:%s",c)
if c:
price = c[0].manual_price
values.update({
'reference': f"{pc.get_name()} / {pc.ratio}%",
'price': round(pc.price, 4),
'price': round(price, 4),
'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id,
'product': sale_line.product.id if sale_line else line.product.id,
})
@@ -99,24 +108,23 @@ class ValuationBase(ModelSQL):
base = sale_line.quantity_theorical if sale_line else line.quantity_theorical
values['state'] = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%"
if pc.price and pc.ratio:
amount = round(pc.price * qty * Decimal(sign) * pc.ratio / 100, 4)
if price != None:
amount = round(price * qty * Decimal(sign), 2)
base_amount = amount
currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id
rate = Decimal(1)
if line.purchase.company.currency != currency:
with Transaction().set_context(date=Date.today()):
base_amount = Currency.compute(currency,amount, line.purchase.company.currency)
rate = round(amount / base_amount,6)
rate = round(amount / (base_amount if base_amount else 1),6)
last_price = pc.get_last_price()
mtm = round(Decimal(last_price) * qty * Decimal(sign), 4) if last_price else Decimal(0)
# mtm = round(Decimal(last_price) * qty * Decimal(sign), 2) if last_price else Decimal(0)
values.update({
'quantity': round(qty, 5),
'amount': amount,
'base_amount': base_amount,
'rate': rate,
'mtm': round(amount - (mtm * pc.ratio / 100), 4),
'mtm': None, #round(amount - (mtm * pc.ratio / 100), 2),
'unit': sale_line.unit.id if sale_line else line.unit.id,
'currency': currency,
})
@@ -135,7 +143,7 @@ class ValuationBase(ModelSQL):
)
qty = lot.get_current_quantity_converted()
amount = round(price * qty * Decimal(sign), 4)
amount = round(price * qty * Decimal(sign), 2)
base_amount = amount
currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id
company_currency = sale_line.sale.company.currency if sale_line else line.purchase.company.currency
@@ -177,12 +185,19 @@ class ValuationBase(ModelSQL):
if line.price_type == 'basis':
for pc in line.price_summary or []:
values = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1)
if values:
price_lines.append(values)
if line.mtm:
for strat in line.mtm:
values['mtm'] = strat.get_mtm(line,values['quantity'])
values['strategy'] = strat
if values:
price_lines.append(values)
else:
if values:
price_lines.append(values)
elif line.price_type in ('priced', 'efp') and lot.lot_price:
price_lines.append(
cls._build_simple_pnl(
values = cls._build_simple_pnl(
line=line,
lot=lot,
sale_line=None,
@@ -191,7 +206,16 @@ class ValuationBase(ModelSQL):
sign=-1,
pnl_type=f'pur. {line.price_type}'
)
)
if line.mtm:
for strat in line.mtm:
values['mtm'] = strat.get_mtm(line,values['quantity'])
values['strategy'] = strat
if values:
price_lines.append(values)
else:
if values:
price_lines.append(values)
sale_lots = [lot] if lot.sale_line else [
lqt.lot_s for lqt in LotQt.search([
@@ -209,12 +233,19 @@ class ValuationBase(ModelSQL):
if sl_line.price_type == 'basis':
for pc in sl_line.price_summary or []:
values = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1)
if values:
price_lines.append(values)
if sl_line.mtm:
for strat in line.mtm:
values['mtm'] = strat.get_mtm(sl_line,values['quantity'])
values['strategy'] = strat
if values:
price_lines.append(values)
else:
if values:
price_lines.append(values)
elif sl_line.price_type in ('priced', 'efp'):
price_lines.append(
cls._build_simple_pnl(
values = cls._build_simple_pnl(
line=line,
lot=sl,
sale_line=sl_line,
@@ -223,11 +254,19 @@ class ValuationBase(ModelSQL):
sign=+1,
pnl_type=f'sale {sl_line.price_type}'
)
)
if sl_line.mtm:
for strat in sl_line.mtm:
values['mtm'] = strat.get_mtm(sl_line,values['quantity'])
values['strategy'] = strat
if values:
price_lines.append(values)
else:
if values:
price_lines.append(values)
return price_lines
@classmethod
def group_fees_by_type_supplier(cls,line,fees):
grouped = defaultdict(list)
@@ -252,44 +291,81 @@ class ValuationBase(ModelSQL):
Date = Pool().get('ir.date')
Currency = Pool().get('currency.currency')
FeeLots = Pool().get('fee.lots')
for lot in line.lots or []:
#if line is matched with sale_line we should add the open sale side
sale_lines = line.get_matched_lines() or []
sale_open_lots = tuple(s.lot_s for s in sale_lines if s.lot_s)
all_lots = (line.lots or ()) + sale_open_lots
for lot in all_lots:
fl = FeeLots.search([('lot', '=', lot.id)])
if not fl:
continue
fees = [e.fee for e in fl]
for sf in cls.group_fees_by_type_supplier(line, fees):
price = Decimal(sf.get_price_per_qt())
sign = -1 if sf.p_r == 'pay' else 1
qty = round(lot.get_current_quantity_converted(), 5)
if sf.mode == 'ppack' or sf.mode == 'rate':
price = sf.price
amount = sf.amount * sign
elif sf.mode == 'lumpsum':
price = sf.price
amount = sf.price * sign
qty = 1
else:
price = Decimal(sf.get_price_per_qt())
amount = round(price * lot.get_current_quantity_converted() * sign, 2)
if sf.currency != line.purchase.currency:
with Transaction().set_context(date=Date.today()):
price = Currency.compute(sf.currency, price, line.purchase.currency)
sign = -1 if sf.p_r == 'pay' else 1
fee_lines.append({
'lot': lot.id,
'sale': lot.sale_line.sale.id if lot.sale_line else None,
'purchase': line.purchase.id,
'line': line.id,
'type': (
'shipment fee' if sf.shipment_in
else 'sale fee' if sf.sale_line
else 'pur. fee'
),
'date': Date.today(),
'price': price,
'counterparty': sf.supplier.id,
'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}",
'product': sf.product.id,
'state': sf.type,
'quantity': round(lot.get_current_quantity_converted(), 5),
'amount': round(price * lot.get_current_quantity_converted() * sign, 2),
'mtm': Decimal(0),
'unit': sf.unit.id if sf.unit else line.unit.id,
'currency': sf.currency.id,
})
if line.mtm:
for strat in line.mtm:
fee_lines.append({
'lot': lot.id,
'sale': lot.sale_line.sale.id if lot.sale_line else None,
'purchase': line.purchase.id,
'line': line.id,
'type': (
'shipment fee' if sf.shipment_in
else 'sale fee' if sf.sale_line
else 'pur. fee'
),
'date': Date.today(),
'price': price,
'counterparty': sf.supplier.id,
'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}",
'product': sf.product.id,
'state': sf.type,
'quantity': qty,
'amount': amount,
'mtm': strat.get_mtm(line,qty),
'strategy': strat,
'unit': sf.unit.id if sf.unit else line.unit.id,
'currency': sf.currency.id,
})
else:
fee_lines.append({
'lot': lot.id,
'sale': lot.sale_line.sale.id if lot.sale_line else None,
'purchase': line.purchase.id,
'line': line.id,
'type': (
'shipment fee' if sf.shipment_in
else 'sale fee' if sf.sale_line
else 'pur. fee'
),
'date': Date.today(),
'price': price,
'counterparty': sf.supplier.id,
'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}",
'product': sf.product.id,
'state': sf.type,
'quantity': qty,
'amount': amount,
'mtm': Decimal(0),
'strategy': None,
'unit': sf.unit.id if sf.unit else line.unit.id,
'currency': sf.currency.id,
})
return fee_lines
@@ -318,8 +394,8 @@ class ValuationBase(ModelSQL):
'product': d.product.id,
'state': 'fixed',
'quantity': round(d.quantity, 5),
'amount': round(price * d.quantity * Decimal(-1), 4),
'mtm': round((price * d.quantity * Decimal(-1)) - (mtm * d.quantity * Decimal(-1)), 4),
'amount': round(price * d.quantity * Decimal(-1), 2),
'mtm': round((price * d.quantity * Decimal(-1)) - (mtm * d.quantity * Decimal(-1)), 2),
'unit': line.unit.id,
'currency': line.purchase.currency.id,
})
@@ -331,7 +407,6 @@ class ValuationBase(ModelSQL):
Date = Pool().get('ir.date')
Valuation = Pool().get('valuation.valuation')
ValuationLine = Pool().get('valuation.valuation.line')
Valuation.delete(Valuation.search([
('line', '=', line.id),
('date', '=', Date.today()),
@@ -359,47 +434,34 @@ class Valuation(ValuationBase, ModelView):
table = cls.__table__()
sql = f"""
WITH ranked AS (
WITH totals AS (
SELECT
CASE
WHEN line IS NOT NULL THEN 'P:' || line::text
WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text
END AS block_key,
date,
amount,
ROW_NUMBER() OVER (
PARTITION BY
CASE
WHEN line IS NOT NULL THEN 'P:' || line::text
WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text
END
ORDER BY date DESC
) AS rn
SUM(amount) AS total_amount
FROM {table}
WHERE line IS NOT NULL
OR sale_line IS NOT NULL
OR sale_line IS NOT NULL
GROUP BY date
),
current_prev AS (
ranked AS (
SELECT
block_key,
MAX(CASE WHEN rn = 1 THEN amount END) AS amount_t,
MAX(CASE WHEN rn = 2 THEN amount END) AS amount_t1
FROM ranked
WHERE rn <= 2
GROUP BY block_key
date,
total_amount,
LAG(total_amount) OVER (ORDER BY date) AS previous_total,
ROW_NUMBER() OVER (ORDER BY date DESC) AS rn
FROM totals
)
SELECT
COALESCE(SUM(amount_t), 0) AS total_t,
COALESCE(SUM(amount_t1), 0) AS total_t1,
COALESCE(SUM(amount_t), 0)
- COALESCE(SUM(amount_t1), 0) AS variation
FROM current_prev
total_amount AS last_total,
total_amount - previous_total AS last_variation
FROM ranked
WHERE rn = 1;
"""
cursor.execute(sql)
total_t, total_t1, variation = cursor.fetchone()
last_total, last_variation = cursor.fetchone()
return total_t, total_t1, variation
return last_total, last_variation
class ValuationLine(ValuationBase, ModelView):
"Last Valuation"
@@ -425,6 +487,7 @@ class ValuationDyn(ModelSQL,ModelView):
r_base_amount = fields.Numeric("Base Amount",digits='r_unit')
r_rate = fields.Numeric("Rate",digits=(16,6))
r_mtm = fields.Numeric("Mtm",digits='r_unit')
r_strategy = fields.Many2One('mtm.strategy',"Strategy")
r_lot = fields.Many2One('lot.lot',"Lot")
@classmethod
@@ -457,6 +520,7 @@ class ValuationDyn(ModelSQL,ModelView):
Sum(val.base_amount).as_('r_base_amount'),
Sum(val.rate).as_('r_rate'),
Sum(val.mtm).as_('r_mtm'),
Max(val.strategy).as_('r_strategy'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.type,val.counterparty,val.state])
@@ -470,11 +534,17 @@ class ValuationReport(ValuationBase, ModelView):
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
Date = Pool().get('ir.date')
val = Valuation.__table__()
context = Transaction().context
valuation_date = context.get('valuation_date')
strategy = context.get('strategy')
if not valuation_date:
valuation_date = Date.today()
wh = (val.date == valuation_date)
if strategy:
wh &= (val.strategy == strategy)
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
@@ -499,6 +569,7 @@ class ValuationReport(ValuationBase, ModelView):
val.base_amount.as_('base_amount'),
val.rate.as_('rate'),
val.mtm.as_('mtm'),
val.strategy.as_('strategy'),
val.lot.as_('lot'),
where=wh)
@@ -520,6 +591,7 @@ class ValuationReportContext(ModelView):
('fixed', 'Fixed'),
('hedged', 'Hedged')
], 'State')
strategy = fields.Many2One('mtm.strategy',"Strategy")
@classmethod
def default_valuation_date(cls):

View File

@@ -0,0 +1,16 @@
<form col="4">
<label name="reference"/>
<field name="reference"/>
<label name="date"/>
<field name="date"/>
<label name="type"/>
<field name="type"/>
<label name="status"/>
<field name="status"/>
<label name="lab"/>
<field name="lab"/>
<label name="analysis"/>
<field name="analysis"/>
<newline/>
<field name="lines" colspan="4"/>
</form>

View File

@@ -0,0 +1,8 @@
<tree editable="1">
<field name="element"/>
<field name="value"/>
<field name="unit"/>
<field name="category"/>
<field name="method"/>
<field name="is_payable"/>
</tree>

View File

@@ -0,0 +1,7 @@
<tree editable="1">
<field name="reference"/>
<field name="date"/>
<field name="type"/>
<field name="status"/>
<field name="lab"/>
</tree>

View File

@@ -0,0 +1,9 @@
<form col="8">
<label name="reference"/>
<field name="reference" colspan="2"/>
<newline/>
<!-- <label name="purchase"/> -->
<field name="purchase" mode="form" view_ids="purchase_trade.purchase_btb_view_form" colspan="4"/>
<!-- <label name="sale"/> -->
<field name="sale" mode="form" view_ids="purchase_trade.sale_btb_view_form" colspan="4"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="reference"/>
<field name="purchase"/>
<field name="sale"/>
</tree>

View File

@@ -1,7 +1,9 @@
<tree editable="1">
<field name="price_source_type"/>
<field name="fix_type"/>
<field name="ratio" width="60"/>
<field name="price_index"/>
<field name="price_matrix"/>
<field name="currency" width="60"/>
<field name="auto" width="60"/>
<field name="fallback" width="80"/>

View File

@@ -1,7 +1,9 @@
<tree editable="1">
<field name="price_source_type"/>
<field name="fix_type"/>
<field name="ratio"/>
<field name="price_index"/>
<field name="price_matrix"/>
<field name="currency"/>
<field name="auto"/>
<field name="fallback"/>

View File

@@ -1,7 +1,9 @@
<tree editable="1">
<field name="price_source_type"/>
<field name="fix_type"/>
<field name="ratio"/>
<field name="price_index"/>
<field name="price_matrix"/>
<field name="currency"/>
<field name="auto"/>
<field name="fallback"/>

View File

@@ -0,0 +1,22 @@
<form col="4">
<label name="element"/>
<field name="element"/>
<label name="component"/>
<field name="component"/>
<label name="payable_rule"/>
<field name="payable_rule"/>
<label name="penalty_rules"/>
<field name="penalty_rules"/>
<newline/>
<label name="manual_price"/>
<field name="manual_price"/>
<label name="currency"/>
<field name="currency"/>
<label name="unit"/>
<field name="unit"/>
<newline/>
<label name="valid_from"/>
<field name="valid_from"/>
<label name="valid_to"/>
<field name="valid_to"/>
</form>

View File

@@ -0,0 +1,9 @@
<tree editable="1">
<field name="element"/>
<field name="component"/>
<field name="payable_rule"/>
<field name="penalty_rules"/>
<field name="manual_price"/>
<field name="currency"/>
<field name="unit"/>
</tree>

View File

@@ -9,10 +9,13 @@
<field name="tol_min"/>
<field name="tol_max"/>
<field name="price_type"/>
<field name="payment_term"/>
<field name="incoterm"/>
<field name="crop"/>
<field name="del_period"/>
<field name="from_del"/>
<field name="to_del"/>
<field name="from_location"/>
<field name="to_location"/>
<field name="category" tree_invisible="1"/>
</tree>

View File

@@ -22,7 +22,7 @@
</page>
<page string="Demo" id="de">
<group colspan="6">
<field name="demos" widget="html_viewer" height="500" colspan="6"/>
<field name="demos" widget="html_viewer" height="700" colspan="6"/>
</group>
</page>
</notebook>

View File

@@ -0,0 +1,6 @@
<form>
<label name="dimension"/>
<field name="dimension"/>
<label name="value"/>
<field name="value"/>
</form>

View File

@@ -0,0 +1,4 @@
<tree editable="1">
<field name="dimension"/>
<field name="value"/>
</tree>

View File

@@ -0,0 +1,8 @@
<form>
<label name="name"/>
<field name="name"/>
<label name="code"/>
<field name="code"/>
<label name="active"/>
<field name="active"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="name"/>
<field name="code"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,14 @@
<form col="4">
<label name="dimension"/>
<field name="dimension"/>
<label name="name"/>
<field name="name"/>
<label name="code"/>
<field name="code"/>
<label name="parent"/>
<field name="parent"/>
<label name="active"/>
<field name="active"/>
<newline/>
<field name="children" colspan="4"/>
</form>

View File

@@ -0,0 +1,7 @@
<tree editable="1">
<field name="dimension"/>
<field name="name"/>
<field name="code"/>
<field name="parent"/>
<field name="active"/>
</tree>

View File

@@ -1,4 +1,5 @@
<tree editable="1">
<field name="trigger"/>
<field name="estimated_date"/>
<field name="fin_int_delta"/>
</tree>

View File

@@ -5,13 +5,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="type"/>
<field name="product"/>
<field name="supplier"/>
<field name="currency"/>
<field name="p_r"/>
<field name="mode"/>
<field name="unit"/>
<field name="auto_calculation" width="60"/>
<field name="price"/>
<field name="currency"/>
<field name="weight_type"/>
<field name="quantity" symbol="unit"/>
<field name="p_r"/>
<field name="quantity"/>
<field name="amount"/>
<field name="qt_state"/>
<field name="inherit_shipment"/>
<!-- <field name="purchase"/> -->
<field name="inv"/>

View File

@@ -5,13 +5,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="type"/>
<field name="product"/>
<field name="supplier"/>
<field name="currency"/>
<field name="p_r"/>
<field name="mode"/>
<field name="unit"/>
<field name="auto_calculation" width="60"/>
<field name="price"/>
<field name="currency"/>
<field name="weight_type"/>
<field name="quantity" symbol="unit"/>
<field name="p_r"/>
<field name="quantity"/>
<field name="amount"/>
<field name="qt_state"/>
<!-- <field name="purchase"/> -->
<field name="inv"/>
<field name="state"/>

View File

@@ -9,6 +9,10 @@
<field name="pnl_id"/>
<label name="forex_id"/>
<field name="forex_id"/>
<label name="position_id"/>
<field name="position_id"/>
<label name="payload"/>
<field name="payload"/>
<label name="automation"/>
<field name="automation"/>
</form>

Some files were not shown because too many files have changed in this diff Show More