class ProductGroup(Model): _name = "product.group" _string = "Product Group" _key = ["code"] _fields = { "name": fields.Char("Group Name", required=True, search=True), "code": fields.Char("Group Code", search=True), "parent_id": fields.Many2One("product.group", "Parent"), "products": fields.Many2Many("product", "Products"), "filter_products": fields.Many2Many("product", "Products", function="get_filter_products"), "image": fields.File("Image"), "company_id": fields.Many2One("company", "Company"), } _order = "name" def get_filter_products(self, ids, context={}): group_id = ids[0] cond = [["groups.id", "=", group_id], ["is_published", "=", True]] if context.get("product_filter"): cond.append(context["product_filter"]) prod_ids = get_model("product").search(cond) vals = { group_id: prod_ids, } return vals
class Profile(Model): _name = "profile" _string = "Profile" _key = ["name"] _fields = { "name": fields.Char("Name", required=True, search=True), "code": fields.Char("Short Code"), "perms": fields.One2Many("profile.access", "profile_id", "Model Permissions"), "field_perms": fields.One2Many("field.access", "profile_id", "Field Permissions"), "menu_perms": fields.One2Many("menu.access", "profile_id", "Menu Permissions"), "other_perms": fields.Many2Many("permission", "Other Permissions"), "home_action": fields.Char("Login Action"), "login_company_id": fields.Many2One("company", "Login Company"), "prevent_login": fields.Boolean("Prevent Login"), "comments": fields.One2Many("message", "related_id", "Comments"), "default_model_perms": fields.Selection([["full", "Full Access"], ["readonly","Read-only Access"], ["no", "No Access"]], "Default Model Permissions"), "default_menu_access": fields.Selection([["visible", "Visible"], ["hidden", "Hidden"]], "Default Menu Access"), } _order = "name" _defaults = { "default_model_perms": "full", } def get_data(self, context={}): vals = {} perms = [] for m in get_model("model").search_browse([]): perms.append({ "model_id": [m.id, m.string], }) vals["perms"] = perms return vals def copy(self, ids, context={}): obj = self.browse(ids)[0] vals = { "name": obj.name + " (Copy)", "perms": [], "other_perms": [("set", [p.id for p in obj.other_perms])], "home_action": obj.home_action, } for perm in obj.perms: vals["perms"].append(("create", { "model_id": perm.model_id.id, "perm_read": perm.perm_read, "perm_create": perm.perm_create, "perm_write": perm.perm_write, "perm_delete": perm.perm_delete, "view_all": perm.view_all, "modif_all": perm.modif_all, })) profile_id = get_model("profile").create(vals) return { "next": { "name": "profile", "mode": "form", "active_id": profile_id, }, "flash": "New profile created", }
class ProductBrand(Model): _name = "product.brand" _string = "Brand" _fields = { "name": fields.Char("Name", required=True, search=True), "description": fields.Text("Description", search=True), "image": fields.File("Image"), "code": fields.Char("Code"), "parent_id": fields.Many2One("product.brand","Parent Brand"), "sub_brands": fields.One2Many("product.brand","parent_id","Sub Brands"), "products": fields.One2Many("product","brand_id","Products", operator="child_of"), "num_products": fields.Integer("Number of products", function="get_num_products"), "groups": fields.Many2Many("product.brand.group","Group"), } _order = "name" def get_num_products(self, ids, context={}): vals = {} for obj in self.browse(ids): nums = 0 for product in obj.products: if not product.parent_id: nums += 1 vals[obj.id] = nums return vals
class VariantValue(Model): _name = "product.variant.values" _transient = True _fields = { "popup_id": fields.Many2One("prod.create.variants", "Popup", required=True, on_delete="cascade"), "attribute_id": fields.Many2One("product.attribute", "Attribute", required=True), "values": fields.Many2Many("product.attribute.option", "Values", required=True), }
class Group(Model): _name = "user.group" _string = "Group" _fields = { "name": fields.Char("Group Name", required=True, search=True), "users": fields.Many2Many("base.user", "Users"), "comments": fields.One2Many("message", "related_id", "Comments"), } _order = "name"
class PayItemProfile(Model): _name = "hr.payitem.profile" _string = "Pay Item Profile" _fields = { "name": fields.Char("Name", required=True, search=True), #'lines': fields.One2Many("hr.payitem.profile.line","profile_id","Lines"), 'pay_items': fields.Many2Many("hr.payitem", "Pay Items"), } _defaults = {}
class Leave_type(Model): _name = "hr.leave.type" _string = "Leave Type" _key = ["name"] _fields = { "name": fields.Char("Name", search=True), "description": fields.Text("Description"), "comments": fields.One2Many("message", "related_id", "Comments"), "employees": fields.Many2Many("hr.employee", "Employees"), }
class ProductBrandGroup(Model): _name = "product.brand.group" _string = "Product Brand Group" _key = ["code"] _fields = { "name": fields.Char("Group Name", required=True, search=True), "code": fields.Char("Group Code", search=True), "parent_id": fields.Many2One("product.brand.group", "Parent"), "brands": fields.Many2Many("product.brand", "Product Brands"), "image": fields.File("Image"), "company_id": fields.Many2One("company","Company"), } _order = "name"
class Event(Model): _name = "sale.event" _string = "Event" _key = ["code"] _fields = { "name": fields.Char("Event Name", required=True, search=True), "code": fields.Char("Event Code", required=True, search=True), "products": fields.Many2Many("product", "Products", condition=[["is_published", "=", True]]) }
class PricelistAdd(Model): _name = "pricelist.add" _transient = True _fields = { "pricelist_id": fields.Many2One("price.list", "Price List", required=True, on_delete="cascade"), "product_categs": fields.Many2Many("product.categ", "Product Categories"), } _defaults = { "pricelist_id": lambda self, ctx: ctx["refer_id"], } def add_products(self, ids, context={}): obj = self.browse(ids)[0] pricelist = obj.pricelist_id categ_ids = [c.id for c in obj.product_categs] for prod in get_model("product").search_browse( [["categs.id", "in", categ_ids]]): factor = pricelist.factor or 1.0 if pricelist.base_price == "product": base_price = prod.sale_price or 0 elif pricelist.base_price == "other_pricelist": if not pricelist.other_pricelist_id: raise Exception("Missing base price list") base_price = get_model("price.list").get_price( pricelist.other_pricelist_id.id, prod.id, 1) or 0 # XXX: qty elif pricelist.base_price == "volume": base_price = prod.volume or 0 else: raise Exception("Invalid base price type") price = utils.round_amount(base_price * factor, pricelist.rounding, pricelist.rounding_method) vals = { "list_id": pricelist.id, "product_id": prod.id, "price": price, } get_model("price.list.item").create(vals) return { "next": { "name": "pricelist_item", }, "flash": "Products added to price list", }
class Bom(Model): _name = "bom" _string = "Bill of Material" _name_field = "number" _key = ["number"] _fields = { "number": fields.Char("Number", required=True, search=True), "product_id": fields.Many2One("product", "Product", required=True, search=True), "qty": fields.Decimal("Qty", required=True, scale=6), "uom_id": fields.Many2One("uom", "UoM", required=True), "location_id": fields.Many2One("stock.location", "FG Warehouse"), "routing_id": fields.Many2One("routing", "Routing"), "lines": fields.One2Many("bom.line", "bom_id", "Lines"), "comments": fields.One2Many("message", "related_id", "Comments"), "documents": fields.One2Many("document", "related_id", "Documents"), "max_qty_loss": fields.Decimal("Max Qty Loss", scale=6), "container": fields.Selection([["sale", "From Sales Order"]], "FG Container"), "lot": fields.Selection([["production", "From Production Order"]], "FG Lot"), "qc_tests": fields.Many2Many("qc.test", "QC Tests"), } def _get_number(self, context={}): while 1: num = get_model("sequence").get_number("bom") if not num: return None res = self.search([["number", "=", num]]) if not res: return num get_model("sequence").increment("bom") _defaults = { "number": _get_number, } def onchange_product(self,context={}): data=context['data'] path=context['path'] line=get_data_path(data,path,parent=True) product_id=line['product_id'] if product_id: product=get_model('product').browse(product_id) line['uom_id']=product.uom_id.id return data
class Contact(Model): _inherit = "contact" _fields = { "previous_sale_products": fields.Many2Many("product", "Previously Ordered Products", function="get_previous_sale_products"), } def get_previous_sale_products(self, ids, context={}): db = database.get_connection() res = db.query( "SELECT contact_id,product_id,COUNT(*) FROM sale_order_line l JOIN sale_order o ON o.id=l.order_id and plr_order_type='grocery' WHERE o.contact_id IN %s GROUP BY contact_id,product_id", tuple(ids)) contact_prods = {} for r in res: contact_prods.setdefault(r.contact_id, []).append(r.product_id) vals = {} for obj in self.browse(ids): vals[obj.id] = contact_prods.get(obj.id, [])[:5] # XXX return vals
class Product(Model): _name = "product" _key = ["name"] _order = "name" _name_field = "name" _fields = { "name": fields.Char("Name", required=True), "code": fields.Char("Code"), "description": fields.Text("Description"), "purchase_price": fields.Decimal("Purchase Price"), "sale_price": fields.Decimal("Sale Price"), "tags": fields.Many2Many("tag", "Tags"), "image": fields.File("Image"), "cost_method": fields.Selection( [["standard", "Standard Cost"], ["average", "Weighted Average"], ["fifo", "FIFO"], ["lifo", "LIFO"]], "Costing Method"), "cost_price": fields.Decimal("Cost Price"), "stock_in_account_id": fields.Many2One("account.account", "Stock Input Account"), "stock_out_account_id": fields.Many2One("account.account", "Stock Output Account"), "uuid": fields.Char("UUID"), } _defaults = { "uuid": lambda *a: str(uuid.uuid4()), }
class SaleReturnLine(Model): _name = "sale.return.line" _name_field = "order_id" _fields = { "order_id": fields.Many2One("sale.return", "Sales Return", required=True, on_delete="cascade", search=True), "product_id": fields.Many2One("product", "Product", search=True), "description": fields.Text("Description", required=True, search=True), "qty": fields.Decimal("Qty"), "uom_id": fields.Many2One("uom", "UoM"), "unit_price": fields.Decimal("Unit Price", required=True, search=True, scale=6), "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"), "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "amount_cur": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "qty_stock": fields.Decimal("Qty (Stock UoM)"), "qty_received": fields.Decimal("Received Qty", function="get_qty_received"), "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"), "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}, search=True), "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}, search=True), "user_id": fields.Many2One("base.user", "Owner", function="_get_related", function_search="_search_related", function_context={"path": "order_id.user_id"}, search=True), "amount_discount": fields.Decimal("Discount Amount", function="get_amount", function_multi=True), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}, search=True), "qty2": fields.Decimal("Secondary Qty"), "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]), "product_categs": fields.Many2Many("product.categ", "Product Categories", function="_get_related", function_context={"path": "product_id.categs"}, function_search="_search_related", search=True), "discount": fields.Decimal("Disc %"), # XXX: rename to discount_percent later "discount_amount": fields.Decimal("Disc Amt"), "qty_avail": fields.Decimal("Qty In Stock", function="get_qty_avail"), "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]), "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]), "remark": fields.Char("Remark"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), "sequence": fields.Char("Item No."), "return_type": fields.Selection([["refund", "Refund"], ["exchange", "Exchange"]], "Return Type"), "reason_code_id": fields.Many2One("reason.code", "Reason Code", condition=[["type", "=", "sale_return"]]), } def create(self, vals, context={}): id = super().create(vals, context) self.function_store([id]) return id def write(self, ids, vals, context={}): super().write(ids, vals, context) self.function_store(ids) def get_amount(self, ids, context={}): vals = {} settings = get_model("settings").browse(1) sale_ids = [] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids = list(set(sale_ids)) for sale in get_model("sale.return").browse(sale_ids): prod_qtys = {} for line in sale.lines: prod_qtys.setdefault(line.product_id.id, 0) prod_qtys[line.product_id.id] += line.qty for line in sale.lines: amt = line.qty * line.unit_price if line.discount: disc = amt * line.discount / 100 else: disc = 0 if line.discount_amount: disc += line.discount_amount amt -= disc order = line.order_id vals[line.id] = { "amount": amt, "amount_cur": get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id), "amount_discount": disc, } return vals def get_qty_received(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.return").browse(order_ids): received_qtys = {} for move in order.stock_moves: if move.state != "done": continue prod_id = move.product_id.id k = (prod_id, move.location_to_id.id) received_qtys.setdefault(k, 0) received_qtys[k] += move.qty # XXX: uom k = (prod_id, move.location_from_id.id) received_qtys.setdefault(k, 0) received_qtys[k] -= move.qty # XXX: uom for line in order.lines: k = (line.product_id.id, line.location_id.id) received_qty = received_qtys.get(k, 0) # XXX: uom used_qty = min(line.qty, received_qty) vals[line.id] = used_qty if k in received_qtys: received_qtys[k] -= used_qty for line in reversed(order.lines): k = (line.product_id.id, line.location_id.id) remain_qty = received_qtys.get(k, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty received_qtys[k] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_invoiced(self, ids, context={}): vals = {} for obj in self.browse(ids): order = obj.order_id qty = 0 for inv in order.invoices: if inv.state == "voided": continue for line in inv.lines: if obj.product_id: if line.product_id.id != obj.product_id.id: continue else: if line.product_id or line.description != obj.description: continue if inv.type == "out": qty += line.qty # XXX: uom elif inv.type == "in": qty -= line.qty # XXX: uom vals[obj.id] = qty return vals def get_qty_avail(self, ids, context={}): vals = {} for obj in self.browse(ids): prod_id = obj.product_id.id loc_id = obj.location_id.id if prod_id and loc_id: res = get_model("stock.location").compute_balance([loc_id], prod_id) qty = res["bal_qty"] else: qty = None vals[obj.id] = qty return vals
class ShipMethod(Model): _name = "ship.method" _string = "Shipping Method" _key = ["code"] _fields = { "name": fields.Char("Name", required=True, search=True), "code": fields.Char("Code", search=True), "rates": fields.One2Many("ship.rate", "method_id", "Shipping Rates"), "comments": fields.One2Many("message", "related_id", "Comments"), "sequence": fields.Integer("Sequence", required=True), "type": fields.Selection([], "Type"), "ship_product_id": fields.Many2One("product", "Shipping Product"), "exclude_ship_methods": fields.Many2Many("ship.method", "Exclude Shipping Methods", reltable="m2m_ship_method_exclude", relfield="method1_id", relfield_other="method2_id"), "active": fields.Boolean("Active"), "ship_amount": fields.Decimal("Shipping Amount", function="get_ship_amount"), } _order = "sequence" _defaults = { "active": True, } def create_delivery_order(self, ids, context={}): pass def get_ship_amount(self, ids, context={}): vals = {} contact_id = context.get("contact_id") contact = get_model("contact").browse( contact_id) if contact_id else None addr_id = context.get("ship_address_id") if addr_id: addr = get_model("address").browse(addr_id) else: addr = None order_amount = context.get("order_amount") order_weight = context.get("order_weight") for obj in self.browse(ids): if contact and contact.ship_free: vals[obj.id] = 0 continue amt = None for rate in obj.rates: #print("try rate",rate.id) if rate.country_id and ( not addr or addr.country_id.id != rate.country_id.id): #print(" skip country") continue if rate.province_id and (not addr or addr.province_id.id != rate.province_id.id): #print(" skip province (%s / %s %s)"%(rate.province_id.id,addr.id,addr.province_id.id if addr else None)) continue if rate.district_id and (not addr or addr.district_id.id != rate.district_id.id): #print(" skip district") continue if rate.postal_code and (not addr or addr.postal_code != rate.postal_code): #print(" skip postal code") continue if rate.address_name and (not addr or addr.name != rate.address_name): #print(" skip address name") continue if rate.min_amount and (order_amount is None or order_amount < rate.min_amount): #print(" skip min amount") continue if rate.min_weight and (order_weight is None or order_weight < rate.min_weight): #print(" skip min weight") continue #print(" OK ship_price=%s"%rate.ship_price) if amt is None or rate.ship_price < amt: amt = rate.ship_price #if amt is not None: #print("=> shipping price found: %s"%amt) #else: #print("=> no shipping price found") vals[obj.id] = amt return vals
class SaleOrderLine(Model): _name = "sale.order.line" _name_field = "order_id" _fields = { "order_id": fields.Many2One("sale.order", "Sales Order", required=True, on_delete="cascade", search=True), "product_id": fields.Many2One("product", "Product", search=True), "description": fields.Text("Description", required=True, search=True), "qty": fields.Decimal("Qty"), "uom_id": fields.Many2One("uom", "UoM"), "unit_price": fields.Decimal("Unit Price", search=True, required=True, scale=6), "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"), "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "amount_cur": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "qty_stock": fields.Decimal("Qty (Stock UoM)"), "qty_delivered": fields.Decimal("Delivered Qty", function="get_qty_delivered"), "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"), "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}, search=True), "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}, search=True), "user_id": fields.Many2One("base.user", "Owner", function="_get_related", function_search="_search_related", function_context={"path": "order_id.user_id"}, search=True), "amount_discount": fields.Decimal("Discount Amount", function="get_amount", function_multi=True), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}, search=True), "qty2": fields.Decimal("Secondary Qty"), "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]), "product_categs": fields.Many2Many("product.categ", "Product Categories", function="_get_related", function_context={"path": "product_id.categs"}, function_search="_search_related", search=True), "product_categ_id": fields.Many2Many("product.categ", "Product Category", function="_get_related", function_context={"path": "product_id.categ_id"}, function_search="_search_related", search=True), "discount": fields.Decimal("Disc %"), # XXX: rename to discount_percent later "discount_amount": fields.Decimal("Disc Amt"), "qty_avail": fields.Decimal("Qty In Stock", function="get_qty_avail"), "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]), "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]), "remark": fields.Char("Remark"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), "sequence": fields.Char("Item No."), "due_date": fields.Date("Due Date"), "est_cost_amount": fields.Float("Est. Cost Amount",function="get_est_profit",function_multi=True), "est_profit_amount": fields.Float("Est. Profit Amount",function="get_est_profit",function_multi=True), "est_margin_percent": fields.Float("Est. Margin %",function="get_est_profit",function_multi=True), "act_cost_amount": fields.Float("Act. Cost Amount",function="get_act_profit",function_multi=True), "act_profit_amount": fields.Float("Act. Profit Amount",function="get_act_profit",function_multi=True,store=True), "act_margin_percent": fields.Float("Act. Margin %",function="get_act_profit",function_multi=True), "promotion_amount": fields.Decimal("Prom Amt",function="get_amount",function_multi=True), "agg_act_profit": fields.Decimal("Total Actual Profit", agg_function=["sum", "act_profit_amount"]), "production_id": fields.Many2One("production.order","Production Order"), "lot_id": fields.Many2One("stock.lot","Lot / Serial Number"), "ship_address_id": fields.Many2One("address", "Shipping Address"), "packaging_id": fields.Many2One("stock.packaging", "Packaging"), "delivery_slot_id": fields.Many2One("delivery.slot","Delivery Slot"), "ship_tracking": fields.Char("Tracking Numbers", function="get_ship_tracking"), } def create(self, vals, context={}): id = super(SaleOrderLine, self).create(vals, context) self.function_store([id]) return id def write(self, ids, vals, context={}): super(SaleOrderLine, self).write(ids, vals, context) self.function_store(ids) def get_amount(self, ids, context={}): vals = {} settings = get_model("settings").browse(1) sale_ids=[] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) for sale in get_model("sale.order").browse(sale_ids): prod_qtys={} prom_amts={} prom_pcts={} for line in sale.lines: prod_qtys.setdefault(line.product_id.id,0) prod_qtys[line.product_id.id]+=line.qty for line in sale.used_promotions: if line.amount and line.product_id: prom_amts.setdefault(line.product_id.id,0) prom_amts[line.product_id.id]+=line.amount elif line.percent: prom_pcts.setdefault(line.product_id.id,0) prom_pcts[line.product_id.id]+=line.percent for line in sale.lines: amt = line.qty * line.unit_price if line.discount: disc = amt * line.discount / 100 else: disc = 0 if line.discount_amount: disc += line.discount_amount amt-=disc amt_before_prom=amt prom_amt=prom_amts.get(line.product_id.id,Decimal(0))/prod_qtys[line.product_id.id]*line.qty prom_pct=prom_pcts.get(line.product_id.id,Decimal(0))+prom_pcts.get(None,0) if prom_pct: prom_amt+=math.ceil(amt_before_prom/line.qty*prom_pct/100)*line.qty if prom_amt: amt-=prom_amt order = line.order_id vals[line.id] = { "amount": amt, "amount_cur": get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id), "amount_discount": disc, "promotion_amount": prom_amt, } return vals def get_qty_delivered(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): delivered_qtys = {} for move in order.stock_moves: if move.state != "done": continue prod_id = move.product_id.id k = (prod_id, move.location_from_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] += move.qty # XXX: uom k = (prod_id, move.location_to_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] -= move.qty # XXX: uom for line in order.lines: k = (line.product_id.id, line.location_id.id) delivered_qty = delivered_qtys.get(k, 0) # XXX: uom used_qty = min(line.qty, delivered_qty) vals[line.id] = used_qty if k in delivered_qtys: delivered_qtys[k] -= used_qty for line in reversed(order.lines): k = (line.product_id.id, line.location_id.id) remain_qty = delivered_qtys.get(k, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty delivered_qtys[k] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_invoiced(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): inv_qtys = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.product_id.id inv_qtys.setdefault(prod_id, 0) inv_qtys[prod_id] += line.qty for line in order.lines: if line.id not in ids: continue prod_id = line.product_id.id inv_qty = inv_qtys.get(prod_id, 0) # XXX: uom used_qty = min(line.qty, inv_qty) vals[line.id] = used_qty if prod_id in inv_qtys: inv_qtys[prod_id] -= used_qty for line in reversed(order.lines): prod_id = line.product_id.id remain_qty = inv_qtys.get(prod_id, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty inv_qtys[prod_id] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_avail(self, ids, context={}): vals = {} for obj in self.browse(ids): prod_id = obj.product_id.id loc_id = obj.location_id.id if prod_id and loc_id: res = get_model("stock.location").compute_balance([loc_id], prod_id) qty = res["bal_qty"] else: qty = None vals[obj.id] = qty return vals def get_est_profit(self,ids,context={}): sale_ids=[] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) item_costs={} for sale in get_model("sale.order").browse(sale_ids): for cost in sale.est_costs: k=(sale.id,cost.sequence) if k not in item_costs: item_costs[k]=0 amt=cost.amount or 0 if cost.currency_id: rate=sale.get_relative_currency_rate(cost.currency_id.id) amt=amt*rate item_costs[k]+=amt vals={} for line in self.browse(ids): k=(line.order_id.id,line.sequence) cost=item_costs.get(k,0) profit=line.amount-cost margin=profit*100/line.amount if line.amount else None vals[line.id]={ "est_cost_amount": cost, "est_profit_amount": profit, "est_margin_percent": margin, } return vals def get_act_profit(self,ids,context={}): sale_ids=[] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) item_costs={} for sale in get_model("sale.order").browse(sale_ids): for line in sale.track_entries: k=(sale.id,line.track_id.code) if k not in item_costs: item_costs[k]=0 # TODO: convert currency item_costs[k]-=line.amount vals={} for line in self.browse(ids): track_code="%s / %s"%(line.order_id.number,line.sequence) k=(line.order_id.id,track_code) cost=item_costs.get(k,0) profit=line.amount-cost margin=profit*100/line.amount if line.amount else None vals[line.id]={ "act_cost_amount": cost, "act_profit_amount": profit, "act_margin_percent": margin, } return vals def get_ship_tracking(self, ids, context={}): vals = {} for obj in self.browse(ids): track_nos = [] if obj.due_date: for pick in obj.order_id.pickings: if pick.date[:10]==obj.due_date and pick.ship_tracking: track_nos.append(pick.ship_tracking) vals[obj.id] = ", ".join(track_nos) return vals
class SaleOrderLine(Model): _name = "sale.order.line" _name_field = "order_id" _fields = { "order_id": fields.Many2One("sale.order", "Sales Order", required=True, on_delete="cascade", search=True), "product_id": fields.Many2One("product", "Product", search=True), "description": fields.Text("Description", required=True, search=True), "qty": fields.Decimal("Qty"), "uom_id": fields.Many2One("uom", "UoM"), "unit_price": fields.Decimal("Unit Price", search=True, required=True, scale=6), "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"), "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "amount_cur": fields.Decimal("Amount (Cur)", function="get_amount", function_multi=True, store=True, function_order=1, search=True), "qty_stock": fields.Decimal("Qty (Stock UoM)"), "qty_delivered": fields.Decimal("Delivered Qty", function="get_qty_delivered"), "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"), "unit_amount_invoiced": fields.Decimal("UNIT amount invoiced", function="get_unit_amount_invoiced"), "qty_service_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_service_invoiced"), "unit_service_amount_invoiced": fields.Decimal("service unit amount invoiced", function="get_service_unit_amount_invoiced"), "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}, search=True), "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}, search=True), "user_id": fields.Many2One("base.user", "Owner", function="_get_related", function_search="_search_related", function_context={"path": "order_id.user_id"}, search=True), "amount_discount": fields.Decimal("Discount Amount", function="get_amount", function_multi=True), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}, search=True), "qty2": fields.Decimal("Secondary Qty"), "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]), "product_categ_id": fields.Many2Many("product.categ", "Product Category", function="_get_related", function_context={"path": "product_id.categ_id"}, function_search="_search_related", search=True), "discount": fields.Decimal("Disc %"), # XXX: rename to discount_percent later "discount_amount": fields.Decimal("Disc Amt"), "qty_avail": fields.Decimal("Qty In Stock", function="get_qty_avail"), "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]), "agg_amount_cur": fields.Decimal("Total Amount Cur", agg_function=["sum", "amount_cur"]), "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]), "remark": fields.Char("Remark"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), "sequence": fields.Char("Item No."), "est_cost_amount": fields.Float("Est. Cost Amount",function="get_est_profit",function_multi=True), "est_profit_amount": fields.Float("Est. Profit Amount",function="get_est_profit",function_multi=True), "est_margin_percent": fields.Float("Est. Margin %",function="get_est_profit",function_multi=True), "act_cost_amount": fields.Float("Act. Cost Amount",function="get_act_profit",function_multi=True), "act_profit_amount": fields.Float("Act. Profit Amount",function="get_act_profit",function_multi=True), "act_margin_percent": fields.Float("Act. Margin %",function="get_act_profit",function_multi=True), "promotion_amount": fields.Decimal("Prom Amt",function="get_amount",function_multi=True), "agg_act_profit": fields.Decimal("Total Actual Profit", agg_function=["sum", "act_profit_amount"]), "production_id": fields.Many2One("production.order","Production Order"), } _order_expression="case when tbl0.sequence is not null then (substring(tbl0.sequence, '^[0-9]+'))::int else tbl0.id end, tbl0.sequence" def get_service_unit_amount_invoiced(self, ids, context={}): #this will get qty based on the description order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): inv_amounts = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.description # assume description as the product and fitler inv_amounts.setdefault(prod_id, 0) inv_amounts[prod_id] += line.amount or 0 for line in order.lines: if line.id not in ids: continue prod_id = line.description inv_amount = inv_amounts.get(prod_id, 0) # XXX: uom used_amount = min(line.amount, inv_amount) vals[line.id] = used_amount if prod_id in inv_amounts: inv_amounts[prod_id] -= used_amount for line in reversed(order.lines): prod_id = line.description remain_amount = inv_amounts.get(prod_id, 0) # XXX: uom if remain_amount: vals[line.id] += remain_amount inv_amounts[prod_id] -= remain_amount vals = {x: vals[x] for x in ids} return vals def get_unit_amount_invoiced(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): inv_amounts = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.product_id.id inv_amounts.setdefault(prod_id, 0) inv_amounts[prod_id] += line.amount or 0 for line in order.lines: if line.id not in ids: continue prod_id = line.product_id.id inv_amount = inv_amounts.get(prod_id, 0) # XXX: uom used_amount = min(line.amount, inv_amount) vals[line.id] = used_amount if prod_id in inv_amounts: inv_amounts[prod_id] -= used_amount for line in reversed(order.lines): prod_id = line.product_id.id remain_amount = inv_amounts.get(prod_id, 0) # XXX: uom if remain_amount: vals[line.id] += remain_amount inv_amounts[prod_id] -= remain_amount vals = {x: vals[x] for x in ids} return vals def get_qty_service_invoiced(self, ids, context={}): #this will get qty based on the description order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): inv_qtys = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.description # assume description as the product and fitler inv_qtys.setdefault(prod_id, 0) inv_qtys[prod_id] += line.qty or 0 for line in order.lines: if line.id not in ids: continue prod_id = line.description inv_qty = inv_qtys.get(prod_id, 0) # XXX: uom used_qty = min(line.qty, inv_qty) vals[line.id] = used_qty if prod_id in inv_qtys: inv_qtys[prod_id] -= used_qty for line in reversed(order.lines): prod_id = line.description remain_qty = inv_qtys.get(prod_id, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty inv_qtys[prod_id] -= remain_qty vals = {x: vals[x] for x in ids} return vals def create(self, vals, context={}): if not vals.get('qty_stock'): product_id=vals.get('product_id') location_id=vals.get('location_id') if product_id and location_id: vals['qty_stock']=get_model('stock.balance').get_qty_stock(product_id, location_id) id = super(SaleOrderLine, self).create(vals, context) self.function_store([id]) return id def write(self, ids, vals, context={}): super(SaleOrderLine, self).write(ids, vals, context) self.function_store(ids) def get_amount(self, ids, context={}): vals = {} settings = get_model("settings").browse(1) sale_ids=[] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) for sale in get_model("sale.order").browse(sale_ids): prod_qtys={} prom_amts={} prom_pcts={} for line in sale.lines: prod_qtys.setdefault(line.product_id.id,0) prod_qtys[line.product_id.id]+=line.qty for line in sale.used_promotions: if line.amount and line.product_id: prom_amts.setdefault(line.product_id.id,0) prom_amts[line.product_id.id]+=line.amount elif line.percent: prom_pcts.setdefault(line.product_id.id,0) prom_pcts[line.product_id.id]+=line.percent for line in sale.lines: amt = line.qty * line.unit_price amt = roundup(amt) if line.discount: disc = amt * line.discount / 100 else: disc = 0 if line.discount_amount: disc += line.discount_amount amt-=disc amt_before_prom=amt prom_amt=prom_amts.get(line.product_id.id,Decimal(0))/prod_qtys[line.product_id.id]*line.qty prom_pct=prom_pcts.get(line.product_id.id,Decimal(0))+prom_pcts.get(None,0) if prom_pct: prom_amt+=math.ceil(amt_before_prom/line.qty*prom_pct/100)*line.qty if prom_amt: amt-=prom_amt order = line.order_id new_cur=get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id, rate_type="sell", date=sale.date) vals[line.id] = { "amount": roundup(amt), "amount_discount": disc, "promotion_amount": prom_amt, "amount_cur": new_cur and new_cur or None, } return vals def get_qty_delivered(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): delivered_qtys = {} for move in order.stock_moves: if move.state != "done": continue prod_id = move.product_id.id k = (prod_id, move.location_from_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] += move.qty # XXX: uom k = (prod_id, move.location_to_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] -= move.qty # XXX: uom for job in order.jobs: for move in job.stock_moves: if move.state != "done": continue prod_id = move.product_id.id k = (prod_id, move.location_from_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] += move.qty # XXX: uom k = (prod_id, move.location_to_id.id) delivered_qtys.setdefault(k, 0) delivered_qtys[k] -= move.qty # XXX: uom for line in order.lines: k = (line.product_id.id, line.location_id.id) delivered_qty = delivered_qtys.get(k, 0) # XXX: uom used_qty = min(line.qty, delivered_qty) vals[line.id] = used_qty if k in delivered_qtys: delivered_qtys[k] -= used_qty for line in reversed(order.lines): k = (line.product_id.id, line.location_id.id) remain_qty = delivered_qtys.get(k, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty delivered_qtys[k] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_invoiced(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("sale.order").browse(order_ids): inv_qtys = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.product_id.id inv_qtys.setdefault(prod_id, 0) inv_qtys[prod_id] += line.qty or 0 for job in order.jobs: for inv in job.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.product_id.id inv_qtys.setdefault(prod_id, 0) inv_qtys[prod_id] += line.qty or 0 for line in order.lines: if line.id not in ids: continue prod_id = line.product_id.id inv_qty = inv_qtys.get(prod_id, 0) # XXX: uom used_qty = min(line.qty, inv_qty) vals[line.id] = used_qty if prod_id in inv_qtys: inv_qtys[prod_id] -= used_qty for line in reversed(order.lines): prod_id = line.product_id.id remain_qty = inv_qtys.get(prod_id, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty inv_qtys[prod_id] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_avail(self, ids, context={}): vals = {} for obj in self.browse(ids): prod_id = obj.product_id.id loc_id = obj.location_id.id if prod_id and loc_id: res = get_model("stock.location").compute_balance([loc_id], prod_id) qty = res["bal_qty"] else: qty = None vals[obj.id] = qty return vals def get_est_profit(self,ids,context={}): sale_ids=[] settings = get_model('settings').browse(1) currency_id = settings.currency_id.id for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) item_costs={} for sale in get_model("sale.order").browse(sale_ids): for cost in sale.est_costs: k=(sale.id,cost.sequence) if k not in item_costs: item_costs[k]=0 if cost.currency_id == currency_id: item_costs[k]+=cost.amount or 0 else: item_costs[k]+=cost.amount_conv or 0 vals={} for line in self.browse(ids): profit=0 k=(line.order_id.id,line.sequence) cost=item_costs.get(k,0) profit=line.amount-cost margin=(profit/line.amount)*100 if line.amount else 0 vals[line.id]={ "est_cost_amount": cost, "est_profit_amount": profit, "est_margin_percent": margin, } return vals def get_act_profit(self,ids,context={}): sale_ids=[] for line in self.browse(ids): sale_ids.append(line.order_id.id) sale_ids=list(set(sale_ids)) item_costs={} for sale in get_model("sale.order").browse(sale_ids): for line in sale.track_entries: k=(sale.id,line.track_id.code) if k not in item_costs: item_costs[k]=0 # TODO: convert currency item_costs[k]-=line.amount vals={} for line in self.browse(ids): track_code="%s / %s"%(line.order_id.number,line.sequence) k=(line.order_id.id,track_code) cost=item_costs.get(k,0) profit=line.amount-cost margin=profit*100/line.amount if line.amount else None vals[line.id]={ "act_cost_amount": cost, "act_profit_amount": profit, "act_margin_percent": margin, } return vals
class MoveLine(Model): _name = "account.move.line" _name_field = "move_id" #_key = ["move_id", "sequence"] _fields = { "move_id": fields.Many2One("account.move", "Journal Entry", required=True, on_delete="cascade"), "description": fields.Text("Description", required=True), "account_id": fields.Many2One("account.account", "Account", required=True, search=True), "debit": fields.Decimal("Debit", required=True), "credit": fields.Decimal("Credit", required=True), "statement_line_id": fields.Many2One("account.statement.line", "Statement Line"), # XXX: not used any more # XXX: simplify this... "statement_line_search": fields.Many2One("account.statement.line", "Statement Line", store=False, function_search="_search_statement_line"), "state": fields.Selection([["not_reconciled", "Not Reconciled"], ["reconciled", "Reconciled"]], "Status"), # XXX: remove store? "move_date": fields.Date("Date", function="_get_related", function_context={"path": "move_id.date"}, store=True, search=True), "move_state": fields.Selection( [["draft", "Draft"], ["posted", "Posted"], ["voided", "Voided"]], "Status", function="_get_related", function_context={"path": "move_id.state"}, store=True), "move_narration": fields.Char("Narration", function="_get_related", function_context={"path": "move_id.narration"}), "move_type": fields.Selection([["invoice", "Invoice"], ["payment", "Payment"], ["transfer", "Transfer"], ["picking", "Picking"], ["stock_count", "Stock Count"], ["claim", "Claim"], ["manual", "Manual"]], "Type", function="_get_related", function_context={"path": "move_id.type"}), "move_ref": fields.Char("Reference", function="_get_related", function_context={"path": "move_id.ref"}), "move_number": fields.Char("Number", function="_get_related", function_context={"path": "move_id.number"}, function_search="_search_related", search=True), "account_name": fields.Char("Account Name", function="_get_related", function_context={"path": "account_id.name"}), "account_code": fields.Char("Account Code", function="_get_related", function_context={"path": "account_id.code"}), "stock_move_id": fields.Many2One("stock.move", "Stock Move"), "product_id": fields.Many2One("product", "Product"), "tax_comp_id": fields.Many2One("account.tax.component", "Tax Comp.", on_delete="restrict"), "tax_base": fields.Decimal("Tax Base"), "contact_id": fields.Many2One("contact", "Contact", search=True), "due_date": fields.Date("Due Date"), "track_id": fields.Many2One("account.track.categ", "Track-1", condition=[["type", "=", "1"]]), "track2_id": fields.Many2One("account.track.categ", "Track-2", condition=[["type", "=", "2"]]), "invoice_id": fields.Many2One("account.invoice", "Invoice"), "qty": fields.Decimal("Qty"), "reconcile_id": fields.Many2One("account.reconcile", "Reconciliation"), "bank_reconcile_id": fields.Many2One("account.bank.reconcile", "Bank Reconciliation"), "statement_lines": fields.Many2Many("account.statement.line", "Statement Lines"), "is_account_reconciled": fields.Boolean("Is Reconciled?", function="_is_account_reconciled"), "empty_contact": fields.Boolean("Empty Contact", store=False, function_search="_search_empty_contact", search=True), "tax_no": fields.Char("Tax No."), "tax_date": fields.Date("Tax Invoice Date"), "sequence": fields.Integer("Sequence"), "amount_cur": fields.Decimal("Currency Amt"), } _defaults = { "state": "not_reconciled", "debit": 0, "credit": 0, } _indexes = [ ("account_id", "move_date"), ] _order = "sequence,id" def view_transaction(self, ids, context={}): obj = self.browse(ids)[0] res = obj.move_id.view_journal() return res def unreconcile(self, ids, context={}): st_line_ids = [] for obj in self.browse(ids): for st_line in obj.statement_lines: st_line_ids.append(st_line.id) st_line_ids = list(set(st_line_ids)) get_model("account.statement.line").unreconcile(st_line_ids) self.write(ids, {"state": "not_reconciled"}) def reconcile(self, ids, context={}): print("MoveLine.reconcile", ids) rec_id = get_model("account.reconcile").create({}) all_ids = ids[:] for line in self.browse(ids): rec = line.reconcile_id if not rec: continue for rline in rec.lines: all_ids.append(rline.id) all_ids = list(set(all_ids)) acc_id = None for obj in self.browse(all_ids): if not acc_id: acc_id = obj.account_id.id else: if obj.account_id.id != acc_id: acc = get_model("account.account").browse(acc_id) raise Exception( "Can only reconcile transactions of same account (%s / %s)" % (obj.account_id.code, acc.code)) self.write(all_ids, {"reconcile_id": rec_id}) inv_ids = [] for obj in self.browse(all_ids): move = obj.move_id rel = move.related_id if rel._model == "account.invoice": inv_ids.append(rel.id) if inv_ids: get_model("account.invoice").function_store(inv_ids) def unreconcile_manual(self, ids, context={}): all_ids = ids[:] for line in self.browse(ids): rec = line.reconcile_id if not rec: continue for rline in rec.lines: all_ids.append(rline.id) all_ids = list(set(all_ids)) self.write(all_ids, {"reconcile_id": None}) def write(self, ids, vals, **kw): rec_ids = [] for obj in self.browse(ids): if obj.reconcile_id: rec_ids.append(obj.reconcile_id.id) super().write(ids, vals, **kw) for obj in self.browse(ids): if obj.reconcile_id: rec_ids.append(obj.reconcile_id.id) if rec_ids: rec_ids = list(set(rec_ids)) get_model("account.reconcile").function_store(rec_ids) def delete(self, ids, **kw): rec_ids = [] for obj in self.browse(ids): if obj.reconcile_id: rec_ids.append(obj.reconcile_id.id) super().delete(ids, **kw) if rec_ids: rec_ids = list(set(rec_ids)) get_model("account.reconcile").function_store(rec_ids) def _is_account_reconciled(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.reconcile_id: vals[obj.id] = abs(obj.reconcile_id.balance) == 0 else: vals[obj.id] = False return vals def _search_empty_contact(self, clause, context={}): if clause[2]: return [["contact_id", "=", None]] else: return [["contact_id", "!=", None]] def reconcile_remove_from_all(self, ids, context={}): print("reconcile_remove_from_all", ids) self.write(ids, {"statement_lines": [("set", [])]})
class PurchaseOrder(Model): _name = "purchase.order" _string = "Purchase Order" _audit_log = True _name_field = "number" _multi_company = True _key = ["company_id", "number"] _fields = { "number": fields.Char("Number", required=True, search=True), "ref": fields.Char("Ref", search=True), "contact_id": fields.Many2One("contact", "Supplier", required=True, search=True), "customer_id": fields.Many2One("contact", "Customer", search=True), "date": fields.Date("Date", required=True, search=True), "date_required": fields.Date("Required Date"), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", required=True, search=True), "lines": fields.One2Many("purchase.order.line", "order_id", "Lines"), "amount_subtotal": fields.Decimal("Subtotal", function="get_amount", function_multi=True, store=True), "amount_tax": fields.Decimal("Tax Amount", function="get_amount", function_multi=True, store=True), "amount_total": fields.Decimal("Total", function="get_amount", function_multi=True, store=True), "amount_total_cur": fields.Decimal("Total", function="get_amount", function_multi=True, store=True), "amount_total_words": fields.Char("Total Words", function="get_amount_total_words"), "qty_total": fields.Decimal("Total Quantity", function="get_qty_total"), "currency_id": fields.Many2One("currency", "Currency", required=True), "tax_type": fields.Selection([["tax_ex", "Tax Exclusive"], ["tax_in", "Tax Inclusive"], ["no_tax", "No Tax"]], "Tax Type", required=True), "invoice_lines": fields.One2Many("account.invoice.line", "purch_id", "Invoice Lines"), #"stock_moves": fields.One2Many("stock.move","purch_id","Stock Moves"), "invoices": fields.One2Many("account.invoice", "related_id", "Invoices"), "pickings": fields.Many2Many("stock.picking", "Stock Pickings", function="get_pickings"), "is_delivered": fields.Boolean("Delivered", function="get_delivered"), "is_paid": fields.Boolean("Paid", function="get_paid"), "invoice_status": fields.Char("Invoice", function="get_invoice_status"), "comments": fields.One2Many("message", "related_id", "Comments"), "delivery_date": fields.Date("Delivery Date"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), # XXX: deprecated "payment_terms": fields.Text("Payment Terms"), "ship_term_id": fields.Many2One("ship.term", "Shipping Terms"), "price_list_id": fields.Many2One("price.list", "Price List", condition=[["type", "=", "purchase"]]), "documents": fields.One2Many("document", "related_id", "Documents"), "company_id": fields.Many2One("company", "Company"), "purchase_type_id": fields.Many2One("purchase.type", "Purchase Type"), "other_info": fields.Text("Other Info"), "bill_address_id": fields.Many2One("address", "Billing Address"), "ship_address_id": fields.Many2One("address", "Shipping Address"), "sequence_id": fields.Many2One("sequence", "Number Sequence"), "stock_moves": fields.One2Many("stock.move", "related_id", "Stock Movements"), "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum_line", "qty", "order_id", "purchase_order_line"]), "agg_amount_total": fields.Decimal("Total Amount", agg_function=["sum", "amount_total"]), "year": fields.Char("Year", sql_function=["year", "date"]), "quarter": fields.Char("Quarter", sql_function=["quarter", "date"]), "month": fields.Char("Month", sql_function=["month", "date"]), "week": fields.Char("Week", sql_function=["week", "date"]), "agg_amount_subtotal": fields.Decimal("Total Amount w/o Tax", agg_function=["sum", "amount_subtotal"]), "user_id": fields.Many2One("base.user", "Owner", search=True), "emails": fields.One2Many("email.message", "related_id", "Emails"), "product_id": fields.Many2One("product", "Product", store=False, function_search="search_product", search=True), "currency_rates": fields.One2Many("custom.currency.rate","related_id","Currency Rates"), "related_id": fields.Reference([],"Related To"), "location_id": fields.Many2One("stock.location", "Location", function_search="search_location", search=True, store=False), } _order = "date desc,number desc" _constraints = ["check_fields"] _sql_constraints = [ ("key_uniq", "unique (company_id, number)", "The number of each company must be unique!") ] def check_fields(self, ids, context={}): for obj in self.browse(ids): if context.get('is_draft'): continue dup=None sequence_item = [] for line in obj.lines: if line.sequence: sequence_item.append(line.sequence) dup_sequence = set() for i in sequence_item: if sequence_item.count(i)>=2: dup_sequence.add(i) dup=True if dup: raise Exception("Lines: Fields Item No. Duplicates : %s" % (str(list(dup_sequence)))) def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence(type="purchase_order") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id,context=context) user_id = get_active_user() set_active_user(1) res = self.search([["number", "=", num]]) set_active_user(user_id) if not res: return num get_model("sequence").increment_number(seq_id,context=context) def _get_currency(self, context={}): settings = get_model("settings").browse(1) return settings.currency_id.id def _get_currency_rates(self,context={}): settings = get_model("settings").browse(1) lines=[] date = time.strftime("%Y-%m-%d") line_vals={ "currency_id": settings.currency_id.id, "rate": settings.currency_id and settings.currency_id.get_rate(date,"buy") or 1 } if context.get("is_create"): lines.append(('create',line_vals)) else: lines.append(line_vals) return lines _defaults = { "state": "draft", "date": lambda *a: time.strftime("%Y-%m-%d"), "number": _get_number, "currency_id": _get_currency, "tax_type": "tax_ex", "company_id": lambda *a: get_active_company(), "user_id": lambda *a: get_active_user(), "currency_rates": _get_currency_rates, } def get_currency_rate(self,context={}): data=context['data'] currency_id = data["currency_id"] currency_rate=0 for cr_rate in data['currency_rates']: if cr_rate['currency_id']==currency_id: currency_rate=cr_rate['rate'] or 0 break if not currency_rate: currency=get_model("currency").browse(currency_id) currency_rate=currency.get_rate(date=data['date'],rate_type="buy") or 1 return currency_rate def onchange_currency(self, context): data=context['data'] currency_id = data["currency_id"] currency=get_model("currency").browse(currency_id) rate=currency.get_rate(date=data['date'],rate_type="buy") or 1 for crr in data['currency_rates']: crr.update({ 'currency_id': currency_id, 'rate': rate, }) break data = self.update_line_currency(context) return data def onchange_pricelist(self, context): data=context['data'] data = self.update_line_currency(context) return data def update_line_currency(self, context): settings=get_model("settings").browse(1) data=context['data'] currency_id = data["currency_id"] pricelist_id = data["price_list_id"] for line in data['lines']: prod_cur_id = None price_list = 0 prod_id=line.get('product_id') if not prod_id: continue prod = get_model("product").browse(prod_id) qty = line.get('qty') or 0 if pricelist_id: price_list = get_model("price.list").get_price(pricelist_id, prod.id, qty) prod_cur_id = get_model("price.list").browse(pricelist_id).currency_id.id or None #continue #price = prod.purchase_price or 0 if price_list == 0: price = prod.purchase_price or 0 else: price = price_list currency_id = data["currency_id"] currency_rate = self.get_currency_rate(context) if not prod_cur_id and prod.purchase_currency_id: prod_cur_id = prod.purchase_currency_id.id if prod_cur_id: if prod_cur_id != currency_id: cur_id=get_model("currency").browse(prod_cur_id) currency_from_rate=cur_id.get_rate(date=data['date'],rate_type="buy") or 1 price = get_model("currency").convert(price, prod_cur_id, currency_id, from_rate=currency_from_rate, to_rate=currency_rate) else: price = get_model("currency").convert(price, prod_cur_id, currency_id, to_rate=currency_rate) else: price = get_model("currency").convert(price, settings.currency_id.id, currency_id, to_rate=currency_rate) line["unit_price"] = price data = self.update_amounts(context) return data def onchange_currency_rate(self, context={}): data=context['data'] path=context['path'] line=get_data_path(data, path, parent=True) currency=get_model("currency").browse(line['currency_id']) line['rate']=currency.get_rate(date=data['date'],rate_type="buy") or 1 data = self.update_line_currency(context) return data def create(self, vals, **kw): context=kw.get('context',{}) context['is_create']=True kw['context']=context id = super(PurchaseOrder, self).create(vals, **kw) self.function_store([id]) return id def write(self, ids, vals, **kw): super(PurchaseOrder, self).write(ids, vals, **kw) self.function_store(ids) line_ids=get_model('purchase.order.line').search([['order_id','in', ids]]) get_model("purchase.order.line").function_store(line_ids) def confirm(self, ids, context={}): settings = get_model("settings").browse(1) for obj in self.browse(ids): if obj.state != "draft": raise Exception("Invalid state") if not obj.amount_total: raise Exception("Cannot confirm PO if total amount is zero") service_count=0 non_pro_count=0 for line in obj.lines: prod = line.product_id if not prod: non_pro_count +=1 if prod and prod.type in ("stock", "consumable", "bundle", "master") and not line.location_id: raise Exception("Missing location for product %s" % prod.code) if prod.purchase_min_qty and line.qty < prod.purchase_min_qty: raise Exception("Minimum Purchases Qty for [%s] %s is %s"%(prod.code,prod.name,prod.purchase_min_qty)) if prod.type=='service': service_count+=1 obj.write({"state": "confirmed"}) if settings.purchase_copy_picking and service_count!=len(obj.lines) and non_pro_count!=len(obj.lines): if obj.pickings: for pick in obj.pickings: pick.delete() res=obj.copy_to_picking() if res: picking_id=res["picking_id"] get_model("stock.picking").pending([picking_id]) if settings.purchase_copy_invoice: if obj.invoices: for inv in obj.invoices: inv.delete() obj.copy_to_invoice() obj.trigger("confirm") def done(self, ids, context={}): for obj in self.browse(ids): if obj.state != "confirmed": raise Exception("Invalid state") obj.write({"state": "done"}) def reopen(self, ids, context={}): for obj in self.browse(ids): if obj.state != "done": raise Exception("Invalid state") obj.write({"state": "confirmed"}) def to_draft(self, ids, context={}): for obj in self.browse(ids): non_pro = 0 for line in obj.lines: if not line.product_id: non_pro += 1 if non_pro == len(obj.lines): obj.is_delivered = False cannot_draft=(obj.is_delivered and obj.pickings) or obj.is_paid if cannot_draft: raise Exception("Cannot to draft if order is delivered or paid!") for inv in obj.invoices: assert inv.state=="draft" or inv.state=="voided" or inv.state=="waiting_approval","Can not To Draft purchase order.The Invoice must be Draft." for pick in obj.pickings: assert pick.state == "draft" or pick.state=="voided","Can not To Draft purchase order.The Stock Picking must be Draft." obj.write({"state": "draft"}) def get_amount(self, ids, context={}): settings = get_model("settings").browse(1) res = {} for obj in self.browse(ids): vals = {} subtotal = 0 tax = 0 for line in obj.lines: if line.tax_id: line_tax = get_model("account.tax.rate").compute_tax( line.tax_id.id, line.amount, tax_type=obj.tax_type) else: line_tax = 0 tax += line_tax if obj.tax_type == "tax_in": subtotal += line.amount - line_tax else: subtotal += line.amount vals["amount_subtotal"] = subtotal vals["amount_tax"] = tax vals["amount_total"] = subtotal + tax vals["amount_total_cur"] = get_model("currency").convert( vals["amount_total"], obj.currency_id.id, settings.currency_id.id) res[obj.id] = vals return res def get_qty_total(self, ids, context={}): res = {} for obj in self.browse(ids): qty = sum([line.qty for line in obj.lines]) res[obj.id] = qty or 0 return res def update_amounts(self, context): settings=get_model("settings").browse(1) data = context["data"] data["amount_subtotal"] = 0 data["amount_tax"] = 0 tax_type = data["tax_type"] for line in data["lines"]: if not line: continue amt = Decimal((line.get("qty") or 0) * (line.get("unit_price") or 0)) if line.get("discount_percent"): disc = amt * line["discount_percent"] / Decimal(100) amt -= disc amt -= (line.get("discount_amount") or 0) line["amount"] = amt currency_rate=self.get_currency_rate(context) line['amount_cur']=get_model("currency").convert(amt, data['currency_id'], settings.currency_id.id, rate=currency_rate) tax_id = line.get("tax_id") if tax_id: tax = get_model("account.tax.rate").compute_tax(tax_id, amt, tax_type=tax_type) data["amount_tax"] += tax else: tax = 0 if tax_type == "tax_in": data["amount_subtotal"] += amt - tax else: data["amount_subtotal"] += amt data["amount_total"] = data["amount_subtotal"] + data["amount_tax"] return data def onchange_product(self, context): data = context["data"] path = context["path"] settings=get_model("settings").browse(1) line = get_data_path(data, path, parent=True) prod_id = line.get("product_id") if not prod_id: return {} prod_cur_id = None prod = get_model("product").browse(prod_id) line["description"] = prod.description line["qty"] = prod.purchase_min_qty or 1 line["uom_id"] = prod.purchase_uom_id.id or prod.uom_id.id pricelist_id = data["price_list_id"] price = 0 if pricelist_id: price = get_model("price.list").get_price(pricelist_id, prod.id, 1) or 0 prod_cur_id = get_model("price.list").browse(pricelist_id).currency_id.id or None if not price or price == 0: price = prod.purchase_price or 0 if not prod_cur_id: prod_cur_id = prod.purchase_currency_id.id or None currency_id = data["currency_id"] currency_rate = self.get_currency_rate(context) if price: if prod_cur_id: if prod_cur_id != currency_id: cur_id=get_model("currency").browse(prod_cur_id) currency_from_rate=cur_id.get_rate(date=data['date'],rate_type="buy") or 1 price = get_model("currency").convert(price, prod_cur_id, currency_id, from_rate=currency_from_rate, to_rate=currency_rate) else: price = get_model("currency").convert(price, prod_cur_id, currency_id, to_rate=currency_rate) else: price = get_model("currency").convert(price, settings.currency_id.id, currency_id, to_rate=currency_rate) line["unit_price"] = price ratio = get_model("uom").browse(int(line["uom_id"])).ratio line["unit_price"] = price * ratio if prod.purchase_uom_id: line["unit_price"] = price * ratio / prod.purchase_uom_id.ratio or 1 if prod.categ_id and prod.categ_id.purchase_tax_id: line["tax_id"] = prod.categ_id.purchase_tax_id.id if prod.purchase_tax_id is not None: line["tax_id"] = prod.purchase_tax_id.id contact_id=data.get('contact_id') if contact_id: contact=get_model("contact").browse(contact_id) if contact.tax_payable_id: line["tax_id"] = contact.tax_payable_id.id if data.get("tax_type","")=="no_tax": line["tax_id"]=None if prod.location_id: line["location_id"] = prod.location_id.id elif prod.locations: line["location_id"] = prod.locations[0].location_id.id #TODO #amount_cur self.onchange_location(context) data = self.update_amounts(context) return data def onchange_location(self, context): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) line["qty_stock"] = 0 if "product_id" in line and 'location_id' in line: product_id=line['product_id'] location_id=line['location_id'] qty_stock=get_model("stock.balance").get_qty_stock(product_id, location_id) line["qty_stock"] = qty_stock return data def onchange_qty(self, context): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) prod_id = line.get("product_id") if not prod_id:return {} prod = get_model("product").browse(prod_id) qty = line["qty"] price = line["unit_price"] if not price: settings = get_model("settings").browse(1) currency_id = data["currency_id"] currency_rate=self.get_currency_rate(context) price_cur = get_model("currency").convert(price, settings.currency_id.id, currency_id, from_rate=1, to_rate=currency_rate) line["unit_price"] = price_cur line['amount_cur']= price_cur*qty if prod.purchase_min_qty and qty < prod.purchase_min_qty: raise Exception("Minimum Sales Qty for [%s] %s is %s"%(prod.code,prod.name,prod.purchase_min_qty)) data = self.update_amounts(context) return data def onchange_uom(self, context): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) prod_id = line.get("product_id") uom_id = line.get("uom_id") pricelist_id = data["price_list_id"] price=None if not prod_id:return data prod = get_model("product").browse(prod_id) if not uom_id: return data uom = get_model("uom").browse(uom_id) if pricelist_id: price = get_model("price.list").get_price(pricelist_id, prod.id, 1) if price is None: price = prod.purchase_price if price is not None: if prod.purchase_currency_id and prod.purchase_currency_id.id != data['currency_id']: price=get_model("currency").convert(price, prod.purchase_currency_id.id, data['currency_id']) line["unit_price"] = price * uom.ratio / prod.uom_id.ratio if prod.purchase_uom_id: line["unit_price"] = price * uom.ratio / prod.purchase_uom_id.ratio or 1 data = self.update_amounts(context) return data def copy_to_picking(self, ids, context={}): settings=get_model("settings").browse(1) obj = self.browse(ids[0]) contact = obj.contact_id pick_vals = { "type": "in", "ref": obj.number, "related_id": "purchase.order,%s" % obj.id, "contact_id": contact.id, "currency_id": obj.currency_id.id, "ship_method_id": obj.ship_method_id.id, "lines": [], } if obj.delivery_date: pick_vals["date"]=obj.delivery_date+datetime.strftime(datetime.now()," %H:%M:%S") if contact and contact.pick_in_journal_id: pick_vals["journal_id"] = contact.pick_in_journal_id.id res = get_model("stock.location").search([["type", "=", "supplier"]],order="id") if not res: raise Exception("Supplier location not found") supp_loc_id = res[0] res = get_model("stock.location").search([["type", "=", "internal"]]) if not res: raise Exception("Warehouse not found") wh_loc_id = res[0] if not settings.currency_id: raise Exception("Missing currency in financial settings") for line in obj.lines: prod = line.product_id if prod.type not in ("stock", "consumable", "bundle", "master"): continue remain_qty = (line.qty or 0) - line.qty_received #remain_qty = (line.qty_stock or line.qty) - line.qty_received if remain_qty <= 0: continue if not prod.unique_lot: unit_price=line.amount/line.qty if line.qty else 0 if obj.tax_type=="tax_in": if line.tax_id: tax_amt = get_model("account.tax.rate").compute_tax( line.tax_id.id, unit_price, tax_type=obj.tax_type) else: tax_amt = 0 cost_price_cur=unit_price-tax_amt else: cost_price_cur=unit_price #if line.qty_stock: #purch_uom=prod.uom_id #if not prod.purchase_to_stock_uom_factor: #raise Exception("Missing purchase order to stock UoM factor for product %s"%prod.code) #cost_price_cur/=prod.purchase_to_stock_uom_factor #else: #purch_uom=line.uom_id purch_uom=line.uom_id cost_price=get_model("currency").convert(cost_price_cur,obj.currency_id.id,settings.currency_id.id,date=pick_vals.get("date")) cost_amount=cost_price*remain_qty line_vals = { "product_id": prod.id, "qty": remain_qty, "uom_id": purch_uom.id, "cost_price_cur": 0 if prod.type == "bundle" else cost_price_cur, "cost_price": 0 if prod.type == "bundle" else cost_price, "cost_amount": 0 if prod.type == "bundle" else cost_amount, "location_from_id": supp_loc_id, "location_to_id": line.location_id.id or wh_loc_id, "related_id": "purchase.order,%s" % obj.id, } pick_vals["lines"].append(("create", line_vals)) else: for i in range(int(line.qty)): if obj.tax_type=="tax_in": if line.tax_id: tax_amt = get_model("account.tax.rate").compute_tax( line.tax_id.id, line.unit_price, tax_type=obj.tax_type) else: tax_amt = 0 cost_price_cur=line.unit_price-tax_amt else: cost_price_cur=line.unit_price purch_uom=line.uom_id cost_price=get_model("currency").convert(cost_price_cur,obj.currency_id.id,settings.currency_id.id,date=pick_vals.get("date")) cost_amount=cost_price line_spilt = { "product_id": prod.id, "qty": 1, "uom_id": purch_uom.id, "cost_price_cur": cost_price_cur, "cost_price": cost_price, "cost_amount": cost_amount, "location_from_id": supp_loc_id, "location_to_id": line.location_id.id or wh_loc_id, "related_id": "purchase.order,%s" % obj.id, } pick_vals["lines"].append(("create", line_spilt)) if not pick_vals["lines"]: return pick_id = get_model("stock.picking").create(pick_vals, {"pick_type": "in"}) pick = get_model("stock.picking").browse(pick_id) pick.set_currency_rate() return { "next": { "name": "pick_in", "mode": "form", "active_id": pick_id, }, "flash": "Goods receipt %s created from purchase order %s" % (pick.number, obj.number), "picking_id": pick_id, } def copy_to_invoice(self, ids, context={}): id = ids[0] obj = self.browse(id) contact = obj.contact_id inv_vals = { "type": "in", "inv_type": "invoice", "ref": obj.number, "related_id": "purchase.order,%s" % obj.id, "contact_id": obj.contact_id.id, "currency_id": obj.currency_id.id, "lines": [], "tax_type": obj.tax_type, } if contact.purchase_journal_id: inv_vals["journal_id"] = contact.purchase_journal_id.id if contact.purchase_journal_id.sequence_id: inv_vals["sequence_id"] = contact.purchase_journal_id.sequence_id.id if contact.purchase_payment_terms_id: inv_vals["payment_terms_id"] = contact.purchase_payment_terms_id.id, inv_vals["due_date"] = get_model("account.invoice").calc_date(time.strftime("%Y-%m-%d"),contact.purchase_payment_terms_id.days) ## get curruncy rate if obj.currency_id: inv_vals["currency_rate"] = obj.currency_id.get_rate(date=time.strftime("%Y-%m-%d"),rate_type="buy") for line in obj.lines: prod = line.product_id remain_qty = line.qty - line.qty_invoiced if remain_qty <= 0: continue # get account for purchase invoice purch_acc_id=None if prod: # 1. get from product purch_acc_id=prod.purchase_account_id and prod.purchase_account_id.id or None # 2. if not get from master / parent product if not purch_acc_id and prod.parent_id: purch_acc_id=prod.parent_id.purchase_account_id.id # 3. if not get from product category categ=prod.categ_id if categ and not purch_acc_id: purch_acc_id= categ.purchase_account_id and categ.purchase_account_id.id or None #if not purch_acc_id: #raise Exception("Missing purchase account configure for product [%s]" % prod.name) amt = Decimal((remain_qty * line.unit_price)) disc = amt * ((line.discount_percent or 0) / Decimal(100)) amt -= disc amt -= line.discount_amount or 0 line_vals = { "product_id": prod.id, "description": line.description, "qty": remain_qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "account_id": purch_acc_id, "tax_id": line.tax_id.id, "discount": line.discount_percent, "discount_amount": line.discount_amount, "amount": amt, } inv_vals["lines"].append(("create", line_vals)) if not inv_vals["lines"]: raise Exception("Nothing left to invoice") inv_id = get_model("account.invoice").create(inv_vals, {"type": "in", "inv_type": "invoice"}) inv = get_model("account.invoice").browse(inv_id) return { "next": { "name": "view_invoice", "active_id": inv_id, }, "flash": "Invoice %s created from purchase order %s" % (inv.number, obj.number), } def get_delivered(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.state == "draft": vals[obj.id] = False return vals is_delivered = True count_line = 0 for line in obj.lines: prod = line.product_id count_line += 1 if not prod: continue if prod.type not in ("stock", "consumable", "bundle", "master"): continue remain_qty = line.qty - line.qty_received if remain_qty > 0: is_delivered = False break if not count_line: is_delivered = False vals[obj.id] = is_delivered return vals def get_paid(self, ids, context={}): vals = {} for obj in self.browse(ids): amt_paid = 0 count_line = 0 for inv in obj.invoices: if inv.state != "paid": continue count_line += 1 amt_paid += inv.amount_total if inv.deposit_notes: amt_paid += inv.amount_deposit is_paid = amt_paid >= obj.amount_total and count_line vals[obj.id] = is_paid if obj.state=='confirmed': if is_paid==True and obj.is_delivered==True: obj.done() return vals def get_invoice_status(self,ids,context={}): vals = {} for obj in self.browse(ids): inv_status = "No" amt_inv = 0 for inv in obj.invoices: if inv.state == "voided" or inv.inv_type != 'invoice': continue amt_inv += inv.amount_total if inv.deposit_notes: amt_inv += inv.amount_deposit if amt_inv >= obj.amount_total: inv_status = "Yes" elif amt_inv != 0: percent = round(Decimal(Decimal(amt_inv / obj.amount_total)*100)) inv_status = "%s"%percent+"%" vals[obj.id] = inv_status return vals def void(self, ids, context={}): obj = self.browse(ids)[0] for pick in obj.pickings: if pick.state != "voided": raise Exception("There are still goods receipts for this purchase order") for inv in obj.invoices: if inv.state != "voided": raise Exception("There are still invoices for this purchase order") obj.write({"state": "voided"}) def copy(self, ids, context): obj = self.browse(ids)[0] vals = { "contact_id": obj.contact_id.id, "date": obj.date, "ref": obj.ref, "currency_id": obj.currency_id.id, "tax_type": obj.tax_type, "lines": [], } for line in obj.lines: line_vals = { "product_id": line.product_id.id, "description": line.description, "qty": line.qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "tax_id": line.tax_id.id, "location_id": line.location_id.id, } vals["lines"].append(("create", line_vals)) new_id = self.create(vals) new_obj = self.browse(new_id) return { "next": { "name": "purchase", "mode": "form", "active_id": new_id, }, "flash": "Purchase order %s copied to %s" % (obj.number, new_obj.number), } def get_invoices(self, ids, context={}): vals = {} for obj in self.browse(ids): inv_ids = [] for inv_line in obj.invoice_lines: inv_id = inv_line.invoice_id.id if inv_id not in inv_ids: inv_ids.append(inv_id) vals[obj.id] = inv_ids return vals def get_pickings(self, ids, context={}): vals = {} for obj in self.browse(ids): pick_ids = [] for move in obj.stock_moves: pick_id = move.picking_id.id if pick_id not in pick_ids: pick_ids.append(pick_id) vals[obj.id] = pick_ids return vals def onchange_contact(self, context): data = context["data"] contact_id = data.get("contact_id") if not contact_id: return {} contact = get_model("contact").browse(contact_id) data["payment_terms"] = contact.payment_terms data["price_list_id"] = contact.purchase_price_list_id.id data["bill_address_id"] = get_model("address").get_billing_address_company() data["ship_address_id"] = get_model("address").get_shipping_address_company() if contact.currency_id: data["currency_id"] = contact.currency_id.id data=self.onchange_currency(context) else: settings = get_model("settings").browse(1) data["currency_id"] = settings.currency_id.id data=self.onchange_currency(context) return data def check_received_qtys(self, ids, context={}): obj = self.browse(ids)[0] for line in obj.lines: if line.qty_received > (line.qty or line.qty_stock): raise Exception("Can not receive excess quantity for purchase order %s and product %s (order qty: %s, received qty: %s)" % ( obj.number, line.product_id.code, line.qty or line.qty_stock, line.qty_received)) def get_purchase_form_template(self, ids, context={}): obj = self.browse(ids)[0] if obj.state == "draft": return "rfq_form" else: return "purchase_form" def get_amount_total_words(self, ids, context={}): vals = {} for obj in self.browse(ids): amount_total_words = utils.num2word(obj.amount_total) vals[obj.id] = amount_total_words return vals def onchange_sequence(self, context={}): data = context["data"] context['date'] = data['date'] seq_id = data["sequence_id"] if not seq_id: seq_id = get_model("sequence").find_sequence(type="purchase_order") while 1: num = get_model("sequence").get_next_number(seq_id, context=context) user_id = get_active_user() set_active_user(1) res = self.search([["number", "=", num]]) set_active_user(user_id) if not res: break get_model("sequence").increment_number(seq_id, context=context) data["number"] = num return data def delete(self, ids, **kw): for obj in self.browse(ids): if obj.state in ("confirmed", "done"): raise Exception("Can not delete purchase order in this status") super().delete(ids, **kw) def view_purchase(self, ids, context={}): obj=get_model("purchase.order.line").browse(ids)[0] return { 'next': { 'name': 'purchase', 'active_id': obj.order_id.id, 'mode': 'form', }, } def copy_to_purchase_return(self,ids,context={}): seq_id = get_model("sequence").find_sequence(type="purchase_return") if not seq_id: raise Exception("Missing Sequence purchase return") for obj in self.browse(ids): order_vals = {} order_vals = { "contact_id":obj.contact_id.id, "date":obj.date, "ref":obj.number, "currency_id":obj.currency_id.id, "tax_type":obj.tax_type, "bill_address_id":obj.bill_address_id.id, "ship_address_id":obj.ship_address_id.id, "price_list_id": obj.price_list_id.id, "payment_terms": obj.payment_terms or obj.contact_id.payment_terms, "lines":[], } for line in obj.lines: line_vals = { "product_id":line.product_id.id, "description":line.description, "qty":line.qty, "uom_id":line.uom_id.id, "unit_price":line.unit_price, "tax_id":line.tax_id.id, "amount":line.amount, "location_id":line.location_id.id, } order_vals["lines"].append(("create", line_vals)) purchase_id = get_model("purchase.return").create(order_vals) purchase = get_model("purchase.return").browse(purchase_id) return { "next": { "name": "purchase_return", "mode": "form", "active_id": purchase_id, }, "flash": "Purchase Return %s created from purchases order %s" % (purchase.number, obj.number), } def onchange_tax_type(self, context={}): data=context['data'] if data['tax_type']=='no_tax': for line in data['lines']: line['tax_id']=None else: for line in data['lines']: product_id=line.get('product_id') if not product_id: continue if line['tax_id']: continue contact_id=data.get('contact_id') if contact_id: contact=get_model("contact").browse(contact_id) if contact.tax_payable_id: line["tax_id"] = contact.tax_payable_id.id product=get_model("product").browse(product_id) if product.purchase_tax_id and not line.get('tax_id'): line["tax_id"] = product.purchase_tax_id.id if product.categ_id and product.categ_id.purchase_tax_id and not line.get('tax_id'): line["tax_id"] = product.categ_id.purchase_tax_id.id data=self.update_amounts(context) return data def search_product(self, clause, context={}): op = clause[1] val = clause[2] if isinstance(val, int): return ["lines.product_id.id", op, val] return ["lines.product_id.name", op, val] def onchange_date(self, context={}): data=self.onchange_sequence(context) data=self.onchange_currency(context) return data def search_location(self, clause, context={}): op = clause[1] val = clause[2] if isinstance(val, int): return ["lines.location_id.id", op, val] return ["lines.location_id.name", op, val] def find_po_line(self, ids, product_id, context={}): obj = self.browse(ids)[0] for line in obj.lines: if line.product_id.id == product_id: return line.id return None
class Campaign(Model): _name = "mkt.campaign" _string = "Campaign" _fields = { "name": fields.Char("Campaign Name", required=True, search=True), "date": fields.Date("Date", required=True, search=True), "target_lists": fields.Many2Many("mkt.target.list", "Target Lists"), "email_tmpl_id": fields.Many2One("email.template", "Email Template"), "mailbox_id": fields.Many2One("email.mailbox", "Email Mailbox"), "comments": fields.One2Many("message", "related_id", "Comments"), "state": fields.Selection([["active", "Active"], ["inactive", "Inactive"]], "Status", required=True), "limit_day": fields.Integer("Daily Limit"), "limit_hour": fields.Integer("Hourly Limit"), "num_targets": fields.Integer("Number targets", function="get_stats", function_multi=True), "num_create": fields.Integer("Number emails created", function="get_stats", function_multi=True), "percent_create": fields.Float("% created", function="get_stats", function_multi=True), "num_sent": fields.Integer("Number emails sent", function="get_stats", function_multi=True), "percent_sent": fields.Float("% sent", function="get_stats", function_multi=True), "num_delivered": fields.Integer("Number emails delivered", function="get_stats", function_multi=True), "percent_delivered": fields.Float("% delivered", function="get_stats", function_multi=True), "num_bounced": fields.Integer("Number emails bounced", function="get_stats", function_multi=True), "percent_bounced": fields.Float("% bounced", function="get_stats", function_multi=True), "num_rejected": fields.Integer("Number emails rejected", function="get_stats", function_multi=True), "percent_rejected": fields.Float("% rejected", function="get_stats", function_multi=True), "num_opened": fields.Integer("Number emails opened", function="get_stats", function_multi=True), "percent_opened": fields.Float("% opened", function="get_stats", function_multi=True), "num_clicked": fields.Integer("Number emails clicked", function="get_stats", function_multi=True), "percent_clicked": fields.Float("% clicked", function="get_stats", function_multi=True), "num_create_day": fields.Integer("Emails created within day", function="get_stats", function_multi=True), "num_create_hour": fields.Integer("Emails created within hour", function="get_stats", function_multi=True), "emails": fields.One2Many("email.message", "related_id", "Emails"), "min_target_life": fields.Integer("Minimum Target Life (days)"), } _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d"), "state": "active", } def create_emails_all(self, context={}): for obj in self.search_browse([["state", "=", "active"]]): obj.create_emails() def create_emails(self, ids, context={}): obj = self.browse(ids)[0] if obj.state != "active": raise Exception("Invalid state") if not obj.email_tmpl_id: raise Exception("Missing email template") limit = None if obj.limit_day: limit = obj.limit_day - obj.num_create_day if obj.limit_hour: l = obj.limit_hour - obj.num_create_hour if limit is None or l < limit: limit = l sent_emails = set() for email in obj.emails: if not email.name_id: continue if email.name_id._model != "mkt.target": continue target_id = email.name_id.id res = get_model("mkt.target").search( [["id", "=", email.name_id.id]]) # XXX if not res: continue target = get_model("mkt.target").browse(target_id) sent_emails.add(target.email) count = 0 for tl in obj.target_lists: for target in tl.targets: if target.email in sent_emails: continue if obj.min_target_life and target.target_life < obj.min_target_life: continue if limit is not None and count >= limit: break settings = get_model("settings").browse(1) data = { "settings": settings, "obj": target, } obj.email_tmpl_id.create_email( data, name_id="mkt.target,%d" % target.id, related_id="mkt.campaign,%d" % obj.id, mailbox_id=obj.mailbox_id) count += 1 db = get_connection() db.commit() return { "next": { "name": "campaign", "mode": "form", "active_id": obj.id, }, "flash": "%d emails created" % count, } def get_stats(self, ids, context={}): vals = {} for obj_id in ids: vals[obj_id] = { "num_targets": 0, "num_create": 0, "num_sent": 0, "num_delivered": 0, "num_bounced": 0, "num_rejected": 0, "num_opened": 0, "num_clicked": 0, "num_create_day": 0, "num_create_hour": 0, } db = get_connection() res = db.query( "SELECT c.id,COUNT(DISTINCT t.email) FROM mkt_campaign c JOIN m2m_mkt_campaign_mkt_target_list r ON r.mkt_campaign_id=c.id JOIN mkt_target t ON t.list_id=r.mkt_target_list_id WHERE c.id IN %s GROUP BY c.id", tuple(ids)) for r in res: vals[r.id]["num_targets"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_create"] = r.count d = (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S") res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND date>%s GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids]), d) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_create_day"] = r.count d = (datetime.now() - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S") res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND date>%s GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids]), d) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_create_hour"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='sent' GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_sent"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='delivered' GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_delivered"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='bounced' GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_bounced"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='rejected' GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_rejected"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND opened GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_opened"] = r.count res = db.query( "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND clicked GROUP BY related_id", tuple(["mkt.campaign,%d" % x for x in ids])) for r in res: obj_id = int(r.related_id.split(",")[1]) v = vals[obj_id] v["num_clicked"] = r.count for obj in self.browse(ids): v = vals[obj.id] v["percent_create"] = v["num_create"] * 100.0 / v[ "num_targets"] if v["num_targets"] else None v["percent_sent"] = v["num_sent"] * 100.0 / v["num_create"] if v[ "num_create"] else None v["percent_delivered"] = v["num_delivered"] * 100.0 / v[ "num_create"] if v["num_create"] else None v["percent_bounced"] = v["num_bounced"] * 100.0 / v[ "num_create"] if v["num_create"] else None v["percent_rejected"] = v["num_rejected"] * 100.0 / v[ "num_create"] if v["num_create"] else None v["percent_opened"] = v["num_opened"] * 100.0 / v[ "num_create"] if v["num_create"] else None v["percent_clicked"] = v["num_clicked"] * 100.0 / v[ "num_create"] if v["num_create"] else None return vals
class StockConsign(Model): _name = "stock.consign" _string = "Consignment Stock" _name_field = "location_id" _multi_company = True _fields = { "location_id": fields.Many2One("stock.location", "Stock Location", required=True, search=True), "contact_id": fields.Many2One("contact", "Contact", required=True, search=True), "type": fields.Selection([["sale", "Sell"], ["purchase", "Purchase"]], "Consignment Type", required=True, search=True), "company_id": fields.Many2One("company", "Company", search=True), "periods": fields.One2Many("stock.consign.period", "consign_id", "Consignment Periods"), "order_type": fields.Selection( [["stock", "From Stock"], ["sale", "From Sales Orders"], ["invoice", "From Customer Invoices"]], "Create Order"), "new_invoice_lines": fields.Many2Many("account.invoice.line", "New Invoice Lines", function="get_new_invoice_lines"), "purchase_orders": fields.One2Many("purchase.order", "related_id", "Purchase Orders"), } _defaults = { "company_id": lambda *a: access.get_active_company(), "order_type": "stock", } def create_purchase(self, ids, context={}): for obj in self.browse(ids): if obj.type != "purchase": raise Exception("Wrong consignment type") if obj.order_type in ("stock", "sale"): # XXX for period in obj.periods: if period.purchase_id: continue period.create_purchase() elif obj.order_type == "invoice": obj.create_purchase_from_invoice() def create_sale(self, ids, context={}): for obj in self.browse(ids): if obj.type != "sale": raise Exception("Wrong consignment type") for period in obj.periods: if period.sale_id: continue period.create_sale() def create_periods(self, ids, context={}): for obj in self.browse(ids): res = get_model("stock.consign.period").search( [["consign_id", "=", obj.id]], order="date_to desc") if res: period_id = res[0] period = get_model("stock.consign.period").browse(period_id) date_from = datetime.strptime( period.date_to, "%Y-%m-%d").date() + timedelta(days=1) else: date_from = date.today() last_d = date.today() + timedelta(days=1) while date_from <= last_d: next_d = date_from + timedelta(days=1) while not is_business_day(next_d.strftime("%Y-%m-%d")): next_d += timedelta(days=1) date_to = next_d - timedelta(days=1) date_from_s = date_from.strftime("%Y-%m-%d") date_to_s = date_to.strftime("%Y-%m-%d") res = get_model("stock.consign.period").search( [["consign_id", "=", obj.id], ["date_from", "<=", date_to_s], ["date_to", ">=", date_from_s]]) if res: print("overlapping consign period already exists", obj.id, date_from_s, date_to_s) continue vals = { "consign_id": obj.id, "date_from": date_from_s, "date_to": date_to_s, } print("create consign period", obj.id, date_from_s, date_to_s) get_model("stock.consign.period").create(vals) date_from = date_to + timedelta(days=1) def get_new_invoice_lines(self, ids, context={}): vals = {} for obj in self.browse(ids): res = get_model("company").search( [["contact_id", "=", obj.contact_id.id]]) if res: sup_company_id = res[0] inv_line_ids = get_model("account.invoice.line").search( [["product_id.company_id", "child_of", sup_company_id], ["purchase_id", "=", None], ["invoice_id.state", "=", "paid"]]) else: inv_line_ids = [] vals[obj.id] = inv_line_ids return vals def create_purchase_from_invoice(self, ids, context={}): obj = self.browse(ids[0]) day_inv_lines = {} for inv_line in obj.new_invoice_lines: inv = inv_line.invoice_id day_inv_lines.setdefault(inv.date, []).append(inv_line.id) for d, inv_line_ids in day_inv_lines.items(): purch_vals = { "date": d, "company_id": obj.company_id.id, "contact_id": obj.contact_id.id, "tax_type": "tax_ex", "lines": [], "related_id": "stock.consign,%d" % obj.id, } prods = {} for inv_line in get_model("account.invoice.line").browse( inv_line_ids): prod = inv_line.product_id prods.setdefault(prod.id, { "qty": 0, "amt": 0, }) qty = inv_line.qty or 0 amt = (prod.purchase_price or 0) * qty prods[prod.id]["qty"] += qty prods[prod.id]["amt"] += amt for prod_id, prod_vals in prods.items(): qty = prod_vals["qty"] amt = prod_vals["amt"] price = amt / qty if qty else 0 line_vals = { "product_id": prod.id, "description": prod.description or "/", "qty": qty, "uom_id": prod.uom_id.id, "unit_price": price, } purch_vals["lines"].append(("create", line_vals)) purch_id = get_model("purchase.order").create(purch_vals) get_model("account.invoice.line").write(inv_line_ids, {"purchase_id": purch_id})
class Product(Model): _name = "product" _string = "Product" _audit_log = True _key = ["code", "state", "company_id"] _order = "code,name" _export_name_field = "code" _history = True _fields = { "name": fields.Char("Name", required=True, search=True, translate=True, size=256), "code": fields.Char("Code", required=True, search=True), "type": fields.Selection([["stock", "Stockable"], ["consumable", "Consumable"], ["service", "Service"], ["master", "Master"], ["bundle", "Bundle"]], "Product Type", required=True, search=True), "uom_id": fields.Many2One("uom", "Default UoM", required=True, search=True), "parent_id": fields.Many2One("product", "Master Product"), "categ_id": fields.Many2One("product.categ", "Product Category", search=True), "description": fields.Text("Description", translate=True), "purchase_price": fields.Decimal("Purchase Price", scale=6), "sale_price": fields.Decimal("List Price (Sales Invoice UoM)", scale=6), "sale_price_order_uom": fields.Decimal("List Price (Sales Order UoM)", scale=6, function="get_sale_price_order_uom"), "tags": fields.Many2Many("tag", "Tags"), "image": fields.File("Image"), "cost_method": fields.Selection( [["standard", "Standard Cost"], ["average", "Weighted Average"], ["fifo", "FIFO"], ["lifo", "LIFO"]], "Costing Method"), "cost_price": fields.Decimal("Cost Price", scale=6), "stock_in_account_id": fields.Many2One("account.account", "Stock Input Account", multi_company=True), # XXX: deprecated "stock_out_account_id": fields.Many2One("account.account", "Stock Output Account", multi_company=True), # XXX: deprecated "cogs_account_id": fields.Many2One("account.account", "Cost Of Goods Sold Account", multi_company=True), "stock_account_id": fields.Many2One("account.account", "Inventory Account", multi_company=True), "purchase_account_id": fields.Many2One("account.account", "Purchase Account", multi_company=True), "purchase_tax_id": fields.Many2One("account.tax.rate", "Purchase Tax"), "supplier_id": fields.Many2One("contact", "Default Supplier"), # XXX: deprecated "sale_account_id": fields.Many2One("account.account", "Sales Account", multi_company=True), "sale_return_account_id": fields.Many2One("account.account", "Sales Returns Account", multi_company=True), "sale_promotion_account_id": fields.Many2One("account.account", "Sales Promotion Account", multi_company=True), "sale_tax_id": fields.Many2One("account.tax.rate", "Sales Tax"), "sale_promotion_tax_id": fields.Many2One("account.tax.rate", "Promotion Tax"), "location_id": fields.Many2One("stock.location", "Warehouse"), # XXX: deprecated "bin_location": fields.Char("Bin Location"), "update_balance": fields.Boolean("Update Balance"), "active": fields.Boolean("Active"), "comments": fields.One2Many("message", "related_id", "Comments"), "categs": fields.Many2Many("product.categ", "Other Categories"), # XXX: deprecated "attributes": fields.One2Many("product.attribute.value", "product_id", "Attributes"), "variants": fields.One2Many("product", "parent_id", "Variants"), #"variant_values": fields.One2Many("product.custom.option.variant.value","product_id","Variant Values"), "custom_options": fields.Many2Many("product.custom.option", "Custom Options"), "images": fields.One2Many("product.image", "product_id", "Images"), "store_type_id": fields.Many2One("store.type", "Storage Type"), "brand_id": fields.Many2One("product.brand", "Brand", search=True), "related_products": fields.Many2Many("product", "Related Products", relfield="product1_id", relfield_other="product2_id"), "min_sale_qty": fields.Decimal("Min Sale Qty"), "sale_unit_qty": fields.Decimal("Sale Unit Qty"), "shelf_life": fields.Decimal("Shelf Life (Days)"), "weight": fields.Decimal("Weight (Kg)"), "volume": fields.Decimal("Volume (M^3)"), "width": fields.Decimal("Width"), "height": fields.Decimal("Height"), "length": fields.Decimal("Length"), "packing_size": fields.Char("Packing Size"), "details": fields.Text("Product Details", translate=True), "details2": fields.Text("Product Details (2)", translate=True), "details2_label": fields.Char("Product Details Label (2)"), "details3": fields.Text("Product Details (3)", translate=True), "details3_label": fields.Char("Product Details Label (3)"), "other_url": fields.Char("Product URL", size=256), "purchase_currency_id": fields.Many2One("currency", "Purchase Currency"), "purchase_currency_rate": fields.Decimal("Purchase Currency Rate", scale=6), "purchase_duty_percent": fields.Decimal("Import Duty (%)"), "purchase_ship_percent": fields.Decimal("Shipping Charge (%)"), "landed_cost": fields.Decimal("Landed Cost", function="get_landed_cost", function_multi=True), "landed_cost_conv": fields.Decimal("Landed Cost (Conv)", function="get_landed_cost", function_multi=True), "gross_profit": fields.Decimal("Gross Profit (%)"), "auto_list_price": fields.Decimal("Auto List Price", function="get_auto_list_price"), "max_discount": fields.Decimal("Max Discount (%)", function="get_max_discount"), "price_index": fields.Decimal("Price Index", function="get_price_index"), "price_notes": fields.Text("Notes"), "price_date": fields.Date("Price Date"), "pricelist_items": fields.One2Many("price.list.item", "product_id", "Pricelist Items"), "procure_method": fields.Selection([["mto", "Make To Order"], ["mts", "Make To Stock"]], "Procurement Method"), "supply_method": fields.Selection( [["purchase", "Purchase"], ["production", "Production"]], "Supply Method"), "can_sell": fields.Boolean("Can Sell"), "can_purchase": fields.Boolean("Can Purchase"), "id": fields.Integer("Database ID", readonly=True), # MTS "create_time": fields.DateTime("Create Time", readonly=True), # MTS "supplier_product_code": fields.Char("Supplier Product Code"), # XXX: deprecated "require_qty2": fields.Boolean("Require Secondary Qty"), "qty2_factor": fields.Decimal("UoM -> Secondary Qty Factor", scale=6), "replacements": fields.Many2Many("product", "Replacement Products", reltable="m2m_product_replacement", relfield="product1_id", relfield_other="product2_id"), "suppliers": fields.One2Many("product.supplier", "product_id", "Suppliers"), "max_qty_loss": fields.Decimal("Max Qty Loss"), "documents": fields.One2Many("document", "related_id", "Documents"), "ship_methods": fields.Many2Many("ship.method", "Shipping Methods"), "ecom_discount_percent": fields.Decimal("Ecom. Discount Percent"), # XXX: deprecated "ecom_special_price": fields.Decimal("Ecom. Special Price"), # XXX: deprecated #"ecom_sale_price": fields.Decimal("Ecom. Sale Price", function="get_ecom_sale_price", function_multi=True), # XXX: deprecated #"ecom_has_discount": fields.Decimal("Ecom. Has Discount", function="get_ecom_sale_price", function_multi=True), # XXX: deprecated "variant_attributes": fields.Json("Variant Attributes", function="get_variant_attributes"), "company_id": fields.Many2One("company", "Company"), "sale_channels": fields.Many2Many("sale.channel", "Sales Channels"), "customer_price": fields.Decimal("Customer Price", function="get_customer_price", function_multi=True), "customer_has_discount": fields.Decimal("Customer Has Discount", function="get_customer_price", function_multi=True), "customer_discount_text": fields.Decimal("Customer Discount Text", function="get_customer_price", function_multi=True), "customer_discount_percent": fields.Decimal("Customer Discount Percent", function="get_customer_price", function_multi=True), "sale_company_id": fields.Many2One("company", "Sold By"), "groups": fields.Many2Many("product.group", "Groups"), "payment_methods": fields.Many2Many("payment.method", "Payment Methods"), "locations": fields.One2Many("product.location", "product_id", "Warehouses"), "stock_qty": fields.Decimal("Total Stock Qty", function="get_stock_qty"), "stock_lots": fields.Many2Many("stock.lot", "Lots In Stock", function="get_stock_lots"), "state": fields.Selection([["draft", "Draft"], ["approved", "Approved"]], "Status"), "sale_uom_id": fields.Many2One("uom", "Sales Order UoM"), "sale_invoice_uom_id": fields.Many2One("uom", "Sales Invoice UoM"), "sale_to_stock_uom_factor": fields.Decimal("Sales Order -> Stock Uom Conversion Factor", scale=6), "sale_to_invoice_uom_factor": fields.Decimal("Sales Order -> Sales Invoice Uom Conversion Factor", scale=6), "purchase_uom_id": fields.Many2One("uom", "Purchase Order UoM"), "purchase_invoice_uom_id": fields.Many2One("uom", "Purchase Invoice UoM"), "purchase_to_stock_uom_factor": fields.Decimal("Purchase Order -> Stock Uom Conversion Factor", scale=6), "purchase_to_invoice_uom_factor": fields.Decimal( "Purchase Order -> Purchase Invoice Uom Conversion Factor", scale=6), "purchase_lead_time": fields.Integer("Purchasing Lead Time (Days)"), "purchase_min_qty": fields.Decimal("Purchase Minimum Qty"), "purchase_qty_multiple": fields.Decimal("Purchase Qty Multiple"), "mfg_lead_time": fields.Integer("Manufacturing Lead Time (Days)"), "mfg_min_qty": fields.Decimal("Manufacturing Minimum Qty"), "mfg_qty_multiple": fields.Decimal("Manufacturing Qty Multiple"), #"purchase_price_uom_id": fields.Many2One("uom", "Purchase Price UoM"), # not needed? #"sale_price_uom_id": fields.Many2One("uom", "List Price UoM"), # not needed? "events": fields.Many2Many("sale.event", "Events"), "is_published": fields.Boolean("Publish Product"), "require_lot": fields.Boolean("Require Lot"), "lot_select": fields.Selection([["fifo", "FIFO"], ["fefo", "FEFO"]], "Lot Selection"), "components": fields.One2Many("product.component", "product_id", "Bundle Components"), "approve_date": fields.DateTime("Approve Date"), "service_items": fields.One2Many("service.item", "product_id", "Service Items"), "lots": fields.One2Many("stock.lot", "product_id", "Lots"), "stock_plan_horizon": fields.Integer("Inventory Planning Horizon (days)"), # XXX: deprecated "ecom_hide_qty": fields.Boolean("Hide Stock Qty From Website"), "ecom_hide_unavail": fields.Boolean("Hide From Website When Out Of Stock"), "ecom_no_order_unavail": fields.Boolean("Prevent Orders When Out Of Stock"), "ecom_select_lot": fields.Boolean("Customers Can Select Lot When Ordering"), "product_origin": fields.Char("Product Origin"), "stock_balances": fields.One2Many("stock.balance", "product_id", "Stock Balances"), "check_lot_neg_stock": fields.Boolean("Check Lot Negative Stock"), "sale_lead_time_nostock": fields.Integer("Sale Lead Time When Out Of Stock (Days)"), "ship_methods": fields.Many2Many("ship.method", "Shipping Methods"), "delivery_weekdays": fields.Char("Delivery Weekday Constraints"), } _defaults = { "update_balance": True, "active": True, "can_sell": False, "can_purchase": False, "company_id": lambda *a: access.get_active_company(), "state": "draft", } def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): if obj.code: name = "[%s] %s" % (obj.code, obj.name) else: name = obj.name vals.append((obj.id, name, obj.image)) return vals def name_search(self, name, condition=None, context={}, limit=None, **kw): print("condition", condition) search_mode = context.get("search_mode") print("##############################") print("search_mode", search_mode) if search_mode == "suffix": cond = [["code", "=ilike", "%" + name]] elif search_mode == "prefix": cond = [["code", "=ilike", name + "%"]] else: cond = [["code", "ilike", name]] if condition: cond = [cond, condition] ids1 = self.search(cond, limit=limit) if search_mode == "suffix": cond = [["name", "=ilike", "%" + name]] elif search_mode == "prefix": cond = [["name", "=ilike", name + "%"]] else: cond = [["name", "ilike", name]] if condition: cond = [cond, condition] ids2 = self.search(cond, limit=limit) ids = list(set(ids1 + ids2)) return self.name_get(ids, context=context) def copy(self, ids, context={}): replace_id = context.get("replace_id") obj = self.browse(ids)[0] code = obj.code if not replace_id: for i in range(1, 10): code = obj.code + " (%s)" % i res = self.search([["code", "=", code]]) if not res: break vals = { "name": obj.name, "code": code, "type": obj.type, "uom_id": obj.uom_id.id, #"parent_id": obj.parent_id.id, XXX "description": obj.description, "image": obj.image, "categ_id": obj.categ_id.id, "categs": [("set", [c.id for c in obj.categs])], "supply_method": obj.supply_method, "procure_method": obj.procure_method, "can_sell": obj.can_sell, "can_purchase": obj.can_purchase, "sale_uom_id": obj.sale_uom_id.id, "sale_invoice_uom_id": obj.sale_invoice_uom_id.id, "sale_to_stock_uom_factor": obj.sale_to_stock_uom_factor, "sale_to_invoice_uom_factor": obj.sale_to_invoice_uom_factor, "purchase_uom_id": obj.purchase_uom_id.id, "purchase_invoice_uom_id": obj.purchase_invoice_uom_id.id, "purchase_to_stock_uom_factor": obj.purchase_to_stock_uom_factor, "purchase_to_invoice_uom_factor": obj.purchase_to_invoice_uom_factor, "purchase_price": obj.purchase_price, "purchase_account_id": obj.purchase_account_id.id, "purchase_tax_id": obj.purchase_tax_id.id, "supplier_id": obj.supplier_id.id, "sale_price": obj.sale_price, "sale_account_id": obj.sale_account_id.id, "sale_tax_id": obj.sale_tax_id.id, "sale_return_account_id": obj.sale_return_account_id.id, "sale_promotion_account_id": obj.sale_promotion_account_id.id, "sale_promotion_tax_id": obj.sale_promotion_tax_id.id, "cost_method": obj.cost_method, "stock_in_account_id": obj.stock_in_account_id.id, "stock_out_account_id": obj.stock_out_account_id.id, "bin_location": obj.bin_location, "sale_company_id": obj.sale_company_id.id, "attributes": [], } vals["attributes"] = [("delete_all", )] for attr in obj.attributes: vals["attributes"].append(("create", { "attribute_id": attr.attribute_id.id, "option_id": attr.option_id.id })) vals["pricelist_items"] = [("delete_all", )] for item in obj.pricelist_items: vals["pricelist_items"].append(("create", { "list_id": item.list_id.id, "price": item.price })) vals["images"] = [("delete_all", )] for image in obj.images: vals["images"].append(("create", { "image": image.image, "title": image.title })) print("vals", vals) if replace_id: self.write([replace_id], vals) new_id = replace_id else: new_id = self.create(vals) return { "next": { "name": "product", "mode": "form", "active_id": new_id, }, "flash": "New product copied from %s" % obj.name, } def get_landed_cost(self, ids, context={}): vals = {} for obj in self.browse(ids): amt = Decimal(obj.purchase_price or 0) * Decimal( 1 + (obj.purchase_duty_percent or 0) / 100) * Decimal(1 + (obj.purchase_ship_percent or 0) / 100) amt_cur = amt * Decimal(obj.purchase_currency_rate or 1) vals[obj.id] = { "landed_cost": amt, "landed_cost_conv": amt_cur, } return vals def get_auto_list_price(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.landed_cost_conv and obj.gross_profit and obj.gross_profit != 100: vals[obj.id] = obj.landed_cost_conv / (1 - obj.gross_profit / 100) else: vals[obj.id] = None return vals def get_max_discount(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.sale_price and obj.landed_cost_conv: vals[obj.id] = max(0, (obj.sale_price - obj.landed_cost_conv) / obj.sale_price * 100) else: vals[obj.id] = None return vals def get_price_index(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.sale_price and obj.landed_cost_conv: vals[obj.id] = obj.sale_price / obj.landed_cost_conv else: vals[obj.id] = None return vals def update_prices(self, context={}): print("update_prices") data = context["data"] purchase_price = data.get("purchase_price") purchase_currency_rate = data.get("purchase_currency_rate") purchase_duty_percent = data.get("purchase_duty_percent") purchase_ship_percent = data.get("purchase_ship_percent") if purchase_price: landed_cost = purchase_price * \ (1 + (purchase_duty_percent or 0) / 100) * (1 + (purchase_ship_percent or 0) / 100) landed_cost_conv = landed_cost * (purchase_currency_rate or 1) else: landed_cost = None landed_cost_conv = None gross_profit = data.get("gross_profit") if landed_cost_conv and gross_profit and gross_profit != 100: auto_list_price = landed_cost_conv / (1 - gross_profit / 100) else: auto_list_price = None sale_price = data.get("sale_price") if sale_price and landed_cost_conv: max_discount = max(0, (sale_price - landed_cost_conv) / sale_price * 100) price_index = sale_price / landed_cost else: max_discount = None price_index = None data.update({ "landed_cost": landed_cost, "landed_cost_conv": landed_cost_conv, "auto_list_price": auto_list_price, "max_discount": max_discount, "price_index": price_index, }) return data def get_ecom_sale_price(self, ids, context={}): raise Exception("Deprecated!") vals = {} for obj in self.browse(ids): if obj.ecom_discount_percent: p = (obj.sale_price or 0) * (1 - obj.ecom_discount_percent / 100) has_disc = True elif obj.ecom_special_price: p = obj.ecom_special_price has_disc = True else: p = obj.sale_price or 0 has_disc = False vals[obj.id] = { "ecom_sale_price": p, "ecom_has_discount": has_disc, } return vals def create_variants(self, ids, context={}): # deprecated print("##################################") print("product.create_variants", ids) obj = self.browse(ids[0]) if obj.type != "master": raise Exception("Not a master product") if not obj.custom_options: raise Exception("No custom options for this product") variants = [{}] for opt in obj.custom_options: new_variants = [] for variant in variants: for opt_val in opt.values: new_variant = variant.copy() new_variant[opt.code] = opt_val.code new_variants.append(new_variant) variants = new_variants print("variants", len(variants), variants) count = 1 for variant in variants: vals = { "code": "%s_VARIANT_%.2d" % (obj.code, count), "name": obj.name, "type": "stock", "uom_id": obj.uom_id.id, "parent_id": obj.id, "location_id": obj.location_id.id, "attributes": [], } for k, v in variant.items(): name = "_CUST_OPT_" + k res = get_model("product.attribute").search( [["name", "=", name]]) if res: attr_id = res[0] else: attr_id = get_model("product.attribute").create( {"name": name}) vals["attributes"].append(("create", { "attribute_id": attr_id, "value": v, })) get_model("product").create(vals) count += 1 return { "flash": "%d variants created" % len(variants), } def get_variant_attributes(self, ids, context={}): print("get_variant_attributes", ids) vals = {} for obj in self.browse(ids): attrs = [] attr_options = {} for variant in obj.variants: for attr in variant.attributes: if not attr.attribute_id or not attr.option_id: continue attr_code = attr.attribute_id.code attr_name = attr.attribute_id.name attrs.append((attr_name, attr_code)) opt_code = attr.option_id.code opt_name = attr.option_id.name opt_sequence = attr.option_id.sequence if attr_code == "color": opt_image = variant.image or attr.option_id.image else: opt_image = attr.option_id.image #attr_options.setdefault(attr_code,[]).append((opt_sequence,opt_name,opt_code,opt_image)) attr_options.setdefault(attr_code, []).append( (opt_sequence, opt_name, opt_code)) attrs = list(set(attrs)) res = [] for attr_name, attr_code in sorted(attrs): attr_vals = { "code": attr_code, "name": attr_name, "values": [], } attr_options[attr_code] = list(set(attr_options[attr_code])) #for opt_sequence,opt_name,opt_code,opt_image in sorted(attr_options[attr_code]): for opt_sequence, opt_name, opt_code in sorted( attr_options[attr_code]): attr_vals["values"].append({ "sequence": opt_sequence, "code": opt_code, "name": opt_name, #"image": opt_image, }) res.append(attr_vals) vals[obj.id] = res print("vals", vals) return vals def get_customer_price(self, ids, context={}): # XXX: make it faster pricelist_id = context.get("pricelist_id") pricelist_ids = context.get("pricelist_ids") if pricelist_ids is None and pricelist_id: pricelist_ids = [pricelist_id] vals = {} for obj in self.browse(ids): sale_price = None discount_text = None discount_percent = None if pricelist_ids: min_sale_price = None for item in obj.pricelist_items: if item.list_id.id in pricelist_ids: sale_price = (item.price or 0) if min_sale_price is None or sale_price < min_sale_price: min_sale_price = sale_price discount_text = item.discount_text discount_percent = item.discount_percent sale_price = min_sale_price if sale_price is None: sale_price = (obj.sale_price or 0) has_discount = sale_price < (obj.sale_price or 0) vals[obj.id] = { "customer_price": sale_price, "customer_has_discount": has_discount, "customer_discount_text": discount_text, "customer_discount_percent": discount_percent, } return vals def get_stock_qty(self, ids, context={}): vals = {} for obj in self.browse(ids): qty = 0 for loc in obj.locations: qty += loc.stock_qty vals[obj.id] = qty return vals def get_stock_lots(self, ids, context={}): db = database.get_connection() res = db.query( "SELECT b.product_id,b.location_id,p.sale_price,l.id AS lot_id,l.number,l.weight,SUM(b.qty_virt) AS qty FROM stock_balance b JOIN stock_lot l ON l.id=b.lot_id JOIN product p ON p.id=b.product_id WHERE b.product_id IN %s GROUP BY b.product_id,b.location_id,p.sale_price,l.id,l.number,l.weight ORDER BY l.expiry_date,l.received_date", tuple(ids)) lots = {} prod_lots = {} for r in res: k = (r.product_id, r.lot_id) if k not in lots: lot_vals = { "id": r.lot_id, "qty": 0, } lots[k] = lot_vals prod_lots.setdefault(r.product_id, []).append(lot_vals) lots[k]["qty"] += r.qty vals = {} for obj in self.browse(ids): vals[obj.id] = [ l["id"] for l in prod_lots.get(obj.id, []) if l["qty"] > 0 ] return vals def to_draft(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "draft", "is_published": False}) #XXX def approve(self, ids, context={}): for obj in self.browse(ids): res = self.search([["code", "=", obj.code], ["state", "=", "approved"], ["company_id", "=", obj.company_id.id]]) if res: repl_id = res[0] obj.copy(context={"replace_id": repl_id}) obj.write({"active": False}) else: vals = { "state": "approved", } if not obj.parent_id: group_ids = [] #XXX for group in obj.groups: group_ids.append(group.id) res = get_model("product.group").search( [["code", "=", "new"]]) if res and res[0] not in group_ids: group_ids.append(res[0]) vals.update({ "is_published": True, "groups": [("set", group_ids)] }) if not obj.approve_date: t = time.strftime("%Y-%m-%d %H:%M:%S") vals.update({"approve_date": t}) obj.write(vals) return { "flash": "Products approved", } def ecom_preview(self, ids, context={}): prod_id = ids[0] return { "next": { "type": "url", "url": "/ecom_product?product_id=%s" % prod_id, } } def get_sale_price_order_uom(self, ids, context={}): vals = {} for obj in self.browse(ids): factor = obj.sale_to_invoice_uom_factor or 1 vals[obj.id] = math.ceil((obj.sale_price or 0) * factor) return vals def create_thumbnails(self, ids, context={}): print("Product.create_thumbnails", ids) for obj in self.browse(ids): if not obj.image: continue dbname = database.get_active_db() if not dbname: return None fdir = os.path.join(os.getcwd(), "static", "db", dbname, "files") path = os.path.join(fdir, obj.image) basename, ext = os.path.splitext(obj.image) res = "," in basename if not res: rand = base64.urlsafe_b64encode(os.urandom(8)).decode() res = os.path.splitext(obj.image) basename, ext = res fname2 = basename + "," + rand + ext #rename image dest_path = fdir + "/" + fname2 print("destination path and file name ", dest_path) cmd = "cp %s %s" % (path, dest_path) os.system(cmd) obj.write({ 'image': fname2, }) utils.create_thumbnails(fname2) else: print("called", obj.image) utils.create_thumbnails(obj.image)
class StatementLine(Model): _name = "account.statement.line" _order = "date,id" _name_field = "description" _fields = { "statement_id": fields.Many2One("account.statement", "Statement", required=True, on_delete="cascade"), "state": fields.Selection([["not_reconciled", "Not Reconciled"], ["reconciled", "Reconciled"]], "Status", required=True), "date": fields.Date("Date", required=True), "description": fields.Char("Description", size=256), "spent": fields.Decimal("Spent", required=True), "received": fields.Decimal("Received", required=True), "balance": fields.Decimal("Balance", required=True, readonly=True), "bank_reconcile_id": fields.Many2One("account.bank.reconcile", "Bank Reconciliation"), "move_lines": fields.Many2Many("account.move.line", "Account Entries"), "account_id": fields.Many2One("account.account", "Account", function="_get_related", function_context={"path": "statement_id.account_id"}), "account_balance": fields.Decimal("Accounting Balance", function="get_account_balance"), } _defaults = { "state": "not_reconciled", "spent": 0, "received": 0, } def get_reconcile_lines(self, ids): st_line_ids = set(ids) new_st_line_ids = set(ids) acc_line_ids = set() while 1: new_acc_line_ids = set() for st_line in get_model("account.statement.line").browse( list(new_st_line_ids)): for acc_line in st_line.move_lines: if acc_line.id not in acc_line_ids and acc_line.id not in new_acc_line_ids: new_acc_line_ids.add(acc_line.id) if not new_acc_line_ids: break acc_line_ids |= new_acc_line_ids new_st_line_ids = set() for acc_line in get_model("account.move.line").browse( list(new_acc_line_ids)): for st_line in acc_line.statement_lines: if st_line.id not in st_line_ids and st_line.id not in new_st_line_ids: new_st_line_ids.add(st_line.id) if not new_st_line_ids: break st_line_ids |= new_st_line_ids return list(st_line_ids), list(acc_line_ids) def reconcile(self, ids, context={}): st_line_ids, acc_line_ids = self.get_reconcile_lines(ids) total_st = 0 for st_line in get_model("account.statement.line").browse(st_line_ids): total_st += st_line.received - st_line.spent total_acc = 0 for acc_line in get_model("account.move.line").browse(acc_line_ids): total_acc += acc_line.debit - acc_line.credit if total_st - total_acc != 0: return { "next": { "name": "reconcile_adjust", "line_id": ids[0], } } get_model("account.statement.line").write(st_line_ids, {"state": "reconciled"}) get_model("account.move.line").write(acc_line_ids, {"state": "reconciled"}) def unreconcile(self, ids, context={}): st_line_ids, acc_line_ids = self.get_reconcile_lines(ids) get_model("account.statement.line").write(st_line_ids, {"state": "not_reconciled"}) get_model("account.move.line").write(acc_line_ids, {"state": "not_reconciled"}) def get_account_balance(self, ids, context={}): vals = {} for obj in self.browse(ids): bal = 0 line_ids = set() for line in obj.move_lines: if line.id in line_ids: continue bal += line.debit - line.credit line_ids.add(line.id) vals[obj.id] = bal return vals def onchange_move_lines(self, context={}): data = context["data"] move_line_ids = data["move_lines"] move_line_ids = list(set(move_line_ids)) bal = 0 for line in get_model("account.move.line").browse(move_line_ids): bal += line.debit - line.credit data["account_balance"] = bal return data
class PurchaseOrderLine(Model): _name = "purchase.order.line" _name_field = "order_id" _fields = { "order_id": fields.Many2One("purchase.order", "Purchase Order", required=True, on_delete="cascade"), "product_id": fields.Many2One("product", "Product"), "description": fields.Text("Description", required=True), "qty": fields.Decimal("Qty", required=True, scale=6), "uom_id": fields.Many2One("uom", "UoM", required=True), "unit_price": fields.Decimal("Unit Price", required=True, scale=6), "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"), "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1), "amount_cur": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1), "qty_received": fields.Decimal("Received Qty", function="get_qty_received"), "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"), "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}), "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}), "sale_id": fields.Many2One("sale.order", "Sales Order"), "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]), "product_categs": fields.Many2Many("product.categ", "Product Categories", function="_get_related", function_context={"path": "product_id.categs"}, function_search="_search_related", search=True), "product_categ_id": fields.Many2Many("product.categ", "Product Category", function="_get_related", function_context={"path": "product_id.categ_id"}, function_search="_search_related", search=True), "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]), "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), "discount_amount": fields.Decimal("Disc Amt"), "qty_stock": fields.Decimal("Qty (Stock UoM)"), } _order = "order_id desc,id" def create(self, vals, context={}): id = super(PurchaseOrderLine, self).create(vals, context) self.function_store([id]) return id def write(self, ids, vals, context={}): super(PurchaseOrderLine, self).write(ids, vals, context) self.function_store(ids) def get_amount(self, ids, context={}): settings = get_model("settings").browse(1) vals = {} for line in self.browse(ids): uom_factor=line.product_id.purchase_to_stock_uom_factor amt=line.qty * line.unit_price if uom_factor: amt=amt*uom_factor amt = amt-(line.discount_amount or 0) order = line.order_id vals[line.id] = { "amount": amt, "amount_cur": get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id), } return vals def get_qty_received(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("purchase.order").browse(order_ids): received_qtys = {} for move in order.stock_moves: if move.state != "done": continue prod_id = move.product_id.id k = (prod_id, move.location_to_id.id) received_qtys.setdefault(k, 0) received_qtys[k] += move.qty # XXX: uom k = (prod_id, move.location_from_id.id) received_qtys.setdefault(k, 0) received_qtys[k] -= move.qty # XXX: uom for line in order.lines: if line.id not in ids: continue k = (line.product_id.id, line.location_id.id) received_qty = received_qtys.get(k, 0) # XXX: uom used_qty = min(line.qty, received_qty) vals[line.id] = used_qty if k in received_qtys: received_qtys[k] -= used_qty for line in reversed(order.lines): k = (line.product_id.id, line.location_id.id) remain_qty = received_qtys.get(k, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty received_qtys[k] -= remain_qty vals = {x: vals[x] for x in ids} return vals def get_qty_invoiced(self, ids, context={}): order_ids = [] for obj in self.browse(ids): order_ids.append(obj.order_id.id) order_ids = list(set(order_ids)) vals = {} for order in get_model("purchase.order").browse(order_ids): inv_qtys = {} for inv in order.invoices: if inv.state not in ("draft","waiting_payment","paid"): continue for line in inv.lines: prod_id = line.product_id.id inv_qtys.setdefault(prod_id, 0) inv_qtys[prod_id] += line.qty for line in order.lines: if line.id not in ids: continue prod_id = line.product_id.id inv_qty = inv_qtys.get(prod_id, 0) # XXX: uom used_qty = min(line.qty, inv_qty) vals[line.id] = used_qty if prod_id in inv_qtys: inv_qtys[prod_id] -= used_qty for line in reversed(order.lines): prod_id = line.product_id.id remain_qty = inv_qtys.get(prod_id, 0) # XXX: uom if remain_qty: vals[line.id] += remain_qty inv_qtys[prod_id] -= remain_qty vals = {x: vals[x] for x in ids} return vals
class ProductCateg(Model): _name = "product.categ" _string = "Product Category" _export_field = "code" _multi_company = True _key = ["code"] _fields = { "name": fields.Char("Name", required=True, search=True), "code": fields.Char("Short Code", search=True), "parent_id": fields.Many2One("product.categ", "Parent Category"), "description": fields.Text("Description"), "comments": fields.One2Many("message", "related_id", "Comments"), "products": fields.One2Many("product", "categ_id", "Products", operator="child_of", condition=[["is_published", "=", True]]), "image": fields.File("Image"), "sub_categories": fields.One2Many("product.categ", "parent_id", "Sub Categories"), "num_products": fields.Integer("Number of products", function="get_num_products"), "gross_profit": fields.Decimal("Gross Profit (%)"), "sale_account_id": fields.Many2One("account.account", "Sales Account", condition=[["type", "!=", "view"]], multi_company=True), "sale_tax_id": fields.Many2One("account.tax.rate", "Sales Tax"), "purchase_account_id": fields.Many2One("account.account", "Purchase Account", condition=[["type", "!=", "view"]], multi_company=True), "purchase_tax_id": fields.Many2One("account.tax.rate", "Purchase Tax"), "cost_method": fields.Selection( [["standard", "Standard Cost"], ["average", "Weighted Average"], ["fifo", "FIFO"], ["lifo", "LIFO"]], "Costing Method"), "cogs_account_id": fields.Many2One("account.account", "Cost Of Goods Sold Account", condition=[["type", "!=", "view"]], multi_company=True), "stock_account_id": fields.Many2One("account.account", "Inventory Account", condition=[["type", "!=", "view"]], multi_company=True), "companies": fields.Many2Many("company", "Companies"), } _order = "name" def get_full_parent_name(self, obj_id): obj = self.browse(obj_id) full = [obj.name] while obj.parent_id: obj = obj.parent_id full.append(obj.name) name = "/".join(full[::-1]) return name def get_num_products(self, ids, context={}): vals = {} for obj in self.browse(ids): nums = 0 for product in obj.products: if not product.parent_id: nums += 1 vals[obj.id] = nums return vals def name_get(self, ids, context={}): if not access.check_permission(self._name, "read", ids): return [(id, "Permission denied") for id in ids] f_name = self._name_field or "name" f_image = self._image_field or "image" if f_image in self._fields: show_image = True fields = [f_name, f_image] else: show_image = False fields = [f_name] res = self.read(ids, fields) for r in res: r[f_name] = self.get_full_parent_name(r["id"]) if show_image: return [(r["id"], r[f_name], r[f_image]) for r in res] else: return [(r["id"], r[f_name]) for r in res] def update_sale_prices(self, ids, context={}): obj = self.browse(ids[0]) if not obj.gross_profit: raise Exception("Missing gross profit") n = 0 for prod in get_model("product").search_browse( [["categ_id", "=", obj.id]]): sale_price = round(prod.landed_cost / (1 - obj.gross_profit / 100), 2) prod.write({ "gross_profit": obj.gross_profit, "sale_price": sale_price }) n += 1 return { "flash": "%d products updated" % n, }
class Project(Model): _name = "project" _string = "Project" _audit_log = True _fields = { "name": fields.Char("Project Name", required=True, search=True), "number": fields.Char("Project Number", search=True), "contact_id": fields.Many2One("contact", "Customer", search=True), "start_date": fields.Date("Start Date", required=True), "end_date": fields.Date("End Date"), "product_id": fields.Many2One("product", "Product"), # XXX: deprecated "comments": fields.One2Many("message", "related_id", "Comments"), "documents": fields.One2Many("document", "related_id", "Documents"), "state": fields.Selection([["in_progress", "In Progress"], ["done", "Completed"], ["canceled", "Canceled"]], "Status", required=True), "jobs": fields.One2Many("job", "project_id", "Jobs"), "tasks": fields.One2Many("task", "project_id", "Tasks"), "work_time": fields.One2Many("work.time", "job_id", "Work Time"), "claims": fields.One2Many("product.claim", "project_id", "Claim Bills"), "borrows": fields.One2Many("product.borrow", "project_id", "Borrow Requests"), "description": fields.Text("Description"), "track_id": fields.Many2One("account.track.categ", "Actual Cost Tracking Code"), "track_balance": fields.Decimal("Tracking Balance", function="_get_related", function_context={"path": "track_id.balance"}), "sub_tracks": fields.One2Many("account.track.categ", None, "Actual Cost Sub-Tracking Codes", function="_get_related", function_context={"path": "track_id.sub_tracks"}), "est_track_id": fields.Many2One("account.track.categ", "Estimate Cost Tracking Code"), "est_track_balance": fields.Decimal("Tracking Balance", function="_get_related", function_context={"path": "est_track_id.balance"}), "est_sub_tracks": fields.One2Many("account.track.categ", None, "Est. Cost Sub-Tracking Codes", function="_get_related", function_context={"path": "est_track_id.sub_tracks"}), "issues": fields.One2Many("issue", "project_id", "Issues"), "resources": fields.Many2Many("service.resource", "Resources"), "milestones": fields.One2Many("project.milestone", "project_id", "Milestones"), } _order = "start_date" _defaults = { "start_date": lambda *a: time.strftime("%Y-%m-%d"), "state": "in_progress", } def copy(self, ids, context={}): obj = self.browse(ids[0]) vals = { "name": obj.name, "number": obj.number, "contact_id": obj.contact_id.id, "start_date": obj.start_date, "end_date": obj.end_date, "description": description, "resources": [("set", [r.id for r in obj.resources])], } new_proj_id = self.create(vals, context=context) new_proj = self.browse(new_proj_id) track = obj.track_id if track: vals = { "name": track.name, # XXX "type": track.type, "code": track.code, # XXX } new_track_id = get_model("account.track.categ").create(vals) new_proj.write({"track_id": new_track_id}) for subtrack in track.sub_tracks: vals = { "parent_id": new_track_id, "name": subtrack.name, "type": subtrack.type, "code": subtrack.code, } get_model("account.track.categ").create(vals) return { "next": { "name": "project", "mode": "form", "active_id": new_proj_id, }, "flash": "New project copied from %s" % obj.name, }
class Employee(Model): _name = "hr.employee" _string = "Employee" _name_field = "first_name" # XXX _multi_company = True _key = ["code", "company_id"] _export_field = "code" _fields = { "code": fields.Char("Employee Code", search=True), "department_id": fields.Many2One("hr.department", "Department", search=True), "title": fields.Selection( [["mr", "Mr."], ["mrs", "Mrs."], ["miss", "Miss"], ["ms", "Ms."]], "Title"), "first_name": fields.Char("First Name", search=True, translate=True), "last_name": fields.Char("Last Name", required=True, search=True, translate=True), "hire_date": fields.Date("Hire Date"), "work_status": fields.Selection([["working", "Working"], ["dismissed", "Dismissed"], ["resigned", "Resigned"], ["died", "Died"]], "Work Status"), "work_type": fields.Selection( [["monthly", "Monthly"], ["daily", "Daily"], ["hourly", "Job"]], "Work Type"), "resign_date": fields.Date("Resign Date"), "position": fields.Char("Position", search=True), "birth_date": fields.Date("Birth Date"), "age": fields.Integer("Age", function="get_age"), "gender": fields.Selection([["male", "Male"], ["female", "Female"]], "Gender"), "marital_status": fields.Selection([["single", "Single"], ["married", "Married"], ["divorced", "Divorced"], ["widowed", "Widowed"]], "Marital Status"), "addresses": fields.One2Many("address", "employee_id", "Address"), "id_no": fields.Char("ID No."), "drive_license_type": fields.Selection([["car", "Car"], ['motorcycle', 'Motorcycle']], "Driving License"), "drive_license_no": fields.Char("Driving License No."), "country_id": fields.Many2One("country", "Country"), "bank_account": fields.Char("Bank Account"), "salary": fields.Decimal("Salary"), "picture": fields.File("Picture"), "tax_no": fields.Char("Taxpayer ID No."), "spouse_first_name": fields.Char("Spouse First Name"), "spouse_last_name": fields.Char("Spouse Last Name"), "spouse_title": fields.Selection([["mr", "Mr."], ["ms", "Ms."]], "Spouse Title"), "spouse_birth_date": fields.Date("Spouse Birth Date"), "spouse_tax_no": fields.Char("Spouse Tax ID No"), "spouse_status": fields.Selection( [["married", "Married existed throughout this tax year"], ["married_new", "Married during this tax year"], ["divorced", "Divorced during tax year"], ["deceased", "Deceased during tax year"]], "Spouse Status"), "spouse_filing_status": fields.Selection( [["joint", "Has income and file joint return"], ["separate", "Has income and file separate tax return"], ["no_income", "Has no income"]], "Spouse Filing Status"), "num_child1": fields.Integer("No. of Children #1 (C3)"), "num_child2": fields.Integer("No. of Children #2 (C3)"), "social_no": fields.Char("Social No."), "social_register": fields.Boolean("Register Soc. Secur."), "social_calc_method": fields.Selection( [["regular", "Regular Rate"], ["none", "Not Participate"], ["special", "Special Rate"]], "Calc. Method"), "prov_fund_no": fields.Char("Prov. Fund No."), "prov_open_date": fields.Char("Opened Prov. Fund A/C Date"), "prov_rate_employer": fields.Decimal("Employer Contribution (%)"), "prov_rate_employee": fields.Decimal("Employee Contribution (%)"), "gov_pension_fund": fields.Decimal("Gov. Pension Fund Amount (B2)"), "teacher_fund": fields.Decimal("Teacher Aid Fund Amount (B3)"), "old_disabled": fields.Decimal("Older than 65 or disabled (personal, B4)"), "old_disabled_spouse": fields.Decimal("Older than 65 or disabled (spouse, B5)"), "severance_pay": fields.Decimal("Severance Pay (B6)"), "education_donation": fields.Decimal("Education Donations (A8)"), "other_donation": fields.Decimal("Other Donations (A10)"), "house_deduct": fields.Decimal("Exemption for home buyer (A13)"), "wht_amount": fields.Decimal("Withholding Tax Amount (A15)"), "father_id_no": fields.Char("Father ID No. (C4)"), "mother_id_no": fields.Char("Mother ID No. (C4)"), "spouse_father_id_no": fields.Char("Father of spouse ID No. (C4)"), "spouse_mother_id_no": fields.Char("Mother of spouse ID No. (C4)"), "disabled_support": fields.Decimal("Disabled person support (C5)"), "parent_health_insurance": fields.Decimal("Parent Health Insurance (C6)"), "life_insurance": fields.Decimal("Life Insurance (C7)"), "retirement_mutual_fund": fields.Decimal("Retirement Mutual Fund (C9)"), "long_term_equity_fund": fields.Decimal("Long Term Equity Fund (C10)"), "interest_residence": fields.Decimal("Interest paid for residence (C11)"), "other_deduct": fields.Decimal("Other Deductions (C12)"), "comments": fields.One2Many("message", "related_id", "Comments"), "active": fields.Boolean("Active"), "time_in": fields.DateTime("Last Sign In", function="get_attend", function_multi=True), "time_out": fields.DateTime("Last Sign Out", function="get_attend", function_multi=True), "attend_state": fields.Selection([["absent", "Absent"], ["present", "Present"]], "Status", function="get_attend", function_multi=True), "user_id": fields.Many2One("base.user", "User", search=True), "payslips": fields.One2Many("hr.payslip", "employee_id", "Payslips"), "documents": fields.One2Many("document", "related_id", "Documents"), "phone": fields.Char("Phone", search=True), "approver_id": fields.Many2One("base.user", "Approver"), "company_id": fields.Many2One("company", "Company"), "leave_types": fields.Many2Many("hr.leave.type", "Leave Types"), "attendance_id": fields.Integer("Attendance ID"), "email": fields.Char("Email", search=True), 'profile_id': fields.Many2One("hr.payitem.profile", "Pay Item Profile"), 'schedule_id': fields.Many2One("hr.schedule", "Working Schedule"), 'leaves': fields.One2Many('hr.leave', 'employee_id', 'Leaves'), } def _get_code(self, context={}): while 1: code = get_model("sequence").get_number("employee") if not code: return None res = self.search([["code", "=", code]]) if not res: return code get_model("sequence").increment("employee") _defaults = { "active": True, "work_status": "working", "code": _get_code, "company_id": lambda *a: get_active_company(), } _order = "code,first_name,last_name" def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): if obj.first_name: name = obj.first_name + " " + obj.last_name else: name = obj.last_name if obj.code: name += " [%s]" % obj.code vals.append((obj.id, name)) return vals def name_search(self, name, condition=[], limit=None, context={}): cond = [[ "or", ["first_name", "ilike", "%" + name + "%"], ["last_name", "ilike", "%" + name + "%"], ["code", "ilike", "%" + name + "%"] ], condition] ids = self.search(cond, limit=limit) return self.name_get(ids, context) def get_age(self, ids, context={}): vals = {} cr_year = int(time.strftime('%Y')) for obj in self.browse(ids): if obj.birth_date: age = cr_year - int(obj.birth_date[0:4]) else: age = 0 vals[obj.id] = age return vals def get_attend(self, ids, context={}): vals = {} for obj in self.browse(ids): # user_id=obj.user_id.id # if user_id: # db=database.get_connection() #res=db.get("SELECT MAX(time) AS time_in FROM hr_attendance WHERE user_id=%s AND action='sign_in'",user_id) # time_in=res.time_in #res=db.get("SELECT MAX(time) AS time_out FROM hr_attendance WHERE user_id=%s AND action='sign_out'",user_id) # time_out=res.time_out # else: # time_in=None # time_out=None db = database.get_connection() res = db.get( "SELECT MAX(time) AS time_in FROM hr_attendance WHERE employee_id=%s AND action='sign_in'", obj.id) time_in = res.time_in res = db.get( "SELECT MAX(time) AS time_out FROM hr_attendance WHERE employee_id=%s AND action='sign_out'", obj.id) time_out = res.time_out if time_in: if time_out and time_out > time_in: state = "absent" else: today = time.strftime("%Y-%m-%d") if time_in.startswith(today): state = "present" else: state = "absent" # should not show timeout of anotherday # if not time_out.startswith(today): # time_out=None else: state = "absent" vals[obj.id] = { "time_in": time_in, "time_out": time_out, "attend_state": state, } return vals def get_address(self, ids, context={}): obj = self.browse(ids)[0] if not obj.addresses: return "" addr = obj.addresses[0] res = addr.get_address_text() return res[addr.id] def onchange_num_child(self, context={}): data = context["data"] setting = get_model("hr.payroll.settings").browse(1) child_alw_limit = setting.child_alw_limit or 0 child_total = (data['num_child1'] or 0) + (data['num_child2'] or 0) if child_alw_limit and child_total > child_alw_limit: data['num_child1'] = 0 data['num_child2'] = 0 return data
class Resource(Model): _name = "service.resource" _string = "Resource" _fields = { "name": fields.Char("Name", required=True, search=True), "employee_id": fields.Many2One("hr.employee", "Employee", search=True), "product_categs": fields.Many2Many("product.categ", "Product Categories"), "regions": fields.Many2Many("region", "Regions"), "skill_level_id": fields.Many2One("skill.level", "Skill Level"), "allocs": fields.One2Many("service.resource.alloc", "resource_id", "Resource Allocations"), "comments": fields.One2Many("message", "related_id", "Comments"), "can_alloc": fields.Boolean("Can Allocate", function="get_can_alloc"), "is_avail": fields.Boolean("Is Available", store=False, function_search="is_avail_search"), "documents": fields.One2Many("document", "related_id", "Documents"), "time_sheets": fields.One2Many("time.sheet", "resource_id", "Time Sheets"), "user_id": fields.Many2One("base.user", "User"), "type": fields.Selection([["person", "Person"], ["machine", "Machine"]], "Resource Type"), "product_id": fields.Many2One("product", "Product"), } _order = "name" def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids, context=context): name = obj.name can_alloc = obj.can_alloc if can_alloc == False: name += " [WARNING]" vals.append((obj.id, name)) return vals def get_can_alloc(self, ids, context={}): print("get_can_alloc", ids, context) job_id = context.get("job_id") if not job_id: return {id: None for id in ids} job = get_model("job").browse(job_id) prod_ids = [] for item in job.items: sitem = item.service_item_id if sitem.product_id: prod_ids.append(sitem.product_id.id) print("prod_ids", prod_ids) vals = {} for obj in self.browse(ids): if job.skill_level_id: if obj.skill_level_id and obj.skill_level_id.level < job.skill_level_id.level: vals[obj.id] = False continue if obj.product_categs and prod_ids: categ_ids = [c.id for c in obj.product_categs] res = get_model("product").search( [["id", "in", prod_ids], ["categs.id", "child_of", categ_ids]]) if not res: vals[obj.id] = False continue region = job.contact_id.region_id if obj.regions and region: region_ids = [r.id for r in obj.regions] if region.id not in region_ids: vals[obj.id] = False continue vals[obj.id] = True return vals def is_avail_search(self, clause, context={}): print("is_avail_search", clause, context) time_start = context.get("time_start") time_stop = context.get("time_stop") job_id = context.get("job_id") if job_id: job = get_model("job").browse(job_id) if job.state == "planned": job.write({"state": "allocated"}) if not time_start or not time_stop: return [] ids = [] for alloc in get_model("service.resource.alloc").search_browse( [["time_stop", ">", time_start], ["time_start", "<", time_stop]]): ids.append(alloc.resource_id.id) ids = list(set(ids)) return [["id", "not in", ids]]
class PurchaseOrder(Model): _name = "purchase.order" _string = "Purchase Order" _audit_log = True _name_field = "number" _multi_company = True _key = ["company_id", "number"] _fields = { "number": fields.Char("Number", required=True, search=True), "ref": fields.Char("Ref", search=True), "contact_id": fields.Many2One("contact", "Supplier", required=True, search=True), "customer_id": fields.Many2One("contact", "Customer", search=True), "date": fields.Date("Date", required=True, search=True), "date_required": fields.Date("Required Date"), "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", required=True), "lines": fields.One2Many("purchase.order.line", "order_id", "Lines"), "amount_subtotal": fields.Decimal("Subtotal", function="get_amount", function_multi=True, store=True), "amount_tax": fields.Decimal("Tax Amount", function="get_amount", function_multi=True, store=True), "amount_total": fields.Decimal("Total", function="get_amount", function_multi=True, store=True), "amount_total_cur": fields.Decimal("Total", function="get_amount", function_multi=True, store=True), "amount_total_words": fields.Char("Total Words", function="get_amount_total_words"), "qty_total": fields.Decimal("Total Quantity", function="get_qty_total"), "currency_id": fields.Many2One("currency", "Currency", required=True), "tax_type": fields.Selection([["tax_ex", "Tax Exclusive"], ["tax_in", "Tax Inclusive"], ["no_tax", "No Tax"]], "Tax Type", required=True), "invoice_lines": fields.One2Many("account.invoice.line", "related_id", "Invoice Lines"), #"stock_moves": fields.One2Many("stock.move","purch_id","Stock Moves"), "invoices": fields.Many2Many("account.invoice", "Invoices", function="get_invoices"), "pickings": fields.Many2Many("stock.picking", "Stock Pickings", function="get_pickings"), "is_delivered": fields.Boolean("Delivered", function="get_delivered"), "is_paid": fields.Boolean("Paid", function="get_paid"), "comments": fields.One2Many("message", "related_id", "Comments"), "location_id": fields.Many2One("stock.location", "Warehouse"), # XXX: deprecated "delivery_date": fields.Date("Delivery Date"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), # XXX: deprecated "payment_terms": fields.Text("Payment Terms"), "ship_term_id": fields.Many2One("ship.term", "Shipping Terms"), "price_list_id": fields.Many2One("price.list", "Price List", condition=[["type", "=", "purchase"]]), "documents": fields.One2Many("document", "related_id", "Documents"), "company_id": fields.Many2One("company", "Company"), "purchase_type_id": fields.Many2One("purchase.type", "Purchase Type"), "other_info": fields.Text("Other Info"), "bill_address_id": fields.Many2One("address", "Billing Address"), "ship_address_id": fields.Many2One("address", "Shipping Address"), "sequence_id": fields.Many2One("sequence", "Number Sequence"), "stock_moves": fields.One2Many("stock.move", "related_id", "Stock Movements"), "agg_amount_total": fields.Decimal("Total Amount", agg_function=["sum", "amount_total"]), "year": fields.Char("Year", sql_function=["year", "date"]), "quarter": fields.Char("Quarter", sql_function=["quarter", "date"]), "month": fields.Char("Month", sql_function=["month", "date"]), "week": fields.Char("Week", sql_function=["week", "date"]), "agg_amount_subtotal": fields.Decimal("Total Amount w/o Tax", agg_function=["sum", "amount_subtotal"]), "user_id": fields.Many2One("base.user", "Owner", search=True), "emails": fields.One2Many("email.message", "related_id", "Emails"), "related_id": fields.Reference([["sale.order", "Sales Order"], ["stock.consign", "Consignment Stock"]], "Related To"), } _order = "date desc,number desc" _sql_constraints = [("key_uniq", "unique (company_id, number)", "The number of each company must be unique!")] def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence(type="purchase_order") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id) user_id = get_active_user() set_active_user(1) res = self.search([["number", "=", num]]) set_active_user(user_id) if not res: return num get_model("sequence").increment_number(seq_id) def _get_currency(self, context={}): settings = get_model("settings").browse(1) return settings.currency_id.id _defaults = { "state": "draft", "date": lambda *a: time.strftime("%Y-%m-%d"), "number": _get_number, "currency_id": _get_currency, "tax_type": "tax_ex", "company_id": lambda *a: get_active_company(), "user_id": lambda *a: get_active_user(), } def create(self, vals, **kw): id = super(PurchaseOrder, self).create(vals, **kw) self.function_store([id]) return id def write(self, ids, vals, **kw): super(PurchaseOrder, self).write(ids, vals, **kw) self.function_store(ids) def confirm(self, ids, context={}): settings = get_model("settings").browse(1) for obj in self.browse(ids): if obj.state != "draft": raise Exception("Invalid state") for line in obj.lines: prod = line.product_id if prod and prod.type in ("stock", "consumable", "bundle") and not line.location_id: raise Exception("Missing location for product %s" % prod.code) obj.write({"state": "confirmed"}) if settings.purchase_copy_picking: res = obj.copy_to_picking() picking_id = res["picking_id"] get_model("stock.picking").pending([picking_id]) if settings.purchase_copy_invoice: obj.copy_to_invoice() obj.trigger("confirm") def done(self, ids, context={}): for obj in self.browse(ids): if obj.state != "confirmed": raise Exception("Invalid state") obj.write({"state": "done"}) def reopen(self, ids, context={}): for obj in self.browse(ids): if obj.state != "done": raise Exception("Invalid state") obj.write({"state": "confirmed"}) def to_draft(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "draft"}) def get_amount(self, ids, context={}): settings = get_model("settings").browse(1) res = {} for obj in self.browse(ids): vals = {} subtotal = 0 tax = 0 for line in obj.lines: if line.tax_id: line_tax = get_model("account.tax.rate").compute_tax( line.tax_id.id, line.amount, tax_type=obj.tax_type) else: line_tax = 0 tax += line_tax if obj.tax_type == "tax_in": subtotal += line.amount - line_tax else: subtotal += line.amount vals["amount_subtotal"] = subtotal vals["amount_tax"] = tax vals["amount_total"] = subtotal + tax vals["amount_total_cur"] = get_model("currency").convert( vals["amount_total"], obj.currency_id.id, settings.currency_id.id) res[obj.id] = vals return res def get_qty_total(self, ids, context={}): res = {} for obj in self.browse(ids): qty = sum([line.qty for line in obj.lines]) res[obj.id] = qty or 0 return res def update_amounts(self, context): data = context["data"] data["amount_subtotal"] = 0 data["amount_tax"] = 0 tax_type = data["tax_type"] for line in data["lines"]: if not line: continue amt = Decimal(((line.get("qty") or 0) * (line.get("unit_price") or 0)) - (line.get("discount_amount") or 0)) line["amount"] = amt tax_id = line.get("tax_id") if tax_id: tax = get_model("account.tax.rate").compute_tax( tax_id, amt, tax_type=tax_type) data["amount_tax"] += tax else: tax = 0 if tax_type == "tax_in": data["amount_subtotal"] += amt - tax else: data["amount_subtotal"] += amt data["amount_total"] = data["amount_subtotal"] + data["amount_tax"] return data def onchange_product(self, context): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) prod_id = line.get("product_id") if not prod_id: return {} prod = get_model("product").browse(prod_id) line["description"] = prod.description line["qty"] = 1 if prod.uom_id is not None: line["uom_id"] = prod.uom_id.id pricelist_id = data["price_list_id"] price = None if pricelist_id: price = get_model("price.list").get_price(pricelist_id, prod.id, 1) price_list = get_model("price.list").browse(pricelist_id) price_currency_id = price_list.currency_id.id if price is None: price = prod.purchase_price settings = get_model("settings").browse(1) price_currency_id = settings.currency_id.id if price is not None: currency_id = data["currency_id"] price_cur = get_model("currency").convert(price, price_currency_id, currency_id) line["unit_price"] = price_cur if prod.purchase_tax_id is not None: line["tax_id"] = prod.purchase_tax_id.id if prod.location_id: line["location_id"] = prod.location_id.id data = self.update_amounts(context) return data def onchange_qty(self, context): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) prod_id = line.get("product_id") if not prod_id: return {} prod = get_model("product").browse(prod_id) pricelist_id = data["price_list_id"] qty = line["qty"] price = None if pricelist_id: price = get_model("price.list").get_price(pricelist_id, prod.id, qty) price_list = get_model("price.list").browse(pricelist_id) price_currency_id = price_list.currency_id.id if price is None: price = prod.purchase_price settings = get_model("settings").browse(1) price_currency_id = settings.currency_id.id if price is not None: currency_id = data["currency_id"] price_cur = get_model("currency").convert(price, price_currency_id, currency_id) line["unit_price"] = price_cur data = self.update_amounts(context) return data def copy_to_picking(self, ids, context={}): settings = get_model("settings").browse(1) obj = self.browse(ids[0]) contact = obj.contact_id pick_vals = { "type": "in", "ref": obj.number, "related_id": "purchase.order,%s" % obj.id, "contact_id": contact.id, "currency_id": obj.currency_id.id, "lines": [], } if obj.delivery_date: pick_vals["date"] = obj.delivery_date if contact and contact.pick_in_journal_id: pick_vals["journal_id"] = contact.pick_in_journal_id.id res = get_model("stock.location").search([["type", "=", "supplier"]], order="id") if not res: raise Exception("Supplier location not found") supp_loc_id = res[0] res = get_model("stock.location").search([["type", "=", "internal"]]) if not res: raise Exception("Warehouse not found") wh_loc_id = res[0] if not settings.currency_id: raise Exception("Missing currency in financial settings") for line in obj.lines: prod = line.product_id if prod.type not in ("stock", "consumable"): continue remain_qty = (line.qty_stock or line.qty) - line.qty_received if remain_qty <= 0: continue unit_price = line.amount / line.qty if line.qty else 0 if obj.tax_type == "tax_in": if line.tax_id: tax_amt = get_model("account.tax.rate").compute_tax( line.tax_id.id, unit_price, tax_type=obj.tax_type) else: tax_amt = 0 cost_price_cur = round(unit_price - tax_amt, 2) else: cost_price_cur = unit_price if line.qty_stock: purch_uom = prod.uom_id if not prod.purchase_to_stock_uom_factor: raise Exception( "Missing purchase order to stock UoM factor for product %s" % prod.code) cost_price_cur /= prod.purchase_to_stock_uom_factor else: purch_uom = line.uom_id cost_price = get_model("currency").convert(cost_price_cur, obj.currency_id.id, settings.currency_id.id) cost_amount = cost_price * remain_qty line_vals = { "product_id": prod.id, "qty": remain_qty, "uom_id": purch_uom.id, "cost_price_cur": cost_price_cur, "cost_price": cost_price, "cost_amount": cost_amount, "location_from_id": supp_loc_id, "location_to_id": line.location_id.id or wh_loc_id, "related_id": "purchase.order,%s" % obj.id, } pick_vals["lines"].append(("create", line_vals)) if not pick_vals["lines"]: raise Exception("Nothing left to receive") pick_id = get_model("stock.picking").create(pick_vals, {"pick_type": "in"}) pick = get_model("stock.picking").browse(pick_id) return { "next": { "name": "pick_in", "mode": "form", "active_id": pick_id, }, "flash": "Goods receipt %s created from purchase order %s" % (pick.number, obj.number), "picking_id": pick_id, } def copy_to_invoice(self, ids, context={}): id = ids[0] obj = self.browse(id) contact = obj.contact_id inv_vals = { "type": "in", "inv_type": "invoice", "ref": obj.number, "related_id": "purchase.order,%s" % obj.id, "contact_id": obj.contact_id.id, "currency_id": obj.currency_id.id, "lines": [], "tax_type": obj.tax_type, } if contact.purchase_journal_id: inv_vals["journal_id"] = contact.purchase_journal_id.id if contact.purchase_journal_id.sequence_id: inv_vals[ "sequence_id"] = contact.purchase_journal_id.sequence_id.id for line in obj.lines: prod = line.product_id remain_qty = line.qty - line.qty_invoiced if remain_qty <= 0: continue purch_acc_id = None if prod: purch_acc_id = prod.purchase_account_id.id if not purch_acc_id and prod.parent_id: purch_acc_id = prod.parent_id.purchase_account_id.id if not purch_acc_id and prod.categ_id and prod.categ_id.purchase_account_id: purch_acc_id = prod.categ_id.purchase_account_id.id line_vals = { "product_id": prod.id, "description": line.description, "qty": remain_qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "account_id": prod and prod.purchase_account_id.id or None, "tax_id": line.tax_id.id, "amount": line.amount, } inv_vals["lines"].append(("create", line_vals)) if not inv_vals["lines"]: raise Exception("Nothing left to invoice") inv_id = get_model("account.invoice").create(inv_vals, { "type": "in", "inv_type": "invoice" }) inv = get_model("account.invoice").browse(inv_id) return { "next": { "name": "view_invoice", "active_id": inv_id, }, "flash": "Invoice %s created from purchase order %s" % (inv.number, obj.number), } def get_delivered(self, ids, context={}): vals = {} #import pdb; pdb.set_trace() for obj in self.browse(ids): is_delivered = True for line in obj.lines: prod = line.product_id if prod.type not in ("stock", "consumable"): continue remain_qty = line.qty - line.qty_received if remain_qty > 0: is_delivered = False break vals[obj.id] = is_delivered return vals def get_paid(self, ids, context={}): vals = {} for obj in self.browse(ids): amt_paid = 0 for inv_line in obj.invoice_lines: inv = inv_line.invoice_id if inv.state != "paid": continue amt_paid += inv_line.amount is_paid = amt_paid >= obj.amount_subtotal vals[obj.id] = is_paid return vals def void(self, ids, context={}): obj = self.browse(ids)[0] for pick in obj.pickings: if pick.state != "voided": raise Exception( "There are still goods receipts for this purchase order") for inv in obj.invoices: if inv.state != "voided": raise Exception( "There are still invoices for this purchase order") obj.write({"state": "voided"}) def copy(self, ids, context): obj = self.browse(ids)[0] vals = { "contact_id": obj.contact_id.id, "date": obj.date, "ref": obj.ref, "currency_id": obj.currency_id.id, "tax_type": obj.tax_type, "lines": [], } for line in obj.lines: line_vals = { "product_id": line.product_id.id, "description": line.description, "qty": line.qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "tax_id": line.tax_id.id, } vals["lines"].append(("create", line_vals)) new_id = self.create(vals) new_obj = self.browse(new_id) return { "next": { "name": "purchase", "mode": "form", "active_id": new_id, }, "flash": "Purchase order %s copied to %s" % (obj.number, new_obj.number), } def get_invoices(self, ids, context={}): vals = {} for obj in self.browse(ids): inv_ids = [] for inv_line in obj.invoice_lines: inv_id = inv_line.invoice_id.id if inv_id not in inv_ids: inv_ids.append(inv_id) vals[obj.id] = inv_ids return vals def get_pickings(self, ids, context={}): vals = {} for obj in self.browse(ids): pick_ids = [] for move in obj.stock_moves: pick_id = move.picking_id.id if pick_id not in pick_ids: pick_ids.append(pick_id) vals[obj.id] = pick_ids return vals def onchange_contact(self, context): data = context["data"] contact_id = data.get("contact_id") if not contact_id: return {} contact = get_model("contact").browse(contact_id) data["payment_terms"] = contact.payment_terms data["price_list_id"] = contact.purchase_price_list_id.id if contact.currency_id: data["currency_id"] = contact.currency_id.id else: settings = get_model("settings").browse(1) data["currency_id"] = settings.currency_id.id return data def check_received_qtys(self, ids, context={}): obj = self.browse(ids)[0] for line in obj.lines: if line.qty_received > (line.qty_stock or line.qty): raise Exception( "Can not receive excess quantity for purchase order %s and product %s (order qty: %s, received qty: %s)" % (obj.number, line.product_id.code, line.qty_stock or line.qty, line.qty_received)) def get_purchase_form_template(self, ids, context={}): obj = self.browse(ids)[0] if obj.state == "draft": return "rfq_form" else: return "purchase_form" def get_amount_total_words(self, ids, context={}): vals = {} for obj in self.browse(ids): amount_total_words = utils.num2word(obj.amount_total) vals[obj.id] = amount_total_words return vals def onchange_sequence(self, context={}): data = context["data"] seq_id = data["sequence_id"] if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id, context=context) res = self.search([["number", "=", num]]) if not res: break get_model("sequence").increment_number(seq_id, context=context) data["number"] = num return data def delete(self, ids, **kw): for obj in self.browse(ids): if obj.state in ("confirmed", "done"): raise Exception("Can not delete purchase order in this status") super().delete(ids, **kw)
class Contact(Model): _name = "contact" _string = "Contact" _audit_log = True _export_field = "name" _key = ["code"] _fields = { "user_id": fields.Many2One("base.user", "User"), "type": fields.Selection([["person", "Individual"], ["org", "Organization"]], "Contact Type", required=True, search=True), "customer": fields.Boolean("Customer", search=True), "supplier": fields.Boolean("Supplier", search=True), "name": fields.Char("Name", required=True, search=True, translate=True, size=256), "code": fields.Char("Code", search=True, required=True), "phone": fields.Char("Phone", search=True), "fax": fields.Char("Fax"), "website": fields.Char("Website"), "industry": fields.Char("Industry"), # XXX: deprecated "employees": fields.Char("Employees"), "revenue": fields.Char("Annual Revenue"), "description": fields.Text("Description"), "tax_no": fields.Char("Tax ID Number"), "tax_branch_no": fields.Char("Tax Branch Id"), "bank_account_no": fields.Char("Bank Account Number"), "bank_account_name": fields.Char("Bank Account Name"), "bank_account_details": fields.Char("Bank Account Details"), "active": fields.Boolean("Active"), "account_receivable_id": fields.Many2One("account.account", "Account Receivable", multi_company=True), "tax_receivable_id": fields.Many2One("account.tax.rate", "Account Receivable Tax"), "account_payable_id": fields.Many2One("account.account", "Account Payable", multi_company=True), "tax_payable_id": fields.Many2One("account.tax.rate", "Account Payable Tax"), "currency_id": fields.Many2One("currency", "Default Currency"), "payables_due": fields.Decimal("Payables Due"), "payables_overdue": fields.Decimal("Payables Overdue"), "receivables_due": fields.Decimal("Receivables Due"), "receivables_overdue": fields.Decimal("Receivables Overdue"), "payable_credit": fields.Decimal("Payable Credit", function="get_credit", function_multi=True), "receivable_credit": fields.Decimal("Receivable Credit", function="get_credit", function_multi=True), "invoices": fields.One2Many("account.invoice", "contact_id", "Invoices"), "sale_price_list_id": fields.Many2One("price.list", "Sales Price List", condition=[["type", "=", "sale"]]), "purchase_price_list_id": fields.Many2One("price.list", "Purchasing Price List", condition=[["type", "=", "purchase"]]), "categ_id": fields.Many2One("contact.categ", "Contact Category"), "payment_terms": fields.Char("Payment Terms"), "opports": fields.One2Many("sale.opportunity", "contact_id", "Open Opportunities", condition=[["state", "=", "open"]]), "addresses": fields.One2Many("address", "contact_id", "Addresses"), "comments": fields.One2Many("message", "related_id", "Comments"), "bank_accounts": fields.One2Many("bank.account", "contact_id", "Bank Accounts"), "last_name": fields.Char("Last Name"), "first_name": fields.Char("First Name"), "first_name2": fields.Char("First Name (2)"), "first_name3": fields.Char("First Name (3)"), "title": fields.Char("Title"), "position": fields.Char("Position"), "report_to_id": fields.Many2One("contact", "Reports To"), "mobile": fields.Char("Mobile"), "email": fields.Char("Email", search=True), "home_phone": fields.Char("Home Phone"), "other_phone": fields.Char("Other Phone"), "assistant": fields.Char("Assistant"), "assistant_phone": fields.Char("Assistant Phone"), "birth_date": fields.Date("Birth Date"), "department": fields.Char("Department"), "job_templates": fields.Many2Many("job.template", "Job Template"), "projects": fields.One2Many("project", "contact_id", "Projects"), "documents": fields.One2Many("document", "contact_id", "Documents"), "assigned_to_id": fields.Many2One("base.user", "Assigned To"), "lead_source": fields.Char("Lead source"), "inquiry_type": fields.Char("Type of inquiry"), "relations": fields.One2Many("contact.relation", "from_contact_id", "Relations", function="_get_relations"), "contact_id": fields.Many2One( "contact", "Parent"), # XXX: not used any more, just there for migration "emails": fields.One2Many("email.message", "name_id", "Emails"), "default_address_id": fields.Many2One("address", "Default Address", function="get_default_address"), "sale_orders": fields.One2Many("sale.order", "contact_id", "Sales Orders"), "country_id": fields.Many2One("country", "Country", search=True), "region": fields.Char("Region"), # XXX: deprecated "service_items": fields.One2Many("service.item", "contact_id", "Service Items", condition=[["parent_id", "=", None]]), "contracts": fields.One2Many("service.contract", "contact_id", "Contracts"), "branch": fields.Char("Branch"), # XXX: add by Cash "industry_id": fields.Many2One("industry", "Industry", search=True), "region_id": fields.Many2One("region", "Region", search=True), "commission_po_percent": fields.Decimal("Commission Purchase Percentage"), "business_area_id": fields.Many2One("business.area", "Business Area", search=True), "fleet_size_id": fields.Many2One("fleet.size", "Fleet Size", search=True), "groups": fields.Many2Many("contact.group", "Groups", search=True), "sale_journal_id": fields.Many2One("account.journal", "Sales Journal"), "purchase_journal_id": fields.Many2One("account.journal", "Purchase Journal"), "pay_in_journal_id": fields.Many2One("account.journal", "Receipts Journal"), "pay_out_journal_id": fields.Many2One("account.journal", "Disbursements Journal"), "pick_in_journal_id": fields.Many2One("stock.journal", "Goods Receipt Journal"), "pick_out_journal_id": fields.Many2One("stock.journal", "Goods Issue Journal"), "coupons": fields.One2Many("sale.coupon", "contact_id", "Coupons"), "companies": fields.Many2Many("company", "Companies"), "request_product_groups": fields.Many2Many("product.group", "Request Product Groups", reltable="m2m_contact_request_product_groups", relfield="contact_id", relfield_other="group_id"), "exclude_product_groups": fields.Many2Many("product.group", "Exclude Product Groups", reltable="m2m_contact_exclude_product_groups", relfield="contact_id", relfield_other="group_id"), "picture": fields.File("Picture"), "users": fields.One2Many("base.user", "contact_id", "Users"), "ship_free": fields.Boolean("Free Shipping"), } def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence(type="contact") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id, context=context) res = self.search([["code", "=", num]]) if not res: return num get_model("sequence").increment_number(seq_id, context=context) _defaults = { "active": True, "type": "person", "code": _get_number, } _order = "name" _constraints = ["check_email"] def create(self, vals, **kw): if not vals.get("type"): if vals.get("name"): vals["type"] = "org" elif vals.get("last_name"): vals["type"] = "person" if vals.get("type") == "person": if vals.get("first_name"): vals["name"] = vals["first_name"] + " " + vals["last_name"] else: vals["name"] = vals["last_name"] new_id = super().create(vals, **kw) return new_id def write(self, ids, vals, set_name=True, **kw): super().write(ids, vals, **kw) if set_name: for obj in self.browse(ids): if obj.type == "person": if obj.first_name: name = obj.first_name + " " + obj.last_name else: name = obj.last_name obj.write({"name": name}, set_name=False) def get_credit(self, ids, context={}): print("contact.get_credit", ids) currency_id = context.get("currency_id") print("currency_id", currency_id) vals = {} for obj in self.browse(ids): ctx = { "contact_id": obj.id, } r_credit = 0 p_credit = 0 for acc in get_model("account.account").search_browse( [["type", "=", "cust_deposit"]], context=ctx): r_credit -= acc.balance for acc in get_model("account.account").search_browse( [["type", "=", "sup_deposit"]], context=ctx): p_credit += acc.balance vals[obj.id] = { "receivable_credit": r_credit, "payable_credit": p_credit, # TODO } return vals def get_address_str(self, ids, context={}): obj = self.browse(ids[0]) if not obj.addresses: return "" addr = obj.addresses[0] return addr.name_get()[0][1] def _get_relations(self, ids, context={}): cond = [ "or", ["from_contact_id", "in", ids], ["to_contact_id", "in", ids] ] rels = get_model("contact.relation").search_read( cond, ["from_contact_id", "to_contact_id"]) vals = {} for rel in rels: from_id = rel["from_contact_id"][0] to_id = rel["to_contact_id"][0] vals.setdefault(from_id, []).append(rel["id"]) vals.setdefault(to_id, []).append(rel["id"]) return vals def get_address(self, ids, pref_type=None, context={}): obj = self.browse(ids)[0] for addr in obj.addresses: if pref_type and addr.type != pref_type: continue return addr.id if obj.addresses: return obj.addresses[0].id return None def get_default_address(self, ids, context={}): vals = {} for obj in self.browse(ids): addr_id = None for addr in obj.addresses: if addr.type == "billing": addr_id = addr.id break if not addr_id and obj.addresses: addr_id = obj.addresses[0].id vals[obj.id] = addr_id print("XXX", vals) return vals def check_email(self, ids, context={}): for obj in self.browse(ids): if not obj.email: continue if not utils.check_email_syntax(obj.email): raise Exception("Invalid email for contact '%s'" % obj.name) def find_address(self, ids, addr_vals, context={}): obj = self.browse(ids[0]) addr_id = None for addr in obj.addresses: if "address" in addr_vals and addr_vals["address"] != addr.address: continue if "address2" in addr_vals and addr_vals[ "address2"] != addr.address2: continue if "city" in addr_vals and addr_vals["city"] != addr.city: continue if "postal_code" in addr_vals and addr_vals[ "postal_code"] != addr.postal_code: continue if "country_id" in addr_vals and addr_vals[ "country_id"] != addr.country_id.id: continue if "province_id" in addr_vals and addr_vals[ "province_id"] != addr.province_id.id: continue if "district_id" in addr_vals and addr_vals[ "district_id"] != addr.district_id.id: continue if "subdistrict_id" in addr_vals and addr_vals[ "subdistrict_id"] != addr.subdistrict_id.id: continue if "phone" in addr_vals and addr_vals["phone"] != addr.phone: continue if "first_name" in addr_vals and addr_vals[ "phone"] != addr.first_name: continue if "last_name" in addr_vals and addr_vals[ "last_name"] != addr.last_name: continue addr_id = addr.id break return addr_id def add_address(self, ids, addr_vals, context={}): addr_id = self.find_address(ids) if not addr_id: vals = addr_vals.copy() vals["contact_id"] = ids[0] addr_id = get_model("address").create(vals) return addr_id