Odoo API: invoice has "paid" status after validation - python

I am working on a Python script that will import all my existing invoices to an Odoo 12 instance. I am using odoorpc library to simplify RPC calls to my Odoo instance.
I achieved to create an invoice using the API, registering it in "draft" state. Then, I want to "validate" it and update its state into "open". Unfortunately, using the "Validate" button in Odoo UI or calling the action from the RPC API do the same: invoice state is changed to "paid". I don't understand this behavior, since I didn't register any payment (it will be my next goal).
Here is a simplified version of my script that can be used to reproduce the issue :
import odoorpc
import settings
"""settings module contains various constants used
to connect with Odoo on my VPS"""
if __name__ == "__main__":
odoo = odoorpc.ODOO(settings.ODOO_HOST, port=settings.ODOO_PORT, timeout=10)
odoo.login(settings.ODOO_DB, settings.ODOO_USER, settings.ODOO_PASSWORD)
Partner = odoo.env["res.partner"]
# This partner already exists in DB
customer = Partner.browse([22])
Invoice = odoo.env["account.invoice"]
invoice_id = Invoice.create({
'partner_id' : customer.id,
'state': 'draft',
# This is ID for "Customers Invoices" journal
'journal_id': 1,
'account_id': customer.property_account_receivable_id.id,
# This is ID for default bank account, already registered
'partner_bank_id': 1,
'payment_term_id': odoo.env.ref("account.account_payment_term_net").id,
})
InvoiceLine = odoo.env["account.invoice.line"]
InvoiceLine.create({
"invoice_id": invoice_id,
"name": "A basic product",
"quantity": 6,
"price_unit": 100.0,
# Not sure about this one:
"uom_id": 1,
# No tax
"invoice_line_tax_ids": [],
'journal_id': 1,
'account_id': customer.property_account_receivable_id.id,
})
inv = Invoice.browse([invoice_id])
print("Before validating:", inv.state)
inv.action_invoice_open()
inv = Invoice.browse([invoice_id])
print("After validating:", inv.state)
result:
Before validating: draft
After validating: paid
I think there is something missing or wrong in my invoice creation, but I didn't find what exactly should be modified to have an invoice created the same way it would be from the UI.
Can anybody help me to find what's wrong in my script ?

I found myself a solution. It seems the account_id must be different for invoice and invoice line. To fix the issue, I retrieved the account.journal instance with ID 1 (for "Customers Invoices" journal), then used it to fill invoice line's account_id based on the journal's default_credit_account_id field:
cust_invoices_journal = odoo.env["account.journal"].browse([1])
# [...]
invoice_id = Invoice.create({
# [...]
'journal_id': cust_invoices_journal.id,
'account_id': customer.property_account_receivable_id.id,
# [...]
})
# [...]
InvoiceLine.create({
# [...]
'account_id': cust_invoices_journal.default_credit_account_id.id,
# [...]
})

Related

Facebook Business SDK: cannot create an Ad

I cannot create a simple Ad with an external link to a mobile app. I have properly set access, can create a Campaign, an AdSet, load an image, but during an Ad creation I get an error:
Ads and ad creatives must be associated with a Facebook Page. Try connecting your ad or ad creative to a Page and resubmit your ad.
But I have associated a page! Here is my code:
# No problem with these ones
adset = ...
image_hash = '...'
url = 'https://itunes.apple.com/app/96...'
page_id = '25036...'
# Create an Ad Creative
creative = AdCreative()
creative['_parent_id'] = my_ads_acc_id
creative[AdCreative.Field.title] = 'Aivan Test Creative'
creative[AdCreative.Field.body] = 'Aivan Test Ad Creative Body'
creative[AdCreative.Field.actor_id] = page_id
creative[AdCreative.Field.link_url] = url
creative[AdCreative.Field.object_url] = url
creative[AdCreative.Field.object_type] = AdCreative.ObjectType.domain
creative[AdCreative.Field.call_to_action_type] = AdCreative.CallToActionType.use_mobile_app
creative[AdCreative.Field.image_hash] = image_hash
# Create an Ad
ad = Ad()
ad['_parent_id'] = my_ads_acc_id
ad[Ad.Field.name] = 'Aivan Ad'
ad[Ad.Field.adset_id] = adset[AdSet.Field.id]
ad[Ad.Field.creative] = creative
# This line generates an exception:
ad.remote_create(params={
'status': Ad.Status.paused,
})
I have specified the actor_id field, also I have tried other different code samples, but nothing works well. How can I connect a page?
Additional info:
My app is in development mode. I cannot turn the production mode because it needs a review which needs a completed app.
I have tried to use object_story_spec with link_data in it, but it creates other error because it doesn't work in development mode.
The app and the page are linked with Facebook Business Manager.
The results is the same if I init the API with app token or system user token: FacebookAdsApi.init(app_id, app_secret, app_access_token / system_user_token). The system user has access to both Ads Account and the Page.
I've solved the problem a long time ago, and since that time my server app successfully created lots of Facebook ads of both types, for websites and mobile apps. The first step to solve the problem was to understand that these ads types are completely different on Facebook, they need different settings for Campaign, AdSet & Ad. Here is my code for mobile ads creation.
1) Create Campaign object. account_id must be the ID of your Ad Account.
campaign = Campaign()
campaign['_parent_id'] = account_id
campaign[Campaign.Field.name] = 'Some Campaign Name'
campaign[Campaign.Field.objective] = 'APP_INSTALLS'
campaign.remote_create()
campaign_id = str(campaign[Campaign.Field.id])
2) Create AdSet object.
adset = AdSet()
adset['_parent_id'] = account_id
adset.update({
AdSet.Field.name: 'Some AdSet Name',
AdSet.Field.campaign_id: campaign_id,
AdSet.Field.lifetime_budget: budget * 100,
AdSet.Field.bid_strategy: 'LOWEST_COST_WITHOUT_CAP',
AdSet.Field.billing_event: AdSet.BillingEvent.link_clicks,
AdSet.Field.optimization_goal: AdSet.OptimizationGoal.link_clicks,
AdSet.Field.promoted_object: {
'object_store_url': app_store_url,
'application_id': ad_app_id,
},
AdSet.Field.targeting: targeting_object,
AdSet.Field.start_time: '2018-12-01 00:00:00',
AdSet.Field.end_time: '2018-12-30 23:59:00',
})
adset.remote_create()
adset_id = str(adset[AdSet.Field.id])
Note that to create mobile ad, you initially need to register your mobile app as a Facebook app (here you will get ad_app_id) and specify links to Apple App Store and Google Play Market. So, the value of app_store_url must be equal to one of those links in your Facebook app settings. Unfortunately, app can be registered only manually (if you know how to do it programmatically – write a comment, please).
Also note that billing_event and optimization_goal are connected with ads type (mobile/web) and with each other, you cannot just choose another one. (But if you know that this is possible, or there are some docs on this topics – let me know.)
budget is a money amount in the currency of your Ad Account. You can specify either lifetime_budget or something like day_budget, read the docs about it.
3) Then, you have to create AdCreative object with some other sub objects. Note that some of these lines of code are necessary for FB ad only, others for IG, others for both of them, but together they work well for everything. You can find description for all the formats here.
link_data = AdCreativeLinkData()
link_data[AdCreativeLinkData.Field.name] = main_text
link_data[AdCreativeLinkData.Field.message] = title
link_data[AdCreativeLinkData.Field.link] = app_store_url
link_data[AdCreativeLinkData.Field.image_hash] = image_hash
link_data[AdCreativeLinkData.Field.call_to_action] = {
'type': 'INSTALL_MOBILE_APP',
'value': {
'application': ad_app_id,
'link': app_store_url,
},
}
object_story_spec = AdCreativeObjectStorySpec()
object_story_spec[AdCreativeObjectStorySpec.Field.page_id] = page_id
object_story_spec[AdCreativeObjectStorySpec.Field.link_data] = link_data
creative = AdCreative()
creative['_parent_id'] = account_id
creative[AdCreative.Field.object_story_spec] = object_story_spec
creative[AdCreative.Field.title] = main_text
creative[AdCreative.Field.body] = title
creative[AdCreative.Field.actor_id] = page_id
creative[AdCreative.Field.link_url] = app_store_url
creative[AdCreative.Field.image_hash] = image_hash
To upload an image and get image_hash, check out this doc. The page_id must be an ID of the page which name and logo will be shown as the author of the ad.
You must note that the user, who creates the ad, must have an access to this page, to the mobile app registered on FB (ad_app_id), and to the Ad Account (account_id). In my server application I use Facebook system users for all the work with API.
4) And finally, create the Ad object itself:
ad = Ad()
ad['_parent_id'] = account_id
ad[Ad.Field.name] = 'Some Ad Name'
ad[Ad.Field.adset_id] = adset_id
ad[Ad.Field.creative] = creative
ad.remote_create(params={
'status': Ad.Status.active,
})
ad_id = str(ad[Ad.Field.id])
That's all!
Maybe someone will need to use or just want to see the difference when creating FB/IG ads for websites, it is a little bit simpler. So, here is my code for website ads creation.
1) Create Campaign object. Notice that website ads has a different objective. account_id must be the ID of your Ad Account.
campaign = Campaign()
campaign['_parent_id'] = account_id
campaign[Campaign.Field.name] = 'Some Campaign Name'
campaign[Campaign.Field.objective] = 'LINK_CLICKS'
campaign.remote_create()
campaign_id = str(campaign[Campaign.Field.id])
2) Create AdSet object. Note that billing_event and optimization_goal are connected with ads type (mobile/web) and with each other. Also, here you don't need to specify promoted_object in the AdSet.
adset = AdSet()
adset['_parent_id'] = account_id
adset.update({
AdSet.Field.name: 'Some AdSet Name',
AdSet.Field.campaign_id: campaign_id,
AdSet.Field.lifetime_budget: budget * 100,
AdSet.Field.bid_strategy: 'LOWEST_COST_WITHOUT_CAP',
AdSet.Field.billing_event: AdSet.BillingEvent.impressions,
AdSet.Field.optimization_goal: AdSet.OptimizationGoal.reach,
AdSet.Field.targeting: targeting_object,
AdSet.Field.start_time: '2018-12-01 00:00:00',
AdSet.Field.end_time: '2018-12-30 23:59:00',
})
adset.remote_create()
adset_id = str(adset[AdSet.Field.id])
Rules for budget are the same: budget is a money amount in the currency of your Ad Account. You can specify either lifetime_budget or something like day_budget, read the docs about it.
3) Then, you have to create AdCreative object with some other sub objects. You can find description for all the formats here.
link_data = AdCreativeLinkData()
link_data[AdCreativeLinkData.Field.name] = main_text
link_data[AdCreativeLinkData.Field.message] = title
link_data[AdCreativeLinkData.Field.link] = website_url
link_data[AdCreativeLinkData.Field.image_hash] = image_hash
object_story_spec = AdCreativeObjectStorySpec()
object_story_spec[AdCreativeObjectStorySpec.Field.page_id] = page_id
object_story_spec[AdCreativeObjectStorySpec.Field.link_data] = link_data
creative = AdCreative()
creative['_parent_id'] = account_id
creative[AdCreative.Field.object_story_spec] = object_story_spec
creative[AdCreative.Field.title] = main_text
creative[AdCreative.Field.body] = title
creative[AdCreative.Field.actor_id] = page_id
creative[AdCreative.Field.link_url] = website_url
creative[AdCreative.Field.object_type] = AdCreative.ObjectType.domain
creative[AdCreative.Field.image_hash] = image_hash
To upload an image and get image_hash, check out this doc. The page_id must be an ID of the page which name and logo will be shown as the author of the ad. Note that the user, who creates the ad, must have an access to this page, to the mobile app registered on FB (ad_app_id), and to the Ad Account (account_id).
4) And finally, create the Ad object itself:
ad = Ad()
ad['_parent_id'] = account_id
ad[Ad.Field.name] = 'Some Ad Name'
ad[Ad.Field.adset_id] = adset_id
ad[Ad.Field.creative] = creative
ad.remote_create(params={
'status': Ad.Status.active,
})
ad_id = str(ad[Ad.Field.id])
As you can see, to promote websites you don't need to register them on Facebook (in contrast to mobile ads).

Odoo10 : How can I automatically get the value for partner when customer number is given?

I am creating a module something like call log. In that i need to search the customer number and get the Partner information or have to link the partner automatically.
the following are the codes, somebody please help me.
class model_call(models.Model):
_inherit = 'res.partner'
_name = 'model.call'
_description = 'call logs'
""" *************** Base Fields ******************* """
name = fields.Char(string='Topic')
customer_number = fields.Char(string='Customer Number', track_visiility='onchange', onchange='get_partner(customer_number)')
#-------------------------------------------------
# apis
#-------------------------------------------------
#api.onchange('customer_number')
def get_partner(self, customer_number):
if customer_number:
customer = self.env['res.partner'].search([('customer_number', '=', record.phone)])
return customer
#------------------------------------------------------
customer = fields.Many2one('res.partner', string='Customer', track_visibility='onchange',index=True, help="Linked partner (optional). Usually created when converting the lead.",)
Your onchange isn't correct. You don't need parameters and have to return a dictionary or nothing. The dictionary is only needed for field filtering changes and warning messages. Value changes like yours are made "directly":
#api.onchange('customer_number')
def get_partner(self):
if self.customer_number:
customer = self.env['res.partner'].search(
[('customer_number', '=', self.phone)]) # shouldn't it be self.customer_number?
self.customer = customer
And try to stick to the Odoo Guidelines and change the field customer to customer_id.

Adapting odoo report for email - How to change env for current partner_id only

I have a odoo report from the OCA that I am trying to adapt to be able to be sent as an email to individual customers. The report is seen here Report and Github link
The below is the important part (as I see it). In this function 'data' is passed through from a wizard.
#api.multi
def render_html(self, docids, data):
company_id = data['company_id']
partner_ids = data['partner_ids']
date_start = data['date_start']
date_end = data['date_end']
today = fields.Date.today()
balance_start_to_display, buckets_to_display = {}, {}
lines_to_display, amount_due = {}, {}
currency_to_display = {}
today_display, date_start_display, date_end_display = {}, {}, {}
balance_start = self._get_account_initial_balance(
company_id, partner_ids, date_start)
When I attempt to add the report to an email template as an attachement, I do not know how to pass through parameters to it as well. So I did the following:
If Data is None...
data={
'date_start': str(date.today()-timedelta(days=120)),
'date_end': str(date.today()),
'company_id': self.env.user.company_id.id,
'partner_ids': self._context['active_ids'],
'show_aging_buckets': True,
'filter_non_due_partners': True,
}`
The problem is ' 'partner_ids': self._context['active_ids'], '
Returns every customers statement on the email to every customer. How do I make it so this is only for the current customer?
Thanks in advance for any assistance.

Odoo 8 #api.onchange function not let update/Created one2many value

I am building a module (Odoo 8) , my target is create offers in sale order, this offer can set a fix price for a determinate product or set a gift to zero cost.
I am adding my custom model offer_line, in new tab inside sale order.
Is defined like this:
class OfferSaleOrderLine(models.Model):
_name = 'offer.sale.order.line'
sale_order_ref = fields.Many2one('sale.order',ondelete='set null', string="Sale Order", index=True)
offer_ref = fields.Many2one('offer',ondelete='set null', string="Oferta", index=True)
is_active = fields.Boolean(default=True,string='Activo')
accumulations = fields.Float(digits=(6, 2), help="Acumulaciones")
class SaleOrder(models.Model):
_inherit = 'sale.order'
offers_lines = fields.One2many('offer.sale.order.line','sale_order_ref', string="Lineas de Ofertas")
I have a new api onchange method inside sale order:
#api.onchange('offers_lines')
def _onchange_offers_lines(self):
I check is offer need apply, and i add to offers_line new lines from this onchange function, like this:
self.offers_lines += self.env['offer.sale.order.line'].new({'is_active': True, 'offer_ref': offer, 'accumulations' : is_offer})
This is working perfect, lines is created, added to tab in form and onchange methods is trigger.
But the problem is next, if i try the same with sale order line, no working:
val = {
'name': gift_line.free_product.name,
'order_id': self.id,
'product_id': gift_line.free_product.id,
'product_uom_qty': gift_line.qty,
'product_uom': self.order_line[0].product_uom.id,
'price_unit': 0.0,
'state': 'draft',
}
self.order_line += self.env['sale.order.line'].new(val)
In log, this lines is created, i can see the newid id is created when i foreach self.order_line
****ORDER LINES : ID : ; Product: product.product(5152,) ; Qty: 6.0 ; Price: 0.0 ;****
but the item is no created in sale order line tab, i dont know why, my custom lines(One2many) is created, but, the sale_order_lines, with same code and one2many field too, is not created. I have the same problem if i try set the price_unit to this sale_order_lines. Log says changes is added, but is not updated in form. In next onchange trigger, the changes is dissapear.
Thanks to all!
#api.onchange('Put Your Onchange Field Here')
def _onchange_offers_lines(self):
vals = {
'name': gift_line.free_product.name,
'order_id': self.id,
'product_id': gift_line.free_product.id,
'product_uom_qty': gift_line.qty,
'product_uom': self.order_line[0].product_uom.id,
'price_unit': 0.0,
'state': 'draft'
}
self.order_line = [(0, 0, vals)]
Hope it will help you.
Odoo doesn't natively support onchange on *2many fields, anymore.
You can see that in openerp.models here https://github.com/odoo/odoo/blob/9.0/openerp/models.py#L6050
And furthermore a discussion on that topic here: https://github.com/odoo/odoo/issues/2693
I'm not sure to have properly understand your problem, but I see two things about it.
First you need to check that the field you want to set onchange is not already set in the base module that you are extending. If so, you had to disable the old-style onchange in the view by setting the attribute to 1 in the field (keep in mind that by disabling the api-v7 onchange on the field will not call the old onchange function you will probably want to call it in your new onchange function).
The second problem is that you can't add an item to a one2many field, you probably can to a many2one instead. You also can't use var += value to add an item to a relation field, you must use the special tupple (as described here).

flask-admin form: Constrain Value of Field 2 depending on Value of Field 1

One feature I have been struggling to implement in flask-admin is when the user edits a form, to constrain the value of Field 2 once Field 1 has been set.
Let me give a simplified example in words (the actual use case is more convoluted). Then I will show a full gist that implements that example, minus the "constrain" feature.
Let's say we have a database that tracks some software "recipes" to output reports in various formats. The recipe table of our sample database has two recipes: "Serious Report", "ASCII Art".
To implement each recipe, we choose one among several methods. The method table of our database has two methods: "tabulate_results", "pretty_print".
Each method has parameters. The methodarg table has two parameter names for "tabulate_results" ("rows", "display_total") and two parameters for "pretty_print" ("embellishment_character", "lines_to_jump").
Now for each of the recipes ("Serious Report", "ASCII Art") we need to provide the value of the arguments of their respective methods ("tabulate_results", "pretty_print").
For each record, the recipearg table lets us select a recipe (that's Field 1, for instance "Serious Report") and an argument name (that's Field 2). The problem is that all possible argument names are shown, whereas they need to be constrained based on the value of Field 1.
What filtering / constraining mechanism can we implement such that once we select "Serious Report", we know we will be using the "tabulate_results" method, so that only the "rows" and "display_total" arguments are available?
I'm thinking some AJAX wizardry that checks Field 1 and sets a query for Field 2 values, but have no idea how to proceed.
You can see this by playing with the gist: click on the Recipe Arg tab. In the first row ("Serious Report"), if you try to edit the "Methodarg" value by clicking on it, all four argument names are available, instead of just two.
# full gist: please run this
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///a_sample_database.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create admin app
admin = Admin(app, name="Constrain Values", template_mode='bootstrap3')
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
class Method(db.Model):
__tablename__ = 'method'
mid = Column(Integer, primary_key=True)
method = Column(String(20), nullable=False, unique=True)
methodarg = relationship('MethodArg', backref='method')
recipe = relationship('Recipe', backref='method')
def __str__(self):
return self.method
class MethodArg(db.Model):
__tablename__ = 'methodarg'
maid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
methodarg = Column(String(20), nullable=False, unique=True)
recipearg = relationship('RecipeArg', backref='methodarg')
inline_models = (Method,)
def __str__(self):
return self.methodarg
class Recipe(db.Model):
__tablename__ = 'recipe'
rid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
recipe = Column(String(20), nullable=False, index=True)
recipearg = relationship('RecipeArg', backref='recipe')
inline_models = (Method,)
def __str__(self):
return self.recipe
class RecipeArg(db.Model):
__tablename__ = 'recipearg'
raid = Column(Integer, primary_key=True)
rid = Column(ForeignKey('recipe.rid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
maid = Column(ForeignKey('methodarg.maid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
strvalue = Column(String(80), nullable=False)
inline_models = (Recipe, MethodArg)
def __str__(self):
return self.strvalue
class MethodArgAdmin(sqla.ModelView):
column_list = ('method', 'methodarg')
column_editable_list = column_list
class RecipeAdmin(sqla.ModelView):
column_list = ('recipe', 'method')
column_editable_list = column_list
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
column_editable_list = column_list
admin.add_view(RecipeArgAdmin(RecipeArg, db.session))
# More submenu
admin.add_view(sqla.ModelView(Method, db.session, category='See Other Tables'))
admin.add_view(MethodArgAdmin(MethodArg, db.session, category='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe, db.session, category='See Other Tables'))
if __name__ == '__main__':
db.drop_all()
db.create_all()
db.session.add(Method(mid=1, method='tabulate_results'))
db.session.add(Method(mid=2, method='pretty_print'))
db.session.commit()
db.session.add(MethodArg(maid=1, mid=1, methodarg='rows'))
db.session.add(MethodArg(maid=2, mid=1, methodarg='display_total'))
db.session.add(MethodArg(maid=3, mid=2, methodarg='embellishment_character'))
db.session.add(MethodArg(maid=4, mid=2, methodarg='lines_to_jump'))
db.session.add(Recipe(rid=1, mid=1, recipe='Serious Report'))
db.session.add(Recipe(rid=2, mid=2, recipe='ASCII Art'))
db.session.commit()
db.session.add(RecipeArg(raid=1, rid=1, maid=2, strvalue='true' ))
db.session.add(RecipeArg(raid=2, rid=1, maid=1, strvalue='12' ))
db.session.add(RecipeArg(raid=3, rid=2, maid=4, strvalue='3' ))
db.session.commit()
# Start app
app.run(debug=True)
I see two ways of tacking this problem:
1- When Flask-Admin generate the form, add data attributes with the mid of each methodArg on each option tag in the methodArg select. Then have some JS code filter the option tags based on the recipe selected.
EDIT
Here is a tentative try at putting a data-mid attribute on each option:
def monkeypatched_call(self, field, **kwargs):
kwargs.setdefault('id', field.id)
if self.multiple:
kwargs['multiple'] = True
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
for (val, label, selected), (_, methodarg) in zip(field.iter_choices(), field._get_object_list()):
html.append(self.render_option(val, label, selected, **{'data-mid': methodarg.mid}))
html.append('</select>')
return HTMLString(''.join(html))
Select.__call__ = monkeypatched_call
The blocker is in the fact that those render calls are triggered from the jinja templates, so you are pretty much stuck updating a widget (Select being the most low-level one in WTForms, and is used as a base for Flask-Admin's Select2Field).
After getting those data-mid on each of your options, you can proceed with just binding an change on your recipe's select and display the methodarg's option that have a matching data-mid. Considering Flask-Admin uses select2, you might have to do some JS tweaking (easiest ugly solution would be to clean up the widget and re-create it for each change event triggered)
Overall, I find this one less robust than the second solution. I kept the monkeypatch to make it clear this should not be used in production imho. (the second solution is slightly less intrusive)
2- Use the supported ajax-completion in Flask-Admin to hack your way into getting the options that you want based on the selected recipe:
First, create a custom AjaxModelLoader that will be responsible for executing the right selection query to the DB:
class MethodArgAjaxModelLoader(sqla.ajax.QueryAjaxModelLoader):
def get_list(self, term, offset=0, limit=10):
query = self.session.query(self.model).filter_by(mid=term)
return query.offset(offset).limit(limit).all()
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
form_ajax_refs = {
'methodarg': MethodArgAjaxModelLoader('methodarg', db.session, MethodArg, fields=['methodarg'])
}
column_editable_list = column_list
Then, update Flask-Admin's form.js to get the browser to send you the recipe information instead of the methodArg name that needs to be autocompleted. (or you could send both in query and do some arg parsing in your AjaxLoader since Flask-Admin does no parsing whatsoever on query, expecting it to be a string I suppose [0]. That way, you would keep the auto-completion)
data: function(term, page) {
return {
query: $('#recipe').val(),
offset: (page - 1) * 10,
limit: 10
};
},
This snippet is taken from Flask-Admin's form.js [1]
Obviously, this needs some tweaking and parametrising (because doing such a hacky solution would block you from using other ajax-populated select in the rest of your app admin + the update on form.js directly like that would make upgrading Flask-Admin extremely cumbersome)
Overall, I am unsatisfied with both solutions and this showcase that whenever you want to go out of the tracks of a framework / tool, you can end up in complex dead ends. This might be an interesting feature request / project for someone willing to contribute a real solution upstream to Flask-Admin though.
There is another easy solution that I made and it works
1- Create your first select option normally with data loaded on it and add a hook to it which will add js event listener when it selects change like this.
from wtforms import SelectField
form_extra_fields = {
'streetname': SelectField(
'streetname',
coerce=str,
choices=([street.streetname for street in StreetsMetadata.query.all()]),
render_kw={'onchange': "myFunction()"}
)
}
**2- Add a JavaScript URL file to the view you want to use this function in, for example.
def render(self, template, **kwargs):
#using extra js in render method allow use url_for that itself requires an app context
self.extra_js = [url_for("static", filename="admin/js/users.js")]
response = render_miror(self, template,**kwargs)
return response
3- Create a role-protected endpoint that you used for this view that will accept a GET request from JS based on the first value specified for the entry, for example this route returns a list of house numbers by querying the street name that came from the first entry
#super_admin_permission.require(http_exception=403)
#adminapp.route('/get_houses_numbers')
def gethouses():
request_data = request.args
if request_data and 'street' in request_data:
street = StreetsMetadata.query.filter(StreetsMetadata.streetname == request_data['street']).one_or_none()
street_houses = lambda:giveMeAllHousesList(street.excluded, street.min, street.max)
if street_houses:
return jsonify({'code': 200, 'houses': street_houses()})
else:
return jsonify({'code': 404, 'houses': []})
else:
return jsonify({'code': 400, 'street': []})
now python part completed time for JavaScript
4- We have to define three functions, the first of which will be called when the form build page is loaded and which do two things first,
A dummy select entry will be created using JS and append that entry to the same string input container
Make string entry read-only to improve user experience
Second, it will send a GET request to the specified route to get a list of house numbers using the specified street input value
Then get the result and create the option elements and append these options to the dummy selection, you can also select the first option while appending the options.
5- The second function "myFunction" is the hook defined in Python in this part
render_kw={'onchange': "myFunction()"}
This function will do nothing new, it will only send a GET request when the first specified input value is changed, send a GET request to get a list of new house numbers based on the given street name input value by doing a query on the database, then dump the inner HTML of the dummy selection entry , then create and append new options to it.
6- The last function is the callback function which listens for the change on the dummy select entry created with JS when the user chooses the house number which will be reflected in the main string entry, finally you can click save and you will see it working
Note that this whole idea I created is not as good as the built in flask admin, but if you are looking for the end goal and without any problems you can use it
My JS code
/*
This Function when run when a form included it will create JS select input with the
default loaded streetname and add house number on that select this select will used
to guide creator of the house number or to select the house number
*/
async function onFlaskFormLoad(){
const streetSelect = document.querySelector("#streetname");
const checkIfForm = document.querySelector("form.admin-form");
if (checkIfForm){
let checkSelect = document.querySelector("#realSelect");
if (!checkSelect){
const mySelectBox = document.createElement("select");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
mySelectBox.classList.add("form-control")
mySelectBox.id = "realSelect";
houseStringCont.appendChild(mySelectBox);
mySelectBox.addEventListener("change", customFlaskAdminUnpredefinedSelect);
houseString.setAttribute("readonly", "readonly");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && mySelectBox){
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
mySelectBox.appendChild(newHouse);
});
}
}
}
}
onFlaskFormLoad();
/*
this function will called to change the string input value to my custom js select
value and then use that string to house number which required by flask-admin
*/
function customFlaskAdminUnpredefinedSelect(){
const theSelect = document.querySelector("#realSelect");
const houseString = document.querySelector("#housenumber");
houseString.value = theSelect.value;
return true;
}
/*
flask admin hook that will listen on street input change and then it will send
get request to secured endpoint with role superadmin required and get the housenumbers
using the streetname selected and then create options and add to my select input
*/
async function myFunction(){
const streetSelect = document.querySelector("#streetname");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
const theSelect = document.querySelector("#realSelect");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && theSelect){
theSelect.innerHTML = "";
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
theSelect.appendChild(newHouse);
});
}
}
Now if I change the street name of the first specified input I will get a new list containing the numbers based on the first input value, note if you have a way to create a python field that accepts the non-predefined options then there is no need to create dummy input you can create and append the new options Directly to second select input
final result

Categories

Resources