main #7
107
AGENTS.md
Normal file
107
AGENTS.md
Normal 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 `"` et `'` 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).
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
4103
modules/account_invoice/invoice_ict.fodt
Normal file
4103
modules/account_invoice/invoice_ict.fodt
Normal file
File diff suppressed because it is too large
Load Diff
4097
modules/account_invoice/invoice_ict_final.fodt
Normal file
4097
modules/account_invoice/invoice_ict_final.fodt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
14
modules/account_itsa/__init__.py
Normal file
14
modules/account_itsa/__init__.py
Normal 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')
|
||||
40
modules/account_itsa/account.py
Normal file
40
modules/account_itsa/account.py
Normal 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
|
||||
|
||||
3336
modules/account_itsa/account_itsa.xml
Normal file
3336
modules/account_itsa/account_itsa.xml
Normal file
File diff suppressed because it is too large
Load Diff
10
modules/account_itsa/tryton.cfg
Normal file
10
modules/account_itsa/tryton.cfg
Normal file
@@ -0,0 +1,10 @@
|
||||
[tryton]
|
||||
version=7.2.3
|
||||
depends:
|
||||
account
|
||||
extras_depend:
|
||||
account_invoice
|
||||
xml:
|
||||
account_itsa.xml
|
||||
#tax_ict.xml
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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
377
modules/automation/cron.py
Normal 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)
|
||||
37
modules/automation/cron.xml
Normal file
37
modules/automation/cron.xml
Normal 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>
|
||||
56
modules/automation/freight_booking.py
Normal file
56
modules/automation/freight_booking.py
Normal 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'),
|
||||
]
|
||||
25
modules/automation/freight_booking.xml
Normal file
25
modules/automation/freight_booking.xml
Normal 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>
|
||||
@@ -5,4 +5,6 @@ depends:
|
||||
res
|
||||
document_incoming
|
||||
xml:
|
||||
automation.xml
|
||||
automation.xml
|
||||
freight_booking.xml
|
||||
cron.xml
|
||||
13
modules/automation/view/freight_booking_info_tree.xml
Normal file
13
modules/automation/view/freight_booking_info_tree.xml
Normal 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>
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
|
||||
@@ -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(','):
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<form>
|
||||
<label name="price"/>
|
||||
<field name="price"/>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="attributes"/>
|
||||
<field name="attributes"/>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tree>
|
||||
<field name="price"/>
|
||||
<field name="product"/>
|
||||
<field name="attributes"/>
|
||||
</tree>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
105
modules/purchase/AGENTS.md
Normal 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.
|
||||
122
modules/purchase/docs/business-rules.template.md
Normal file
122
modules/purchase/docs/business-rules.template.md
Normal 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`
|
||||
|
||||
@@ -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 :
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
22
modules/purchase_trade/backtoback.py
Normal file
22
modules/purchase_trade/backtoback.py
Normal 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")
|
||||
|
||||
65
modules/purchase_trade/backtoback.xml
Normal file
65
modules/purchase_trade/backtoback.xml
Normal 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>
|
||||
@@ -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])
|
||||
|
||||
79
modules/purchase_trade/dimension.py
Normal file
79
modules/purchase_trade/dimension.py
Normal 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'
|
||||
)
|
||||
34
modules/purchase_trade/dimension.xml
Normal file
34
modules/purchase_trade/dimension.xml
Normal 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>
|
||||
157
modules/purchase_trade/docs/business-rules.md
Normal file
157
modules/purchase_trade/docs/business-rules.md
Normal 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
|
||||
149
modules/purchase_trade/docs/template-rules.md
Normal file
149
modules/purchase_trade/docs/template-rules.md
Normal 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:
|
||||
- `"..."` pour les guillemets doubles
|
||||
- `'...'` pour les apostrophes
|
||||
- Eviter les formes avec antislashs:
|
||||
- interdit: `\'\'`
|
||||
- interdit: `\'value\'`
|
||||
- Exemples corrects:
|
||||
- `<replace text:p="set_lang(invoice.party.lang)">`
|
||||
- `<if test="invoice.report_payment_description">`
|
||||
- `<tax.description or ''>`
|
||||
|
||||
### 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 `"` / `'`.
|
||||
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`
|
||||
@@ -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"
|
||||
|
||||
141
modules/purchase_trade/finance_tools.py
Normal file
141
modules/purchase_trade/finance_tools.py
Normal 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")
|
||||
259
modules/purchase_trade/financing_tools.py
Normal file
259
modules/purchase_trade/financing_tools.py
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
14
modules/purchase_trade/icons/tradon-btb.svg
Normal file
14
modules/purchase_trade/icons/tradon-btb.svg
Normal 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 |
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal file
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal 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 |
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal file
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal 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 |
430
modules/purchase_trade/invoice.py
Normal file
430
modules/purchase_trade/invoice.py
Normal 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)
|
||||
@@ -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):
|
||||
|
||||
152
modules/purchase_trade/numbers_to_words.py
Normal file
152
modules/purchase_trade/numbers_to_words.py
Normal 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}"
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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>
|
||||
221
modules/purchase_trade/service.py
Normal file
221
modules/purchase_trade/service.py
Normal 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
|
||||
@@ -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
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
16
modules/purchase_trade/view/assay_form.xml
Normal file
16
modules/purchase_trade/view/assay_form.xml
Normal 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>
|
||||
8
modules/purchase_trade/view/assay_line_tree.xml
Normal file
8
modules/purchase_trade/view/assay_line_tree.xml
Normal 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>
|
||||
7
modules/purchase_trade/view/assay_tree.xml
Normal file
7
modules/purchase_trade/view/assay_tree.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<tree editable="1">
|
||||
<field name="reference"/>
|
||||
<field name="date"/>
|
||||
<field name="type"/>
|
||||
<field name="status"/>
|
||||
<field name="lab"/>
|
||||
</tree>
|
||||
9
modules/purchase_trade/view/btb_form.xml
Normal file
9
modules/purchase_trade/view/btb_form.xml
Normal 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>
|
||||
5
modules/purchase_trade/view/btb_tree.xml
Normal file
5
modules/purchase_trade/view/btb_tree.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<tree>
|
||||
<field name="reference"/>
|
||||
<field name="purchase"/>
|
||||
<field name="sale"/>
|
||||
</tree>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
22
modules/purchase_trade/view/concentrate_form.xml
Normal file
22
modules/purchase_trade/view/concentrate_form.xml
Normal 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>
|
||||
9
modules/purchase_trade/view/concentrate_tree.xml
Normal file
9
modules/purchase_trade/view/concentrate_tree.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
modules/purchase_trade/view/dimension_ass_form.xml
Normal file
6
modules/purchase_trade/view/dimension_ass_form.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<form>
|
||||
<label name="dimension"/>
|
||||
<field name="dimension"/>
|
||||
<label name="value"/>
|
||||
<field name="value"/>
|
||||
</form>
|
||||
4
modules/purchase_trade/view/dimension_ass_tree.xml
Normal file
4
modules/purchase_trade/view/dimension_ass_tree.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<tree editable="1">
|
||||
<field name="dimension"/>
|
||||
<field name="value"/>
|
||||
</tree>
|
||||
8
modules/purchase_trade/view/dimension_form.xml
Normal file
8
modules/purchase_trade/view/dimension_form.xml
Normal 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>
|
||||
5
modules/purchase_trade/view/dimension_tree.xml
Normal file
5
modules/purchase_trade/view/dimension_tree.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
14
modules/purchase_trade/view/dimension_value_form.xml
Normal file
14
modules/purchase_trade/view/dimension_value_form.xml
Normal 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>
|
||||
7
modules/purchase_trade/view/dimension_value_tree.xml
Normal file
7
modules/purchase_trade/view/dimension_value_tree.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<tree editable="1">
|
||||
<field name="dimension"/>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="parent"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
@@ -1,4 +1,5 @@
|
||||
<tree editable="1">
|
||||
<field name="trigger"/>
|
||||
<field name="estimated_date"/>
|
||||
<field name="fin_int_delta"/>
|
||||
</tree>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user