├── stock_delivered_unbilled ├── config │ └── __init__.py ├── public │ └── .gitkeep ├── www │ └── __init__.py ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py ├── __init__.py ├── modules.txt ├── stock_delivered_unbilled │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ └── repost_sales_invoice │ │ │ ├── __init__.py │ │ │ ├── repost_sales_invoice.js │ │ │ ├── repost_sales_invoice.py │ │ │ ├── test_repost_sales_invoice.py │ │ │ └── repost_sales_invoice.json │ └── overrides │ │ ├── delivery_note.py │ │ ├── repost_item_valuation.py │ │ ├── collect_dn_for_si_repost.py │ │ ├── sales_invoice.py │ │ └── get_basic_details.py ├── patches.txt ├── patches │ └── add_default_parking_account_field.py └── hooks.py ├── .gitignore ├── pyproject.toml ├── README.md └── license.txt /stock_delivered_unbilled/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/www/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/templates/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' -------------------------------------------------------------------------------- /stock_delivered_unbilled/modules.txt: -------------------------------------------------------------------------------- 1 | Stock Delivered Unbilled -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/repost_sales_invoice/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | node_modules 7 | __pycache__ -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/repost_sales_invoice/repost_sales_invoice.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Aerele Technologies Private Limited and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Repost Sales Invoice', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/repost_sales_invoice/repost_sales_invoice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Aerele Technologies Private Limited and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class RepostSalesInvoice(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/repost_sales_invoice/test_repost_sales_invoice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Aerele Technologies Private Limited and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestRepostSalesInvoice(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | # Patches added in this section will be executed before doctypes are migrated 3 | # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations 4 | stock_delivered_unbilled.patches.add_default_parking_account_field #Today 5 | 6 | [post_model_sync] 7 | # Patches added in this section will be executed after doctypes are migrated -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "stock_delivered_unbilled" 3 | authors = [ 4 | { name = "Aerele Technologies Private Limited", email = "hello@aerele.in"} 5 | ] 6 | description = "This app enables the parking account for Stock Delivered But Not Billed" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | # "frappe~=15.0.0" # Installed and managed by bench. 12 | ] 13 | 14 | [build-system] 15 | requires = ["flit_core >=3.4,<4"] 16 | build-backend = "flit_core.buildapi" 17 | 18 | # These dependencies are only installed when developer mode is enabled 19 | [tool.bench.dev-dependencies] 20 | # package_name = "~=1.1.0" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Stock Delivered Unbilled 2 | 3 | Like **Stock Received But Not Billed** in the Purchase Cycle, this app will enable the same scenario but in the Sales Cycle (**Stock Delivered But Not Billed**) when a delivery note is made but not yet billed. 4 | 5 | 6 | - Once the Delivery Note is submitted, 1000 SAR (as an example amount) is credited to the Stock Account and 1000 SAR is debited to the **Stock Delivered but Not Billed** Account. 7 | - Once the Sales Invoice is submitted, 1000 SAR is credited to the **Stock Delivered but Not Billed** Account, and 1000 SAR is debited to the **Cost Of Goods Sold** Account. 8 | - Due to back-dated stock entry, if Repost Item Valuation is created against entries in point 1, entries in point 2 will be reposted too. 9 | 10 | **Stock Delivered but Not Billed Account** (an Asset type account) is configurable in the Company doctype like Stock Received But Not Billed. 11 | 12 | #### License 13 | MIT License 14 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/patches/add_default_parking_account_field.py: -------------------------------------------------------------------------------- 1 | 2 | import frappe 3 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields 4 | 5 | def execute(): 6 | create_parking_account_field() 7 | create_disable_in_return_field() 8 | 9 | def create_parking_account_field(): 10 | custom_field = { 11 | "Company": [ 12 | dict( 13 | fieldname="stock_delivered_but_not_billed", 14 | label="Stock Delivered But Not Billed", 15 | fieldtype="Link", 16 | options="Account", 17 | insert_after="stock_received_but_not_billed", 18 | ignore_user_permissions=1, 19 | no_copy=1, 20 | ) 21 | ] 22 | } 23 | create_custom_fields(custom_field, ignore_validate=frappe.flags.in_patch, update=True) 24 | 25 | def create_disable_in_return_field(): 26 | custom_field = { 27 | "Company": [ 28 | dict( 29 | fieldname="disable_sdbnb_in_sr", 30 | label="Disable Stock Delivered But Not Billed in Sales Return", 31 | fieldtype="Check", 32 | insert_after="stock_delivered_but_not_billed", 33 | ignore_user_permissions=1, 34 | no_copy=1, 35 | ), 36 | ] 37 | } 38 | create_custom_fields(custom_field, ignore_validate=frappe.flags.in_patch, update=True) -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/overrides/delivery_note.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from erpnext.stock.doctype.delivery_note.delivery_note import DeliveryNote 4 | 5 | class CustomDeliveryNote(DeliveryNote): 6 | def check_expense_account(self, item): 7 | if not item.get("expense_account"): 8 | msg = _("Please set an Expense Account in the Items table") 9 | frappe.throw( 10 | _("Row #{0}: Expense Account not set for the Item {1}. {2}").format( 11 | item.idx, frappe.bold(item.item_code), msg 12 | ), 13 | title=_("Expense Account Missing"), 14 | ) 15 | 16 | else: 17 | is_expense_account = ( 18 | frappe.get_cached_value("Account", item.get("expense_account"), "report_type") 19 | == "Profit and Loss" 20 | ) 21 | if ( 22 | self.doctype 23 | not in ( 24 | "Purchase Receipt", 25 | "Purchase Invoice", 26 | "Stock Reconciliation", 27 | "Stock Entry", 28 | "Subcontracting Receipt", 29 | "Delivery Note" 30 | ) 31 | and not is_expense_account 32 | ): 33 | frappe.throw( 34 | _("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format( 35 | item.get("expense_account") 36 | ) 37 | ) 38 | if is_expense_account and not item.get("cost_center"): 39 | frappe.throw( 40 | _("{0} {1}: Cost Center is mandatory for Item {2}").format( 41 | _(self.doctype), self.name, item.get("item_code") 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/doctype/repost_sales_invoice/repost_sales_invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_copy": 1, 4 | "creation": "2024-03-13 01:49:32.913766", 5 | "default_view": "List", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "repost_item_valuation", 11 | "affected_sales_invoice", 12 | "completed" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "repost_item_valuation", 17 | "fieldtype": "Link", 18 | "label": "Repost Item Valuation", 19 | "options": "Repost Item Valuation" 20 | }, 21 | { 22 | "default": "0", 23 | "fieldname": "completed", 24 | "fieldtype": "Check", 25 | "label": "Completed" 26 | }, 27 | { 28 | "fieldname": "affected_sales_invoice", 29 | "fieldtype": "Link", 30 | "label": "Affected Sales Invoice", 31 | "options": "Sales Invoice" 32 | } 33 | ], 34 | "hide_toolbar": 0, 35 | "in_create": 0, 36 | "index_web_pages_for_search": 1, 37 | "links": [], 38 | "modified": "2024-06-03 02:01:21.156871", 39 | "modified_by": "Administrator", 40 | "module": "Stock Delivered Unbilled", 41 | "name": "Repost Sales Invoice", 42 | "owner": "Administrator", 43 | "permissions": [ 44 | { 45 | "create": 1, 46 | "delete": 1, 47 | "email": 1, 48 | "export": 1, 49 | "print": 1, 50 | "read": 1, 51 | "report": 1, 52 | "role": "System Manager", 53 | "share": 1, 54 | "write": 1 55 | } 56 | ], 57 | "read_only": 0, 58 | "sort_field": "modified", 59 | "sort_order": "DESC", 60 | "states": [] 61 | } 62 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/overrides/repost_item_valuation.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( 4 | repost_sl_entries, 5 | repost_gl_entries, 6 | notify_error_to_stock_managers, 7 | _get_directly_dependent_vouchers, 8 | in_configured_timeslot 9 | ) 10 | from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime 11 | from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative 12 | from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers, _delete_accounting_ledger_entries 13 | from erpnext.stock.stock_ledger import ( 14 | get_affected_transactions, 15 | get_items_to_be_repost, 16 | repost_future_sle, 17 | ) 18 | 19 | from rq.timeouts import JobTimeoutException 20 | from frappe.exceptions import QueryDeadlockError, QueryTimeoutError 21 | 22 | RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError) 23 | 24 | 25 | def repost_invoice_entries(): 26 | """ 27 | Pick and repost the invoices listed in Repost Sales Invoice List. 28 | """ 29 | 30 | riv_entries = frappe.db.get_all( 31 | "Repost Sales Invoice", 32 | filters={"completed": 0}, 33 | fields=["repost_item_valuation", "affected_sales_invoice", "name"], 34 | ) 35 | 36 | for row in riv_entries: 37 | voucher_obj = frappe.get_doc("Sales Invoice", row.affected_sales_invoice) 38 | expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries()) 39 | _delete_accounting_ledger_entries("Sales Invoice", row.affected_sales_invoice) 40 | voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) 41 | frappe.db.commit() 42 | frappe.db.set_value('Repost Sales Invoice', row.name, 'completed', 1) 43 | 44 | return 45 | 46 | def validate_expense_accoount(self,method): 47 | stock_delivered_but_not_billed_account = None 48 | stock_delivered_but_not_billed_account = frappe.db.get_value("Company", self.company, "stock_delivered_but_not_billed") 49 | 50 | 51 | disable_sdbnb_in_sr = frappe.db.get_value("Company", self.company, "disable_sdbnb_in_sr") 52 | default_expense_account = frappe.db.get_value("Company", self.company, "default_expense_account") 53 | 54 | if stock_delivered_but_not_billed_account: 55 | if disable_sdbnb_in_sr and self.is_return: 56 | if default_expense_account: 57 | for item in self.items: 58 | item.expense_account = default_expense_account 59 | else: 60 | for item in self.items: 61 | item.expense_account = stock_delivered_but_not_billed_account -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/overrides/collect_dn_for_si_repost.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( 4 | repost_sl_entries, 5 | repost_gl_entries, 6 | notify_error_to_stock_managers, 7 | _get_directly_dependent_vouchers, 8 | in_configured_timeslot 9 | ) 10 | from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime 11 | from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative 12 | from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers, _delete_accounting_ledger_entries 13 | from erpnext.stock.stock_ledger import ( 14 | get_affected_transactions, 15 | get_items_to_be_repost, 16 | repost_future_sle, 17 | ) 18 | 19 | from rq.timeouts import JobTimeoutException 20 | from frappe.exceptions import QueryDeadlockError, QueryTimeoutError 21 | 22 | RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError) 23 | 24 | 25 | def queue_affected_sales_invoices(): 26 | """ 27 | This will collect all the affected delivery notes from "Repost Item Valuation" 28 | and adds the associated sales invoice to the "Repost Sales Invoice" list. 29 | """ 30 | 31 | riv_entries = frappe.db.sql( 32 | """ SELECT name from `tabRepost Item Valuation` 33 | WHERE status in ('Completed') and creation <= %s and docstatus = 1 and modified > now() - interval 36 hour 34 | ORDER BY timestamp(posting_date, posting_time) asc, creation asc, status asc 35 | """, 36 | now(), 37 | as_dict=1, 38 | ) 39 | 40 | for row in riv_entries: 41 | doc = frappe.get_doc("Repost Item Valuation", row.name) 42 | if doc.status in ("Completed"): 43 | try: 44 | _queue_affected_sales_invoices(doc) 45 | 46 | except Exception as e: 47 | frappe.db.rollback() 48 | traceback = frappe.get_traceback() 49 | doc.log_error("Unable to fetch affected invoices") 50 | finally: 51 | if not frappe.flags.in_test: 52 | frappe.db.commit() 53 | return 54 | 55 | def _queue_affected_sales_invoices(doc): 56 | directly_dependent_transactions = _get_directly_dependent_vouchers(doc) 57 | repost_affected_transaction = get_affected_transactions(doc) 58 | all_affected_transactions = directly_dependent_transactions + list(repost_affected_transaction) 59 | affected_invoices = [] 60 | for affected_transaction in all_affected_transactions: 61 | document_type, document_name = affected_transaction 62 | if document_type == "Delivery Note": 63 | invoice_list = frappe.get_all("Sales Invoice Item", fields=["name", "parent"], filters={"delivery_note": document_name}) 64 | for invoice in invoice_list: 65 | docstatus = frappe.db.get_value("Sales Invoice", invoice.parent, "docstatus") 66 | if docstatus == 1: 67 | affected_invoices.append(invoice.parent) 68 | 69 | for inv in affected_invoices: 70 | if not frappe.db.exists( 71 | "Repost Sales Invoice", {"affected_sales_invoice": inv, "completed": 0}): 72 | rsi = frappe.new_doc("Repost Sales Invoice") 73 | rsi.repost_item_valuation = doc.name 74 | rsi.affected_sales_invoice = inv 75 | rsi.completed = 0 76 | rsi.insert(ignore_permissions=True) 77 | 78 | 79 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/overrides/sales_invoice.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from erpnext.accounts.utils import get_account_currency 3 | from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate 4 | from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice 5 | 6 | 7 | 8 | 9 | class CustomSalesInvoice(SalesInvoice): 10 | def get_gl_entries(self, warehouse_account=None): 11 | from erpnext.accounts.general_ledger import merge_similar_entries 12 | 13 | gl_entries = [] 14 | 15 | self.make_customer_gl_entry(gl_entries) 16 | 17 | self.make_tax_gl_entries(gl_entries) 18 | self.make_internal_transfer_gl_entries(gl_entries) 19 | 20 | self.make_item_gl_entries(gl_entries) 21 | disable_sdbnb_in_sr = frappe.db.get_value("Company", self.company, "disable_sdbnb_in_sr") 22 | if not self.is_return: 23 | self.stock_delivered_but_not_billed_gl_entries(gl_entries) 24 | else: 25 | if not disable_sdbnb_in_sr: 26 | stock_delivered_but_not_billed_gl_entries(gl_entries) 27 | 28 | self.make_precision_loss_gl_entry(gl_entries) 29 | self.make_discount_gl_entries(gl_entries) 30 | 31 | # merge gl entries before adding pos entries 32 | gl_entries = merge_similar_entries(gl_entries) 33 | 34 | self.make_loyalty_point_redemption_gle(gl_entries) 35 | self.make_pos_gl_entries(gl_entries) 36 | 37 | self.make_write_off_gl_entry(gl_entries) 38 | self.make_gle_for_rounding_adjustment(gl_entries) 39 | 40 | return gl_entries 41 | 42 | def stock_delivered_but_not_billed_gl_entries(self, gl_entries): 43 | if not self.update_stock: 44 | for item in self.get("items"): 45 | if item.delivery_note and item.dn_detail: 46 | is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item") 47 | if is_stock_item: 48 | dn_expense_account = frappe.db.get_value("Delivery Note Item", item.dn_detail, "expense_account") 49 | if dn_expense_account and dn_expense_account != item.expense_account: 50 | item_g = frappe.db.get_value("Stock Ledger Entry", 51 | { 52 | "voucher_no": item.delivery_note, 53 | "voucher_detail_no": item.dn_detail, 54 | "item_code": item.item_code 55 | }, 56 | ["stock_value_difference", "actual_qty"] 57 | ,as_dict = True) 58 | valuation_rate = item_g.stock_value_difference / item_g.actual_qty 59 | valuation_amount = valuation_rate * item.stock_qty 60 | account_currency = get_account_currency(dn_expense_account) 61 | gl_entries.append( 62 | self.get_gl_dict( 63 | { 64 | "account": dn_expense_account, 65 | "against": item.expense_account, 66 | "credit": flt(valuation_amount), 67 | "credit_in_account_currency": ( 68 | flt(valuation_amount) 69 | ), 70 | "cost_center": item.cost_center, 71 | }, 72 | account_currency, 73 | item=item, 74 | ) 75 | ) 76 | gl_entries.append( 77 | self.get_gl_dict( 78 | { 79 | "account": item.expense_account, 80 | "against": dn_expense_account, 81 | "debit": flt(valuation_amount), 82 | "debit_in_account_currency": ( 83 | flt(valuation_amount) 84 | ), 85 | "cost_center": item.cost_center, 86 | }, 87 | account_currency, 88 | item=item, 89 | ) 90 | ) 91 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/hooks.py: -------------------------------------------------------------------------------- 1 | app_name = "stock_delivered_unbilled" 2 | app_title = "Stock Delivered Unbilled" 3 | app_publisher = "Aerele Technologies Private Limited" 4 | app_description = "This app enables the parking account for Stock Delivered But Not Billed" 5 | app_email = "hello@aerele.in" 6 | app_license = "mit" 7 | # required_apps = [] 8 | 9 | # Includes in 10 | # ------------------ 11 | 12 | # include js, css files in header of desk.html 13 | # app_include_css = "/assets/stock_delivered_unbilled/css/stock_delivered_unbilled.css" 14 | # app_include_js = "/assets/stock_delivered_unbilled/js/stock_delivered_unbilled.js" 15 | 16 | # include js, css files in header of web template 17 | # web_include_css = "/assets/stock_delivered_unbilled/css/stock_delivered_unbilled.css" 18 | # web_include_js = "/assets/stock_delivered_unbilled/js/stock_delivered_unbilled.js" 19 | 20 | # include custom scss in every website theme (without file extension ".scss") 21 | # website_theme_scss = "stock_delivered_unbilled/public/scss/website" 22 | 23 | # include js, css files in header of web form 24 | # webform_include_js = {"doctype": "public/js/doctype.js"} 25 | # webform_include_css = {"doctype": "public/css/doctype.css"} 26 | 27 | # include js in page 28 | # page_js = {"page" : "public/js/file.js"} 29 | 30 | # include js in doctype views 31 | # doctype_js = {"doctype" : "public/js/doctype.js"} 32 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 33 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 34 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 35 | 36 | # Svg Icons 37 | # ------------------ 38 | # include app icons in desk 39 | # app_include_icons = "stock_delivered_unbilled/public/icons.svg" 40 | 41 | # Home Pages 42 | # ---------- 43 | 44 | # application home page (will override Website Settings) 45 | # home_page = "login" 46 | 47 | # website user home page (by Role) 48 | # role_home_page = { 49 | # "Role": "home_page" 50 | # } 51 | 52 | # Generators 53 | # ---------- 54 | 55 | # automatically create page for each record of this doctype 56 | # website_generators = ["Web Page"] 57 | 58 | # Jinja 59 | # ---------- 60 | 61 | # add methods and filters to jinja environment 62 | # jinja = { 63 | # "methods": "stock_delivered_unbilled.utils.jinja_methods", 64 | # "filters": "stock_delivered_unbilled.utils.jinja_filters" 65 | # } 66 | 67 | # Installation 68 | # ------------ 69 | 70 | # before_install = "stock_delivered_unbilled.install.before_install" 71 | after_install = "stock_delivered_unbilled.patches.add_default_parking_account_field.execute" 72 | 73 | # Uninstallation 74 | # ------------ 75 | 76 | # before_uninstall = "stock_delivered_unbilled.uninstall.before_uninstall" 77 | # after_uninstall = "stock_delivered_unbilled.uninstall.after_uninstall" 78 | 79 | # Integration Setup 80 | # ------------------ 81 | # To set up dependencies/integrations with other apps 82 | # Name of the app being installed is passed as an argument 83 | 84 | # before_app_install = "stock_delivered_unbilled.utils.before_app_install" 85 | # after_app_install = "stock_delivered_unbilled.utils.after_app_install" 86 | 87 | # Integration Cleanup 88 | # ------------------- 89 | # To clean up dependencies/integrations with other apps 90 | # Name of the app being uninstalled is passed as an argument 91 | 92 | # before_app_uninstall = "stock_delivered_unbilled.utils.before_app_uninstall" 93 | # after_app_uninstall = "stock_delivered_unbilled.utils.after_app_uninstall" 94 | 95 | # Desk Notifications 96 | # ------------------ 97 | # See frappe.core.notifications.get_notification_config 98 | 99 | # notification_config = "stock_delivered_unbilled.notifications.get_notification_config" 100 | 101 | # Permissions 102 | # ----------- 103 | # Permissions evaluated in scripted ways 104 | 105 | # permission_query_conditions = { 106 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 107 | # } 108 | # 109 | # has_permission = { 110 | # "Event": "frappe.desk.doctype.event.event.has_permission", 111 | # } 112 | 113 | # DocType Class 114 | # --------------- 115 | # Override standard doctype classes 116 | 117 | override_doctype_class = { 118 | "Sales Invoice": "stock_delivered_unbilled.stock_delivered_unbilled.overrides.sales_invoice.CustomSalesInvoice", 119 | "Delivery Note": "stock_delivered_unbilled.stock_delivered_unbilled.overrides.delivery_note.CustomDeliveryNote" 120 | } 121 | 122 | from erpnext.stock import get_item_details as original_get_item_details 123 | from stock_delivered_unbilled.stock_delivered_unbilled.overrides import get_basic_details as overridden_get_basic_details 124 | original_get_item_details.get_basic_details = overridden_get_basic_details.get_basic_details 125 | 126 | # Document Events 127 | # --------------- 128 | # Hook on document methods and events 129 | 130 | doc_events = { 131 | "Delivery Note": { 132 | "validate": "stock_delivered_unbilled.stock_delivered_unbilled.overrides.repost_item_valuation.validate_expense_accoount", 133 | } 134 | } 135 | 136 | # Scheduled Tasks 137 | # --------------- 138 | 139 | scheduler_events = { 140 | "hourly_long": [ 141 | "stock_delivered_unbilled.stock_delivered_unbilled.overrides.collect_dn_for_si_repost.queue_affected_sales_invoices", 142 | "stock_delivered_unbilled.stock_delivered_unbilled.overrides.repost_item_valuation.repost_invoice_entries", 143 | ] 144 | } 145 | 146 | # Testing 147 | # ------- 148 | 149 | # before_tests = "stock_delivered_unbilled.install.before_tests" 150 | 151 | # Overriding Methods 152 | # ------------------------------ 153 | # 154 | # override_whitelisted_methods = { 155 | # "frappe.desk.doctype.event.event.get_events": "stock_delivered_unbilled.event.get_events" 156 | # } 157 | # 158 | # each overriding function accepts a `data` argument; 159 | # generated from the base implementation of the doctype dashboard, 160 | # along with any modifications made in other Frappe apps 161 | # override_doctype_dashboards = { 162 | # "Task": "stock_delivered_unbilled.task.get_dashboard_data" 163 | # } 164 | 165 | # exempt linked doctypes from being automatically cancelled 166 | # 167 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 168 | 169 | # Ignore links to specified DocTypes when deleting documents 170 | # ----------------------------------------------------------- 171 | 172 | # ignore_links_on_delete = ["Communication", "ToDo"] 173 | 174 | # Request Events 175 | # ---------------- 176 | # before_request = ["stock_delivered_unbilled.utils.before_request"] 177 | # after_request = ["stock_delivered_unbilled.utils.after_request"] 178 | 179 | # Job Events 180 | # ---------- 181 | # before_job = ["stock_delivered_unbilled.utils.before_job"] 182 | # after_job = ["stock_delivered_unbilled.utils.after_job"] 183 | 184 | # User Data Protection 185 | # -------------------- 186 | 187 | # user_data_fields = [ 188 | # { 189 | # "doctype": "{doctype_1}", 190 | # "filter_by": "{filter_by}", 191 | # "redact_fields": ["{field_1}", "{field_2}"], 192 | # "partial": 1, 193 | # }, 194 | # { 195 | # "doctype": "{doctype_2}", 196 | # "filter_by": "{filter_by}", 197 | # "partial": 1, 198 | # }, 199 | # { 200 | # "doctype": "{doctype_3}", 201 | # "strict": False, 202 | # }, 203 | # { 204 | # "doctype": "{doctype_4}" 205 | # } 206 | # ] 207 | 208 | # Authentication and authorization 209 | # -------------------------------- 210 | 211 | # auth_hooks = [ 212 | # "stock_delivered_unbilled.auth.validate" 213 | # ] 214 | -------------------------------------------------------------------------------- /stock_delivered_unbilled/stock_delivered_unbilled/overrides/get_basic_details.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import frappe 4 | from frappe import _, throw 5 | from frappe.model import child_table_fields, default_fields 6 | from frappe.model.meta import get_field_precision 7 | from frappe.query_builder.functions import IfNull, Sum 8 | from frappe.utils import add_days, add_months, cint, cstr, flt, getdate 9 | 10 | from erpnext import get_company_currency 11 | from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( 12 | get_pricing_rule_for_item, 13 | set_transaction_type, 14 | ) 15 | from erpnext.setup.doctype.brand.brand import get_brand_defaults 16 | from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults 17 | from erpnext.setup.utils import get_exchange_rate 18 | from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_factor 19 | from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no 20 | from erpnext.stock.doctype.price_list.price_list import get_price_list_details 21 | 22 | from erpnext.stock.get_item_details import ( 23 | get_item_warehouse, 24 | get_default_income_account, 25 | get_default_expense_account, 26 | get_default_discount_account, 27 | get_provisional_account, 28 | get_default_cost_center, 29 | get_default_supplier, 30 | get_conversion_factor, 31 | sales_doctypes, 32 | purchase_doctypes, 33 | update_barcode_value 34 | 35 | ) 36 | 37 | def get_basic_details(args, item, overwrite_warehouse=True): 38 | """ 39 | :param args: { 40 | "item_code": "", 41 | "warehouse": None, 42 | "customer": "", 43 | "conversion_rate": 1.0, 44 | "selling_price_list": None, 45 | "price_list_currency": None, 46 | "price_list_uom_dependant": None, 47 | "plc_conversion_rate": 1.0, 48 | "doctype": "", 49 | "name": "", 50 | "supplier": None, 51 | "transaction_date": None, 52 | "conversion_rate": 1.0, 53 | "buying_price_list": None, 54 | "is_subcontracted": 0/1, 55 | "ignore_pricing_rule": 0/1 56 | "project": "", 57 | barcode: "", 58 | serial_no: "", 59 | currency: "", 60 | update_stock: "", 61 | price_list: "", 62 | company: "", 63 | order_type: "", 64 | is_pos: "", 65 | project: "", 66 | qty: "", 67 | stock_qty: "", 68 | conversion_factor: "", 69 | against_blanket_order: 0/1 70 | } 71 | :param item: `item_code` of Item object 72 | :return: frappe._dict 73 | """ 74 | 75 | if not item: 76 | item = frappe.get_doc("Item", args.get("item_code")) 77 | 78 | if item.variant_of and not item.taxes: 79 | item.update_template_tables() 80 | 81 | item_defaults = get_item_defaults(item.name, args.company) 82 | item_group_defaults = get_item_group_defaults(item.name, args.company) 83 | brand_defaults = get_brand_defaults(item.name, args.company) 84 | 85 | defaults = frappe._dict( 86 | { 87 | "item_defaults": item_defaults, 88 | "item_group_defaults": item_group_defaults, 89 | "brand_defaults": brand_defaults, 90 | } 91 | ) 92 | 93 | warehouse = get_item_warehouse(item, args, overwrite_warehouse, defaults) 94 | 95 | if args.get("doctype") == "Material Request" and not args.get("material_request_type"): 96 | args["material_request_type"] = frappe.db.get_value( 97 | "Material Request", args.get("name"), "material_request_type", cache=True 98 | ) 99 | 100 | expense_account = None 101 | 102 | if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: 103 | from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account 104 | 105 | expense_account = get_asset_category_account( 106 | fieldname="fixed_asset_account", item=args.item_code, company=args.company 107 | ) 108 | 109 | stock_delivered_but_not_billed_account = None 110 | stock_delivered_but_not_billed_account = frappe.db.get_value("Company", args.company, "stock_delivered_but_not_billed") 111 | 112 | if args.get("doctype") == "Delivery Note" and stock_delivered_but_not_billed_account: 113 | disable_sdbnb_in_sr = frappe.db.get_value("Company", args.company, "disable_sdbnb_in_sr") 114 | if disable_sdbnb_in_sr and args.get("is_return"): 115 | pass 116 | else: 117 | expense_account = stock_delivered_but_not_billed_account 118 | 119 | # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master 120 | if not args.get("uom"): 121 | if args.get("doctype") in sales_doctypes: 122 | args.uom = item.sales_uom if item.sales_uom else item.stock_uom 123 | elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( 124 | args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase" 125 | ): 126 | args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom 127 | else: 128 | args.uom = item.stock_uom 129 | 130 | # Set stock UOM in args, so that it can be used while fetching item price 131 | args.stock_uom = item.stock_uom 132 | 133 | if args.get("batch_no") and item.name != frappe.get_cached_value( 134 | "Batch", args.get("batch_no"), "item" 135 | ): 136 | args["batch_no"] = "" 137 | 138 | out = frappe._dict( 139 | { 140 | "item_code": item.name, 141 | "item_name": item.item_name, 142 | "description": cstr(item.description).strip(), 143 | "image": cstr(item.image).strip(), 144 | "warehouse": warehouse, 145 | "income_account": get_default_income_account( 146 | args, item_defaults, item_group_defaults, brand_defaults 147 | ), 148 | "expense_account": expense_account 149 | or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), 150 | "discount_account": get_default_discount_account( 151 | args, item_defaults, item_group_defaults, brand_defaults 152 | ), 153 | "provisional_expense_account": get_provisional_account( 154 | args, item_defaults, item_group_defaults, brand_defaults 155 | ), 156 | "cost_center": get_default_cost_center( 157 | args, item_defaults, item_group_defaults, brand_defaults 158 | ), 159 | "has_serial_no": item.has_serial_no, 160 | "has_batch_no": item.has_batch_no, 161 | "batch_no": args.get("batch_no"), 162 | "uom": args.uom, 163 | "stock_uom": item.stock_uom, 164 | "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", 165 | "qty": flt(args.qty) or 1.0, 166 | "stock_qty": flt(args.qty) or 1.0, 167 | "price_list_rate": 0.0, 168 | "base_price_list_rate": 0.0, 169 | "rate": 0.0, 170 | "base_rate": 0.0, 171 | "amount": 0.0, 172 | "base_amount": 0.0, 173 | "net_rate": 0.0, 174 | "net_amount": 0.0, 175 | "discount_percentage": 0.0, 176 | "discount_amount": flt(args.discount_amount) or 0.0, 177 | "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), 178 | "update_stock": args.get("update_stock") 179 | if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] 180 | else 0, 181 | "delivered_by_supplier": item.delivered_by_supplier 182 | if args.get("doctype") in ["Sales Order", "Sales Invoice"] 183 | else 0, 184 | "is_fixed_asset": item.is_fixed_asset, 185 | "last_purchase_rate": item.last_purchase_rate 186 | if args.get("doctype") in ["Purchase Order"] 187 | else 0, 188 | "transaction_date": args.get("transaction_date"), 189 | "against_blanket_order": args.get("against_blanket_order"), 190 | "bom_no": item.get("default_bom"), 191 | "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), 192 | "weight_uom": args.get("weight_uom") or item.get("weight_uom"), 193 | "grant_commission": item.get("grant_commission"), 194 | } 195 | ) 196 | 197 | if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): 198 | out.update(calculate_service_end_date(args, item)) 199 | 200 | # calculate conversion factor 201 | if item.stock_uom == args.uom: 202 | out.conversion_factor = 1.0 203 | else: 204 | out.conversion_factor = args.conversion_factor or get_conversion_factor(item.name, args.uom).get( 205 | "conversion_factor" 206 | ) 207 | 208 | args.conversion_factor = out.conversion_factor 209 | out.stock_qty = out.qty * out.conversion_factor 210 | args.stock_qty = out.stock_qty 211 | 212 | # calculate last purchase rate 213 | if args.get("doctype") in purchase_doctypes and not frappe.db.get_single_value( 214 | "Buying Settings", "disable_last_purchase_rate" 215 | ): 216 | from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate 217 | 218 | out.last_purchase_rate = item_last_purchase_rate( 219 | args.name, args.conversion_rate, item.name, out.conversion_factor 220 | ) 221 | 222 | # if default specified in item is for another company, fetch from company 223 | for d in [ 224 | ["Account", "income_account", "default_income_account"], 225 | ["Account", "expense_account", "default_expense_account"], 226 | ["Cost Center", "cost_center", "cost_center"], 227 | ["Warehouse", "warehouse", ""], 228 | ]: 229 | if not out[d[1]]: 230 | out[d[1]] = frappe.get_cached_value("Company", args.company, d[2]) if d[2] else None 231 | 232 | for fieldname in ("item_name", "item_group", "brand", "stock_uom"): 233 | out[fieldname] = item.get(fieldname) 234 | 235 | if args.get("manufacturer"): 236 | part_no = get_item_manufacturer_part_no(args.get("item_code"), args.get("manufacturer")) 237 | if part_no: 238 | out["manufacturer_part_no"] = part_no 239 | else: 240 | out["manufacturer_part_no"] = None 241 | out["manufacturer"] = None 242 | else: 243 | data = frappe.get_value( 244 | "Item", item.name, ["default_item_manufacturer", "default_manufacturer_part_no"], as_dict=1 245 | ) 246 | 247 | if data: 248 | out.update( 249 | { 250 | "manufacturer": data.default_item_manufacturer, 251 | "manufacturer_part_no": data.default_manufacturer_part_no, 252 | } 253 | ) 254 | 255 | child_doctype = args.doctype + " Item" 256 | meta = frappe.get_meta(child_doctype) 257 | if meta.get_field("barcode"): 258 | update_barcode_value(out) 259 | 260 | if out.get("weight_per_unit"): 261 | out["total_weight"] = out.weight_per_unit * out.stock_qty 262 | 263 | return out --------------------------------------------------------------------------------