mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 11:49:10 +00:00
Merge branch 'frappe:develop' into add-employee-name-to-session-user
This commit is contained in:
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
|
|||||||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
|
|||||||
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
|
|||||||
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ from frappe.contacts.doctype.address.address import (
|
|||||||
class ERPNextAddress(Address):
|
class ERPNextAddress(Address):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_reference()
|
self.validate_reference()
|
||||||
self.update_compnay_address()
|
self.update_company_address()
|
||||||
super().validate()
|
|
||||||
|
if hasattr(super(), "validate"):
|
||||||
|
super().validate()
|
||||||
|
|
||||||
def link_address(self):
|
def link_address(self):
|
||||||
"""Link address based on owner"""
|
"""Link address based on owner"""
|
||||||
@@ -20,7 +22,7 @@ class ERPNextAddress(Address):
|
|||||||
|
|
||||||
return super().link_address()
|
return super().link_address()
|
||||||
|
|
||||||
def update_compnay_address(self):
|
def update_company_address(self):
|
||||||
for link in self.get("links"):
|
for link in self.get("links"):
|
||||||
if link.link_doctype == "Company":
|
if link.link_doctype == "Company":
|
||||||
self.is_your_company_address = 1
|
self.is_your_company_address = 1
|
||||||
@@ -38,6 +40,10 @@ class ERPNextAddress(Address):
|
|||||||
"""
|
"""
|
||||||
After Address is updated, update the related 'Primary Address' on Customer.
|
After Address is updated, update the related 'Primary Address' on Customer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if hasattr(super(), "on_update"):
|
||||||
|
super().on_update()
|
||||||
|
|
||||||
address_display = get_address_display(self.as_dict())
|
address_display = get_address_display(self.as_dict())
|
||||||
filters = {"customer_primary_address": self.name}
|
filters = {"customer_primary_address": self.name}
|
||||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
"cost_center",
|
"cost_center",
|
||||||
"debit",
|
"debit",
|
||||||
"credit",
|
"credit",
|
||||||
|
"reporting_currency_exchange_rate",
|
||||||
|
"debit_in_reporting_currency",
|
||||||
|
"credit_in_reporting_currency",
|
||||||
"account_currency",
|
"account_currency",
|
||||||
"debit_in_account_currency",
|
"debit_in_account_currency",
|
||||||
"credit_in_account_currency",
|
"credit_in_account_currency",
|
||||||
@@ -124,12 +127,30 @@
|
|||||||
"fieldname": "is_period_closing_voucher_entry",
|
"fieldname": "is_period_closing_voucher_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Period Closing Voucher Entry"
|
"label": "Is Period Closing Voucher Entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "debit_in_reporting_currency",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Debit Amount in Reporting Currency",
|
||||||
|
"options": "Company:company:reporting_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "credit_in_reporting_currency",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Credit Amount in Reporting Currency",
|
||||||
|
"options": "Company:company:reporting_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reporting_currency_exchange_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Reporting Currency Exchange Rate",
|
||||||
|
"precision": "9"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-list",
|
"icon": "fa fa-list",
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:05:56.710541",
|
"modified": "2025-08-22 19:13:50.400404",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Account Closing Balance",
|
"name": "Account Closing Balance",
|
||||||
@@ -158,7 +179,8 @@
|
|||||||
"role": "Auditor"
|
"role": "Auditor"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, cstr
|
from frappe.utils import cint, cstr, flt
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
|
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class AccountClosingBalance(Document):
|
class AccountClosingBalance(Document):
|
||||||
@@ -26,12 +29,15 @@ class AccountClosingBalance(Document):
|
|||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
credit: DF.Currency
|
credit: DF.Currency
|
||||||
credit_in_account_currency: DF.Currency
|
credit_in_account_currency: DF.Currency
|
||||||
|
credit_in_reporting_currency: DF.Currency
|
||||||
debit: DF.Currency
|
debit: DF.Currency
|
||||||
debit_in_account_currency: DF.Currency
|
debit_in_account_currency: DF.Currency
|
||||||
|
debit_in_reporting_currency: DF.Currency
|
||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
is_period_closing_voucher_entry: DF.Check
|
is_period_closing_voucher_entry: DF.Check
|
||||||
period_closing_voucher: DF.Link | None
|
period_closing_voucher: DF.Link | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
|
reporting_currency_exchange_rate: DF.Float
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@@ -55,6 +61,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
|||||||
"closing_date": closing_date,
|
"closing_date": closing_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||||
cle.flags.ignore_permissions = True
|
cle.flags.ignore_permissions = True
|
||||||
cle.flags.ignore_links = True
|
cle.flags.ignore_links = True
|
||||||
cle.submit()
|
cle.submit()
|
||||||
@@ -144,3 +151,29 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
|||||||
entries = query.run(as_dict=1)
|
entries = query.run(as_dict=1)
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||||
|
default_currency, reporting_currency = frappe.get_cached_value(
|
||||||
|
"Company", company, ["default_currency", "reporting_currency"]
|
||||||
|
)
|
||||||
|
|
||||||
|
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||||
|
if not reporting_currency_exchange_rate:
|
||||||
|
frappe.throw(
|
||||||
|
title=_("Reporting Currency Exchange Not Found"),
|
||||||
|
msg=_(
|
||||||
|
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||||
|
).format(default_currency, reporting_currency, closing_date),
|
||||||
|
exc=ReportingCurrencyExchangeNotFoundError,
|
||||||
|
)
|
||||||
|
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||||
|
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||||
|
|
||||||
|
cle.update(
|
||||||
|
{
|
||||||
|
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||||
|
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||||
|
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"rate",
|
"rate",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
@@ -95,6 +96,13 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
|||||||
@@ -155,8 +155,10 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
condition = ""
|
condition = ""
|
||||||
|
pe_condition = ""
|
||||||
if not include_reconciled_entries:
|
if not include_reconciled_entries:
|
||||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||||
|
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||||
|
|
||||||
journal_entries = frappe.db.sql(
|
journal_entries = frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
@@ -181,19 +183,20 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
payment_entries = frappe.db.sql(
|
payment_entries = frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
select
|
select
|
||||||
"Payment Entry" as payment_document, name as payment_entry,
|
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||||
reference_no as cheque_number, reference_date as cheque_date,
|
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||||
from `tabPayment Entry`
|
from `tabPayment Entry` as pe
|
||||||
|
join `tabCompany` c on c.name = pe.company
|
||||||
where
|
where
|
||||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||||
{condition}
|
{pe_condition}
|
||||||
order by
|
order by
|
||||||
posting_date ASC, name DESC
|
pe.posting_date ASC, pe.name DESC
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"account": account,
|
"account": account,
|
||||||
|
|||||||
@@ -29,14 +29,17 @@
|
|||||||
"against_voucher",
|
"against_voucher",
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
"transaction_exchange_rate",
|
"transaction_exchange_rate",
|
||||||
|
"reporting_currency_exchange_rate",
|
||||||
"amounts_section",
|
"amounts_section",
|
||||||
"debit_in_account_currency",
|
"debit_in_account_currency",
|
||||||
"debit",
|
"debit",
|
||||||
"debit_in_transaction_currency",
|
"debit_in_transaction_currency",
|
||||||
|
"debit_in_reporting_currency",
|
||||||
"column_break_bm1w",
|
"column_break_bm1w",
|
||||||
"credit_in_account_currency",
|
"credit_in_account_currency",
|
||||||
"credit",
|
"credit",
|
||||||
"credit_in_transaction_currency",
|
"credit_in_transaction_currency",
|
||||||
|
"credit_in_reporting_currency",
|
||||||
"dimensions_section",
|
"dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"column_break_lmnm",
|
"column_break_lmnm",
|
||||||
@@ -353,13 +356,31 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_8abq",
|
"fieldname": "column_break_8abq",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "debit_in_reporting_currency",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Debit Amount in Reporting Currency",
|
||||||
|
"options": "Company:company:reporting_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "credit_in_reporting_currency",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Credit Amount in Reporting Currency",
|
||||||
|
"options": "Company:company:reporting_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reporting_currency_exchange_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Reporting Currency Exchange Rate",
|
||||||
|
"precision": "9"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-list",
|
"icon": "fa fa-list",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-21 15:29:11.221890",
|
"modified": "2025-08-22 12:57:17.750252",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "GL Entry",
|
"name": "GL Entry",
|
||||||
@@ -390,8 +411,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ from erpnext.accounts.party import (
|
|||||||
validate_party_gle_currency,
|
validate_party_gle_currency,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||||
from erpnext.exceptions import InvalidAccountCurrency
|
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
exclude_from_linked_with = True
|
exclude_from_linked_with = True
|
||||||
|
|
||||||
@@ -42,9 +43,11 @@ class GLEntry(Document):
|
|||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
credit: DF.Currency
|
credit: DF.Currency
|
||||||
credit_in_account_currency: DF.Currency
|
credit_in_account_currency: DF.Currency
|
||||||
|
credit_in_reporting_currency: DF.Currency
|
||||||
credit_in_transaction_currency: DF.Currency
|
credit_in_transaction_currency: DF.Currency
|
||||||
debit: DF.Currency
|
debit: DF.Currency
|
||||||
debit_in_account_currency: DF.Currency
|
debit_in_account_currency: DF.Currency
|
||||||
|
debit_in_reporting_currency: DF.Currency
|
||||||
debit_in_transaction_currency: DF.Currency
|
debit_in_transaction_currency: DF.Currency
|
||||||
due_date: DF.Date | None
|
due_date: DF.Date | None
|
||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
@@ -57,6 +60,7 @@ class GLEntry(Document):
|
|||||||
posting_date: DF.Date | None
|
posting_date: DF.Date | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
remarks: DF.Text | None
|
remarks: DF.Text | None
|
||||||
|
reporting_currency_exchange_rate: DF.Float
|
||||||
to_rename: DF.Check
|
to_rename: DF.Check
|
||||||
transaction_currency: DF.Link | None
|
transaction_currency: DF.Link | None
|
||||||
transaction_date: DF.Date | None
|
transaction_date: DF.Date | None
|
||||||
@@ -88,6 +92,8 @@ class GLEntry(Document):
|
|||||||
self.validate_party()
|
self.validate_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
|
||||||
|
self.set_amount_in_reporting_currency()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
adv_adj = self.flags.adv_adj
|
adv_adj = self.flags.adv_adj
|
||||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||||
@@ -131,18 +137,20 @@ class GLEntry(Document):
|
|||||||
|
|
||||||
if not self.is_cancelled and not (self.party_type and self.party):
|
if not self.is_cancelled and not (self.party_type and self.party):
|
||||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||||
if account_type == "Receivable":
|
# skipping validation for payroll entry creation in case party is not required
|
||||||
frappe.throw(
|
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
if account_type == "Receivable":
|
||||||
self.voucher_type, self.voucher_no, self.account
|
frappe.throw(
|
||||||
|
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||||
|
self.voucher_type, self.voucher_no, self.account
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
elif account_type == "Payable":
|
||||||
elif account_type == "Payable":
|
frappe.throw(
|
||||||
frappe.throw(
|
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
self.voucher_type, self.voucher_no, self.account
|
||||||
self.voucher_type, self.voucher_no, self.account
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# Zero value transaction is not allowed
|
# Zero value transaction is not allowed
|
||||||
if not (
|
if not (
|
||||||
@@ -292,6 +300,25 @@ class GLEntry(Document):
|
|||||||
if self.party_type and self.party:
|
if self.party_type and self.party:
|
||||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||||
|
|
||||||
|
def set_amount_in_reporting_currency(self):
|
||||||
|
default_currency, reporting_currency = frappe.get_cached_value(
|
||||||
|
"Company", self.company, ["default_currency", "reporting_currency"]
|
||||||
|
)
|
||||||
|
transaction_date = self.transaction_date or self.posting_date
|
||||||
|
self.reporting_currency_exchange_rate = get_exchange_rate(
|
||||||
|
default_currency, reporting_currency, transaction_date
|
||||||
|
)
|
||||||
|
if not self.reporting_currency_exchange_rate:
|
||||||
|
frappe.throw(
|
||||||
|
title=_("Reporting Currency Exchange Not Found"),
|
||||||
|
msg=_(
|
||||||
|
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||||
|
).format(default_currency, reporting_currency, transaction_date),
|
||||||
|
exc=ReportingCurrencyExchangeNotFoundError,
|
||||||
|
)
|
||||||
|
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
|
||||||
|
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
|
||||||
|
|
||||||
def validate_and_set_fiscal_year(self):
|
def validate_and_set_fiscal_year(self):
|
||||||
if not self.fiscal_year:
|
if not self.fiscal_year:
|
||||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||||
|
|||||||
@@ -644,8 +644,11 @@ class JournalEntry(AccountsController):
|
|||||||
def validate_party(self):
|
def validate_party(self):
|
||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||||
|
|
||||||
|
# skipping validation for payroll entry creation
|
||||||
|
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||||
if account_type in ["Receivable", "Payable"]:
|
if account_type in ["Receivable", "Payable"]:
|
||||||
if not (d.party_type and d.party):
|
if not (d.party_type and d.party) and not skip_validation:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"help_section",
|
"help_section",
|
||||||
"loyalty_program_help"
|
"loyalty_program_help"
|
||||||
],
|
],
|
||||||
@@ -144,6 +145,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"section_break_4",
|
"section_break_4",
|
||||||
"invoices"
|
"invoices"
|
||||||
],
|
],
|
||||||
@@ -63,6 +64,12 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "accounting_dimensions_section",
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"invoice_name",
|
"invoice_name",
|
||||||
"invoices",
|
"invoices",
|
||||||
@@ -194,6 +195,12 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.party",
|
"depends_on": "eval:doc.party",
|
||||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint, qb
|
from frappe import _, msgprint, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||||
@@ -392,6 +393,12 @@ class PaymentReconciliation(Document):
|
|||||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||||
|
|
||||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||||
|
allocated_amount_precision = get_field_precision(
|
||||||
|
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||||
|
)
|
||||||
|
difference_amount_precision = get_field_precision(
|
||||||
|
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||||
|
)
|
||||||
difference_amount = 0
|
difference_amount = 0
|
||||||
if frappe.get_cached_value(
|
if frappe.get_cached_value(
|
||||||
"Account", self.receivable_payable_account, "account_currency"
|
"Account", self.receivable_payable_account, "account_currency"
|
||||||
@@ -399,8 +406,14 @@ class PaymentReconciliation(Document):
|
|||||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||||
"exchange_rate", 1
|
"exchange_rate", 1
|
||||||
):
|
):
|
||||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
allocated_amount_in_ref_rate = flt(
|
||||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||||
|
difference_amount_precision,
|
||||||
|
)
|
||||||
|
allocated_amount_in_inv_rate = flt(
|
||||||
|
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||||
|
difference_amount_precision,
|
||||||
|
)
|
||||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||||
|
|
||||||
return difference_amount
|
return difference_amount
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
|||||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||||
)
|
)
|
||||||
|
|
||||||
if pricing_rule.coupon_code_based == 1:
|
if pricing_rule.get("coupon_code_based") == 1:
|
||||||
if not args.coupon_code:
|
if not args.coupon_code:
|
||||||
continue
|
continue
|
||||||
coupon_code = frappe.db.get_value(
|
coupon_code = frappe.db.get_value(
|
||||||
|
|||||||
@@ -34,11 +34,13 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%">{{ _("Date") }}</th>
|
<th style="width: 12%">{{ _("Date") }}</th>
|
||||||
<th style="width: 15%">{{ _("Reference") }}</th>
|
<th style="width: 20%">{{ _("Reference") }}</th>
|
||||||
<th style="width: 25%">{{ _("Remarks") }}</th>
|
|
||||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||||
|
{% if filters.show_remarks %}
|
||||||
|
<th style="width: 20%">{{ _("Remarks") }}</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -47,36 +49,51 @@
|
|||||||
{% if(row.posting_date) %}
|
{% if(row.posting_date) %}
|
||||||
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
|
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
|
||||||
<td>{{ row.voucher_type }}
|
<td>{{ row.voucher_type }}
|
||||||
<br>{{ row.voucher_no }}</td>
|
<br>{{ row.voucher_no }}
|
||||||
<td>
|
{% if not (filters.party or filters.account) %}
|
||||||
{% if not (filters.party or filters.account) %}
|
|
||||||
{{ row.party or row.account }}
|
{{ row.party or row.account }}
|
||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<br>{{ _("Remarks:") }} {{ row.remarks }}
|
|
||||||
{% if row.bill_no %}
|
{% if row.bill_no %}
|
||||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
||||||
|
<td style="text-align: right">
|
||||||
|
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||||
|
</td>
|
||||||
|
{% if filters.show_remarks %}
|
||||||
|
<td>
|
||||||
|
{% if row.remarks %}
|
||||||
|
{{ _("Remarks:") }} {{ row.remarks }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td>
|
||||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
<b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b>
|
||||||
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
{{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
{{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||||
</td>
|
</td>
|
||||||
|
{% if filters.show_remarks %}
|
||||||
|
<td>
|
||||||
|
{% if row.remarks %}
|
||||||
|
{{ _("Remarks:") }} {{ row.remarks }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
frm.set_query("print_format", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
print_format_for: "Report",
|
||||||
|
report: frm.doc.report,
|
||||||
|
disabled: 0,
|
||||||
|
print_format_type: "Jinja",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
if (frm.doc.__islocal) {
|
if (frm.doc.__islocal) {
|
||||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||||
frm.set_value("to_date", frappe.datetime.get_today());
|
frm.set_value("to_date", frappe.datetime.get_today());
|
||||||
@@ -106,6 +116,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
|||||||
filters: filters,
|
filters: filters,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
frm.set_query("print_format", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
print_format_for: "Report",
|
||||||
|
report: frm.doc.report,
|
||||||
|
disabled: 0,
|
||||||
|
print_format_type: "Jinja",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
customer_collection: function (frm) {
|
customer_collection: function (frm) {
|
||||||
frm.set_value("collection_name", "");
|
frm.set_value("collection_name", "");
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"column_break_17",
|
"column_break_17",
|
||||||
"customers",
|
"customers",
|
||||||
"preferences",
|
"preferences",
|
||||||
|
"print_format",
|
||||||
"orientation",
|
"orientation",
|
||||||
"include_break",
|
"include_break",
|
||||||
"include_ageing",
|
"include_ageing",
|
||||||
@@ -406,10 +407,16 @@
|
|||||||
"fieldname": "show_future_payments",
|
"fieldname": "show_future_payments",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Future Payments"
|
"label": "Show Future Payments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "print_format",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Print Format",
|
||||||
|
"options": "Print Format"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-29 00:20:08.088189",
|
"modified": "2025-09-03 14:24:43.608565",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
pdf_name: DF.Data | None
|
pdf_name: DF.Data | None
|
||||||
posting_date: DF.Date | None
|
posting_date: DF.Date | None
|
||||||
primary_mandatory: DF.Check
|
primary_mandatory: DF.Check
|
||||||
|
print_format: DF.Link | None
|
||||||
project: DF.TableMultiSelect[PSOAProject]
|
project: DF.TableMultiSelect[PSOAProject]
|
||||||
report: DF.Literal["General Ledger", "Accounts Receivable"]
|
report: DF.Literal["General Ledger", "Accounts Receivable"]
|
||||||
sales_partner: DF.Link | None
|
sales_partner: DF.Link | None
|
||||||
@@ -109,6 +110,25 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
self.to_date = self.start_date
|
self.to_date = self.start_date
|
||||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||||
|
|
||||||
|
if self.print_format:
|
||||||
|
pf = frappe.db.get_value(
|
||||||
|
"Print Format",
|
||||||
|
self.print_format,
|
||||||
|
["print_format_type", "print_format_for", "report", "disabled"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if not pf:
|
||||||
|
frappe.throw(title=_("Invalid Print Format"), msg=_("Selected Print Format does not exist."))
|
||||||
|
if pf.print_format_type != "Jinja":
|
||||||
|
frappe.throw(title=_("Invalid Print Format"), msg=_("Print Format Type should be Jinja."))
|
||||||
|
if pf.print_format_for != "Report" or pf.report != self.report or pf.disabled:
|
||||||
|
frappe.throw(
|
||||||
|
title=_("Invalid Print Format"),
|
||||||
|
msg=_(
|
||||||
|
"Print Format must be an enabled Report Print Format matching the selected Report."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_account(self):
|
def validate_account(self):
|
||||||
if not self.account:
|
if not self.account:
|
||||||
return
|
return
|
||||||
@@ -290,6 +310,10 @@ def get_html(doc, filters, entry, col, res, ageing):
|
|||||||
if process_soa_html and process_soa_html.get(doc.report):
|
if process_soa_html and process_soa_html.get(doc.report):
|
||||||
template_path = process_soa_html[doc.report][-1]
|
template_path = process_soa_html[doc.report][-1]
|
||||||
|
|
||||||
|
if doc.print_format:
|
||||||
|
custom_html, custom_css = frappe.db.get_value("Print Format", doc.print_format, ["html", "css"])
|
||||||
|
template_path = f"<style>{custom_css}</style> {custom_html}"
|
||||||
|
|
||||||
if doc.letter_head:
|
if doc.letter_head:
|
||||||
from frappe.www.printview import get_letter_head
|
from frappe.www.printview import get_letter_head
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not(filters.show_future_payments) %}
|
{% if not(filters.show_future_payments) and filters.show_remarks %}
|
||||||
<th style="width: 20%">
|
<th style="width: 20%">
|
||||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||||
{{ _("Remarks") }}
|
{{ _("Remarks") }}
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<td>{{ data[i]["sales_person"] }}</td>
|
<td>{{ data[i]["sales_person"] }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not (filters.show_future_payments) %}
|
{% if not (filters.show_future_payments) and filters.show_remarks %}
|
||||||
<td>
|
<td>
|
||||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||||
{{ data[i]["party"] }}
|
{{ data[i]["party"] }}
|
||||||
@@ -327,12 +327,23 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
{% if (filters.show_future_payments) or filters.show_remarks %}
|
||||||
<td></td>
|
<td></td>
|
||||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
{% endif %}
|
||||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
{% if not(filters.show_future_payments) %}
|
||||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
<td></td>
|
||||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
{% else %}
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"account_currency",
|
"account_currency",
|
||||||
"net_amount",
|
"net_amount",
|
||||||
@@ -214,6 +215,13 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||||
|
|||||||
@@ -275,6 +275,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "customer.tax_id",
|
||||||
"fieldname": "tax_id",
|
"fieldname": "tax_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -2241,7 +2242,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-08-04 19:20:28.732039",
|
"modified": "2025-09-09 14:48:59.472826",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"rate",
|
"rate",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
@@ -191,6 +192,13 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"shipping_amount_section",
|
"shipping_amount_section",
|
||||||
"calculate_based_on",
|
"calculate_based_on",
|
||||||
"column_break_8",
|
"column_break_8",
|
||||||
@@ -138,6 +139,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-truck",
|
"icon": "fa fa-truck",
|
||||||
|
|||||||
@@ -164,6 +164,12 @@
|
|||||||
{% } %}
|
{% } %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
<div class="show-filters">
|
||||||
|
{% if subtitle %}
|
||||||
|
{{ subtitle }}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for(var i=0, l=data.length; i<l; i++) { %}
|
{% for(var i=0, l=data.length; i<l; i++) { %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -976,6 +976,7 @@ class ReceivablePayableReport:
|
|||||||
|
|
||||||
if self.account_type == "Receivable":
|
if self.account_type == "Receivable":
|
||||||
self.add_customer_filters()
|
self.add_customer_filters()
|
||||||
|
self.exclude_employee_transaction()
|
||||||
|
|
||||||
elif self.account_type == "Payable":
|
elif self.account_type == "Payable":
|
||||||
self.add_supplier_filters()
|
self.add_supplier_filters()
|
||||||
@@ -1055,6 +1056,9 @@ class ReceivablePayableReport:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def exclude_employee_transaction(self):
|
||||||
|
self.qb_selection_filter.append(self.ple.party_type != "Employee")
|
||||||
|
|
||||||
def add_supplier_filters(self):
|
def add_supplier_filters(self):
|
||||||
supplier = qb.DocType("Supplier")
|
supplier = qb.DocType("Supplier")
|
||||||
if self.filters.get("supplier_group"):
|
if self.filters.get("supplier_group"):
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
|
|||||||
get_report_summary as get_pl_summary,
|
get_report_summary as get_pl_summary,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
|
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
|
||||||
|
from erpnext.accounts.utils import get_zero_cutoff
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
@@ -564,7 +565,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
|||||||
|
|
||||||
row[company] = flt(d.get(company, 0.0), 3)
|
row[company] = flt(d.get(company, 0.0), 3)
|
||||||
|
|
||||||
if abs(row[company]) >= 0.005:
|
if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency):
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
total += flt(row[company])
|
total += flt(row[company])
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
|||||||
filter_out_zero_value_rows,
|
filter_out_zero_value_rows,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||||
|
from erpnext.accounts.utils import get_zero_cutoff
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
@@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
|||||||
for dimension in dimension_list:
|
for dimension in dimension_list:
|
||||||
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||||
|
|
||||||
if abs(row[frappe.scrub(dimension)]) >= 0.005:
|
if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency):
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||||
|
|||||||
@@ -34,6 +34,12 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{% } %}
|
{% } %}
|
||||||
<hr>
|
<hr>
|
||||||
|
<div class="show-filters">
|
||||||
|
{% if subtitle %}
|
||||||
|
{{ subtitle }}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|||||||
get_dimension_with_children,
|
get_dimension_with_children,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff
|
||||||
|
|
||||||
|
|
||||||
def get_period_list(
|
def get_period_list(
|
||||||
@@ -306,7 +306,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
|||||||
|
|
||||||
row[period.key] = flt(d.get(period.key, 0.0), 3)
|
row[period.key] = flt(d.get(period.key, 0.0), 3)
|
||||||
|
|
||||||
if abs(row[period.key]) >= 0.005:
|
if abs(row[period.key]) >= get_zero_cutoff(company_currency):
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
total += flt(row[period.key])
|
total += flt(row[period.key])
|
||||||
|
|||||||
@@ -75,6 +75,12 @@
|
|||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="show-filters">
|
||||||
|
{% if subtitle %}
|
||||||
|
{{ subtitle }}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<table style="width:100%; font-size: 11px">
|
<table style="width:100%; font-size: 11px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
|
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
|
||||||
|
|||||||
@@ -667,7 +667,7 @@ def get_columns(filters):
|
|||||||
"options": "GL Entry",
|
"options": "GL Entry",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
},
|
},
|
||||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||||
{
|
{
|
||||||
"label": _("Account"),
|
"label": _("Account"),
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
|||||||
filter_out_zero_value_rows,
|
filter_out_zero_value_rows,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||||
|
from erpnext.accounts.utils import get_zero_cutoff
|
||||||
|
|
||||||
value_fields = ("income", "expense", "gross_profit_loss")
|
value_fields = ("income", "expense", "gross_profit_loss")
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
|||||||
for key in value_fields:
|
for key in value_fields:
|
||||||
row[key] = flt(d.get(key, 0.0), 3)
|
row[key] = flt(d.get(key, 0.0), 3)
|
||||||
|
|
||||||
if abs(row[key]) >= 0.005:
|
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import (
|
|||||||
set_gl_entries_by_account,
|
set_gl_entries_by_account,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||||
|
from erpnext.accounts.utils import get_zero_cutoff
|
||||||
|
|
||||||
value_fields = (
|
value_fields = (
|
||||||
"opening_debit",
|
"opening_debit",
|
||||||
@@ -412,7 +413,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
|||||||
for key in value_fields:
|
for key in value_fields:
|
||||||
row[key] = flt(d.get(key, 0.0), 3)
|
row[key] = flt(d.get(key, 0.0), 3)
|
||||||
|
|
||||||
if abs(row[key]) >= 0.005:
|
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from erpnext.accounts.party import get_party_shipping_address
|
|||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
get_future_stock_vouchers,
|
get_future_stock_vouchers,
|
||||||
get_voucherwise_gl_entries,
|
get_voucherwise_gl_entries,
|
||||||
|
get_zero_cutoff,
|
||||||
sort_stock_vouchers_by_posting_date,
|
sort_stock_vouchers_by_posting_date,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
@@ -157,6 +158,11 @@ class TestUtils(IntegrationTestCase):
|
|||||||
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
|
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
|
||||||
frappe.db.set_default("supp_master_name", "Supplier Name")
|
frappe.db.set_default("supp_master_name", "Supplier Name")
|
||||||
|
|
||||||
|
def test_get_zero_cutoff(self):
|
||||||
|
self.assertEqual(get_zero_cutoff(None), 0.005)
|
||||||
|
self.assertEqual(get_zero_cutoff("EUR"), 0.005)
|
||||||
|
self.assertEqual(get_zero_cutoff("BHD"), 0.0005)
|
||||||
|
|
||||||
|
|
||||||
ADDRESS_RECORDS = [
|
ADDRESS_RECORDS = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from frappe.utils import (
|
|||||||
now,
|
now,
|
||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
|
from frappe.utils.caching import site_cache
|
||||||
from pypika import Order
|
from pypika import Order
|
||||||
from pypika.functions import Coalesce
|
from pypika.functions import Coalesce
|
||||||
from pypika.terms import ExistsCriterion
|
from pypika.terms import ExistsCriterion
|
||||||
@@ -1147,6 +1148,29 @@ def get_currency_precision():
|
|||||||
return precision
|
return precision
|
||||||
|
|
||||||
|
|
||||||
|
def get_fraction_units(currency: str) -> int:
|
||||||
|
"""Returns the number of fraction units for a currency."""
|
||||||
|
fraction_units = frappe.db.get_value("Currency", currency, "fraction_units")
|
||||||
|
|
||||||
|
if fraction_units is None:
|
||||||
|
fraction_units = 100
|
||||||
|
|
||||||
|
return fraction_units
|
||||||
|
|
||||||
|
|
||||||
|
@site_cache()
|
||||||
|
def get_zero_cutoff(currency: str) -> float:
|
||||||
|
"""Returns the zero cutoff for a currency.
|
||||||
|
|
||||||
|
For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005.
|
||||||
|
We don't want to display values less than the zero cutoff.
|
||||||
|
This value was chosen for compatibility with the previous hard-coded value of 0.005.
|
||||||
|
"""
|
||||||
|
fraction_units = get_fraction_units(currency)
|
||||||
|
|
||||||
|
return 0.5 / (fraction_units or 1)
|
||||||
|
|
||||||
|
|
||||||
def get_held_invoices(party_type, party):
|
def get_held_invoices(party_type, party):
|
||||||
"""
|
"""
|
||||||
Returns a list of names Purchase Invoices for the given party that are on hold
|
Returns a list of names Purchase Invoices for the given party that are on hold
|
||||||
@@ -2482,6 +2506,10 @@ def build_qb_match_conditions(doctype, user=None) -> list:
|
|||||||
for filter in match_filters:
|
for filter in match_filters:
|
||||||
for link_option, allowed_values in filter.items():
|
for link_option, allowed_values in filter.items():
|
||||||
fieldnames = link_fields_map.get(link_option, [])
|
fieldnames = link_fields_map.get(link_option, [])
|
||||||
|
cond = None
|
||||||
|
|
||||||
|
if link_option == doctype:
|
||||||
|
cond = _dt["name"].isin(allowed_values)
|
||||||
|
|
||||||
for fieldname in fieldnames:
|
for fieldname in fieldnames:
|
||||||
field = _dt[fieldname]
|
field = _dt[fieldname]
|
||||||
@@ -2490,6 +2518,7 @@ def build_qb_match_conditions(doctype, user=None) -> list:
|
|||||||
if not apply_strict_user_permissions:
|
if not apply_strict_user_permissions:
|
||||||
cond = (Coalesce(field, "") == "") | cond
|
cond = (Coalesce(field, "") == "") | cond
|
||||||
|
|
||||||
|
if cond:
|
||||||
criterion.append(cond)
|
criterion.append(cond)
|
||||||
|
|
||||||
return criterion
|
return criterion
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"target_fixed_asset_account"
|
"target_fixed_asset_account"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -275,6 +276,12 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
"fixed_asset_account"
|
"fixed_asset_account"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -98,6 +99,13 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "finance_book",
|
"fieldname": "finance_book",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class AssetMovement(Document):
|
|||||||
if d.source_location:
|
if d.source_location:
|
||||||
if current_location != d.source_location:
|
if current_location != d.source_location:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
_("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
d.source_location = current_location
|
d.source_location = current_location
|
||||||
@@ -76,11 +76,11 @@ class AssetMovement(Document):
|
|||||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_employee(self, d):
|
def validate_employee(self, d):
|
||||||
if self.purpose == "Tranfer and Issue":
|
if self.purpose == "Transfer and Issue":
|
||||||
if not d.from_employee:
|
if not d.from_employee:
|
||||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class AssetMovement(Document):
|
|||||||
|
|
||||||
if current_custodian != d.from_employee:
|
if current_custodian != d.from_employee:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
_("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not d.to_employee:
|
if not d.to_employee:
|
||||||
@@ -97,7 +97,7 @@ class AssetMovement(Document):
|
|||||||
|
|
||||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
|||||||
@@ -503,6 +503,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
|||||||
}
|
}
|
||||||
|
|
||||||
onload() {
|
onload() {
|
||||||
|
super.onload();
|
||||||
this.frm.set_query("supplier", function () {
|
this.frm.set_query("supplier", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"order_confirmation_no",
|
"order_confirmation_no",
|
||||||
"order_confirmation_date",
|
"order_confirmation_date",
|
||||||
"get_items_from_open_material_requests",
|
"get_items_from_open_material_requests",
|
||||||
|
"mps",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"schedule_date",
|
"schedule_date",
|
||||||
@@ -1315,6 +1316,13 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"label": "Last Scanned Warehouse"
|
"label": "Last Scanned Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mps",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "MPS",
|
||||||
|
"options": "Master Production Schedule",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1322,7 +1330,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-31 17:19:40.816883",
|
"modified": "2025-08-28 11:00:56.635116",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
items: DF.Table[PurchaseOrderItem]
|
items: DF.Table[PurchaseOrderItem]
|
||||||
language: DF.Data | None
|
language: DF.Data | None
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
|
mps: DF.Link | None
|
||||||
named_place: DF.Data | None
|
named_place: DF.Data | None
|
||||||
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
|
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
|
||||||
net_total: DF.Currency
|
net_total: DF.Currency
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ class InvalidAccountDimensionError(frappe.ValidationError):
|
|||||||
|
|
||||||
class MandatoryAccountDimensionError(frappe.ValidationError):
|
class MandatoryAccountDimensionError(frappe.ValidationError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingCurrencyExchangeNotFoundError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ doctype_list_js = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
extend_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||||
|
|
||||||
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}
|
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}
|
||||||
|
|
||||||
|
|||||||
1155
erpnext/locale/ar.po
1155
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/bs.po
1157
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/cs.po
1149
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/da.po
1157
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
1209
erpnext/locale/de.po
1209
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/eo.po
1157
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1155
erpnext/locale/es.po
1155
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
1177
erpnext/locale/fa.po
1177
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1348
erpnext/locale/fr.po
1348
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/hr.po
1157
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/hu.po
1149
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
1155
erpnext/locale/id.po
1155
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
1255
erpnext/locale/it.po
1255
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2284
erpnext/locale/nb.po
2284
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/nl.po
1149
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/pl.po
1149
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1163
erpnext/locale/pt.po
1163
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/ru.po
1149
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
1161
erpnext/locale/sr.po
1161
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1229
erpnext/locale/sv.po
1229
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1155
erpnext/locale/th.po
1155
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/tr.po
1157
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/vi.po
1149
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
1157
erpnext/locale/zh.po
1157
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -1414,7 +1414,7 @@ def get_children(parent=None, is_root=False, **filters):
|
|||||||
return bom_items
|
return bom_items
|
||||||
|
|
||||||
|
|
||||||
def add_additional_cost(stock_entry, work_order):
|
def add_additional_cost(stock_entry, work_order, job_card=None):
|
||||||
# Add non stock items cost in the additional cost
|
# Add non stock items cost in the additional cost
|
||||||
stock_entry.additional_costs = []
|
stock_entry.additional_costs = []
|
||||||
company_account = frappe.db.get_value(
|
company_account = frappe.db.get_value(
|
||||||
@@ -1427,13 +1427,16 @@ def add_additional_cost(stock_entry, work_order):
|
|||||||
expense_account = (
|
expense_account = (
|
||||||
company_account.default_operating_cost_account or company_account.default_expense_account
|
company_account.default_operating_cost_account or company_account.default_expense_account
|
||||||
)
|
)
|
||||||
add_non_stock_items_cost(stock_entry, work_order, expense_account)
|
add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=job_card)
|
||||||
add_operations_cost(stock_entry, work_order, expense_account)
|
add_operations_cost(stock_entry, work_order, expense_account, job_card=job_card)
|
||||||
|
|
||||||
|
|
||||||
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=None):
|
||||||
bom = frappe.get_doc("BOM", work_order.bom_no)
|
bom = frappe.get_doc("BOM", work_order.bom_no)
|
||||||
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
|
||||||
|
table = "items"
|
||||||
|
if work_order and not job_card:
|
||||||
|
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
||||||
|
|
||||||
items = {}
|
items = {}
|
||||||
for d in bom.get(table):
|
for d in bom.get(table):
|
||||||
@@ -1464,13 +1467,16 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
|||||||
|
|
||||||
|
|
||||||
def add_operating_cost_component_wise(
|
def add_operating_cost_component_wise(
|
||||||
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None
|
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None, job_card=None
|
||||||
):
|
):
|
||||||
if not work_order:
|
if not work_order:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cost_added = False
|
cost_added = False
|
||||||
for row in work_order.operations:
|
for row in work_order.operations:
|
||||||
|
if job_card and job_card.operation_id != row.name:
|
||||||
|
continue
|
||||||
|
|
||||||
workstation_cost = frappe.get_all(
|
workstation_cost = frappe.get_all(
|
||||||
"Workstation Cost",
|
"Workstation Cost",
|
||||||
fields=["operating_component", "operating_cost"],
|
fields=["operating_component", "operating_cost"],
|
||||||
@@ -1511,14 +1517,14 @@ def get_component_account(parent):
|
|||||||
return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account")
|
return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account")
|
||||||
|
|
||||||
|
|
||||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
|
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
|
||||||
|
|
||||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||||
|
|
||||||
if operating_cost_per_unit:
|
if operating_cost_per_unit:
|
||||||
cost_added = add_operating_cost_component_wise(
|
cost_added = add_operating_cost_component_wise(
|
||||||
stock_entry, work_order, operating_cost_per_unit, expense_account
|
stock_entry, work_order, operating_cost_per_unit, expense_account, job_card=job_card
|
||||||
)
|
)
|
||||||
|
|
||||||
if not cost_added:
|
if not cost_added:
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate"
|
"label": "Rate",
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 1,
|
"columns": 1,
|
||||||
@@ -158,7 +159,8 @@
|
|||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_yuca",
|
"fieldname": "column_break_yuca",
|
||||||
@@ -180,13 +182,15 @@
|
|||||||
"fieldname": "base_amount",
|
"fieldname": "base_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Base Amount"
|
"label": "Base Amount",
|
||||||
|
"options": "Company:company:default_currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "base_rate",
|
"fieldname": "base_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Base Rate"
|
"label": "Base Rate",
|
||||||
|
"options": "Company:company:default_currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from frappe.utils import (
|
|||||||
time_diff_in_hours,
|
time_diff_in_hours,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost
|
||||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
||||||
get_mins_between_operations,
|
get_mins_between_operations,
|
||||||
)
|
)
|
||||||
@@ -817,9 +818,6 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_work_order(self):
|
def update_work_order(self):
|
||||||
if self.track_semi_finished_goods:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -849,9 +847,9 @@ class JobCard(Document):
|
|||||||
|
|
||||||
def update_semi_finished_good_details(self):
|
def update_semi_finished_good_details(self):
|
||||||
if self.operation_id:
|
if self.operation_id:
|
||||||
frappe.db.set_value(
|
qty = max(flt(self.manufactured_qty), flt(self.total_completed_qty))
|
||||||
"Work Order Operation", self.operation_id, "completed_qty", self.manufactured_qty
|
|
||||||
)
|
frappe.db.set_value("Work Order Operation", self.operation_id, "completed_qty", qty)
|
||||||
if (
|
if (
|
||||||
self.finished_good
|
self.finished_good
|
||||||
and frappe.get_cached_value("Work Order", self.work_order, "production_item")
|
and frappe.get_cached_value("Work Order", self.work_order, "production_item")
|
||||||
@@ -1322,6 +1320,9 @@ class JobCard(Document):
|
|||||||
|
|
||||||
ste.make_stock_entry()
|
ste.make_stock_entry()
|
||||||
ste.stock_entry.flags.ignore_mandatory = True
|
ste.stock_entry.flags.ignore_mandatory = True
|
||||||
|
wo_doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
add_additional_cost(ste.stock_entry, wo_doc, self)
|
||||||
|
|
||||||
ste.stock_entry.save()
|
ste.stock_entry.save()
|
||||||
|
|
||||||
if auto_submit:
|
if auto_submit:
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
"overproduction_percentage_for_sales_order",
|
"overproduction_percentage_for_sales_order",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"overproduction_percentage_for_work_order",
|
"overproduction_percentage_for_work_order",
|
||||||
|
"section_break_xhtl",
|
||||||
|
"transfer_extra_materials_percentage",
|
||||||
|
"column_break_kemp",
|
||||||
"job_card_section",
|
"job_card_section",
|
||||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
"enforce_time_logs",
|
"enforce_time_logs",
|
||||||
@@ -243,13 +246,28 @@
|
|||||||
"fieldname": "enforce_time_logs",
|
"fieldname": "enforce_time_logs",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enforce Time Logs"
|
"label": "Enforce Time Logs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xhtl",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Extra Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_kemp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The user will be able to transfer additional materials from the store to the Work in Progress (WIP) warehouse.",
|
||||||
|
"fieldname": "transfer_extra_materials_percentage",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "Transfer Extra Raw Materials to WIP (%)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-16 11:23:16.916512",
|
"modified": "2025-09-08 19:48:31.726126",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class ManufacturingSettings(Document):
|
|||||||
overproduction_percentage_for_sales_order: DF.Percent
|
overproduction_percentage_for_sales_order: DF.Percent
|
||||||
overproduction_percentage_for_work_order: DF.Percent
|
overproduction_percentage_for_work_order: DF.Percent
|
||||||
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
|
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
|
||||||
|
transfer_extra_materials_percentage: DF.Percent
|
||||||
update_bom_costs_automatically: DF.Check
|
update_bom_costs_automatically: DF.Check
|
||||||
validate_components_quantities_per_bom: DF.Check
|
validate_components_quantities_per_bom: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("Master Production Schedule", {
|
||||||
|
refresh(frm) {
|
||||||
|
frm.trigger("set_query_filters");
|
||||||
|
|
||||||
|
frm.set_df_property("items", "cannot_add_rows", true);
|
||||||
|
frm.fields_dict.items.$wrapper.find("[data-action='duplicate_rows']").css("display", "none");
|
||||||
|
|
||||||
|
frm.trigger("set_custom_buttons");
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(frm) {
|
||||||
|
frm.trigger("set_indicator_for_item");
|
||||||
|
},
|
||||||
|
|
||||||
|
set_indicator_for_item(frm) {
|
||||||
|
frm.set_indicator_formatter("item_code", function (doc) {
|
||||||
|
if (doc.order_release_date < frappe.datetime.get_today()) {
|
||||||
|
return "orange";
|
||||||
|
} else if (doc.order_release_date > frappe.datetime.get_today()) {
|
||||||
|
return "blue";
|
||||||
|
} else {
|
||||||
|
return "green";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_query_filters(frm) {
|
||||||
|
frm.set_query("parent_warehouse", (doc) => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_group: 1,
|
||||||
|
company: doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_actual_demand(frm) {
|
||||||
|
frm.call({
|
||||||
|
method: "get_actual_demand",
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Generating Master Production Schedule..."),
|
||||||
|
callback: (r) => {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_custom_buttons(frm) {
|
||||||
|
if (!frm.is_new()) {
|
||||||
|
frm.add_custom_button(__("View MRP"), () => {
|
||||||
|
frappe.set_route("query-report", "Material Requirements Planning Report", {
|
||||||
|
company: frm.doc.company,
|
||||||
|
from_date: frm.doc.from_date,
|
||||||
|
to_date: frm.doc.to_date,
|
||||||
|
mps: frm.doc.name,
|
||||||
|
warehouse: frm.doc.parent_warehouse,
|
||||||
|
sales_forecast: frm.doc.sales_forecast,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get_sales_orders(frm) {
|
||||||
|
frm.sales_order_dialog = new frappe.ui.Dialog({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
label: __("Filters for Sales Orders"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "customer",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Customer",
|
||||||
|
label: __("Customer"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("From Date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("To Date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "delivery_from_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("Delivery From Date"),
|
||||||
|
default: frm.doc.from_date,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "delivery_to_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("Delivery To Date"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: __("Get Sales Orders"),
|
||||||
|
size: "large",
|
||||||
|
primary_action_label: __("Get Sales Orders"),
|
||||||
|
primary_action: (data) => {
|
||||||
|
frm.sales_order_dialog.hide();
|
||||||
|
frm.events.fetch_sales_orders(frm, data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.sales_order_dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch_sales_orders(frm, data) {
|
||||||
|
frm.call({
|
||||||
|
method: "fetch_sales_orders",
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Fetching Sales Orders..."),
|
||||||
|
args: data,
|
||||||
|
callback: (r) => {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_material_requests(frm) {
|
||||||
|
frm.sales_order_dialog = new frappe.ui.Dialog({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
label: __("Filters for Material Requests"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "material_request_type",
|
||||||
|
fieldtype: "Select",
|
||||||
|
label: __("Purpose"),
|
||||||
|
options: "\nPurchase\nManufacture",
|
||||||
|
default: "Manufacture",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("From Date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("To Date"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: __("Get Material Requests"),
|
||||||
|
size: "large",
|
||||||
|
primary_action_label: __("Get Material Requests"),
|
||||||
|
primary_action: (data) => {
|
||||||
|
frm.sales_order_dialog.hide();
|
||||||
|
frm.events.fetch_materials_requests(frm, data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.sales_order_dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch_materials_requests(frm, data) {
|
||||||
|
frm.call({
|
||||||
|
method: "fetch_materials_requests",
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Fetching Material Requests..."),
|
||||||
|
args: data,
|
||||||
|
callback: (r) => {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "naming_series:",
|
||||||
|
"creation": "2025-08-08 19:54:43.478386",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"mps_tab",
|
||||||
|
"naming_series",
|
||||||
|
"company",
|
||||||
|
"column_break_xdcy",
|
||||||
|
"posting_date",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"section_break_rrrx",
|
||||||
|
"column_break_klws",
|
||||||
|
"select_items",
|
||||||
|
"column_break_mtqw",
|
||||||
|
"parent_warehouse",
|
||||||
|
"sales_orders_and_material_requests_tab",
|
||||||
|
"open_orders_section",
|
||||||
|
"get_sales_orders",
|
||||||
|
"sales_orders",
|
||||||
|
"get_material_requests",
|
||||||
|
"material_requests",
|
||||||
|
"section_break_xtby",
|
||||||
|
"column_break_yhkr",
|
||||||
|
"column_break_vvys",
|
||||||
|
"get_actual_demand",
|
||||||
|
"section_break_cmgo",
|
||||||
|
"items",
|
||||||
|
"forecast_demand_section",
|
||||||
|
"sales_forecast",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xdcy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rrrx",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_klws",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_mtqw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 1,
|
||||||
|
"fieldname": "items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Items",
|
||||||
|
"options": "Master Production Schedule Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cmgo",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Actual Demand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "MPS.YY.-.######",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "MPS.YY.-.######",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse.",
|
||||||
|
"fieldname": "parent_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Parent Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "To Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_orders_and_material_requests_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Demand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_orders",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Sales Orders",
|
||||||
|
"options": "Production Plan Sales Order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mps_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Planning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "material_requests",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Material Requests",
|
||||||
|
"options": "Production Plan Material Request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "open_orders_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Open Orders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xtby",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vvys",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Master Production Schedule",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_forecast",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Sales Forecast",
|
||||||
|
"options": "Sales Forecast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_sales_orders",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Sales Orders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_material_requests",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Material Requests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "select_items",
|
||||||
|
"fieldtype": "Table MultiSelect",
|
||||||
|
"label": "Select Items",
|
||||||
|
"options": "Master Production Schedule Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_yhkr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_actual_demand",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Actual Demand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "forecast_demand_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Forecast Demand"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-09-02 19:33:28.244544",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Master Production Schedule",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Manufacturing User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Manufacturing Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import add_days, flt, getdate, parse_json, today
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
|
|
||||||
|
class MasterProductionSchedule(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.master_production_schedule_item.master_production_schedule_item import (
|
||||||
|
MasterProductionScheduleItem,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.production_plan_material_request.production_plan_material_request import (
|
||||||
|
ProductionPlanMaterialRequest,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.production_plan_sales_order.production_plan_sales_order import (
|
||||||
|
ProductionPlanSalesOrder,
|
||||||
|
)
|
||||||
|
|
||||||
|
amended_from: DF.Link | None
|
||||||
|
company: DF.Link
|
||||||
|
from_date: DF.Date
|
||||||
|
items: DF.Table[MasterProductionScheduleItem]
|
||||||
|
material_requests: DF.Table[ProductionPlanMaterialRequest]
|
||||||
|
naming_series: DF.Literal["MPS.YY.-.######"]
|
||||||
|
parent_warehouse: DF.Link | None
|
||||||
|
posting_date: DF.Date
|
||||||
|
sales_forecast: DF.Link | None
|
||||||
|
sales_orders: DF.Table[ProductionPlanSalesOrder]
|
||||||
|
select_items: DF.TableMultiSelect[MasterProductionScheduleItem]
|
||||||
|
to_date: DF.Date | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_actual_demand(self):
|
||||||
|
self.set("items", [])
|
||||||
|
|
||||||
|
actual_demand_data = self.get_demand_data()
|
||||||
|
|
||||||
|
item_wise_data = self.get_item_wise_mps_data(actual_demand_data)
|
||||||
|
|
||||||
|
if not item_wise_data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
self.update_item_details(item_wise_data)
|
||||||
|
self.add_mps_data(item_wise_data)
|
||||||
|
|
||||||
|
if not self.is_new():
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
self.set_to_date()
|
||||||
|
|
||||||
|
def set_to_date(self):
|
||||||
|
self.to_date = None
|
||||||
|
for row in self.items:
|
||||||
|
if not self.to_date or getdate(row.delivery_date) > getdate(self.to_date):
|
||||||
|
self.to_date = row.delivery_date
|
||||||
|
|
||||||
|
forecast_delivery_dates = self.get_sales_forecast_data()
|
||||||
|
for date in forecast_delivery_dates:
|
||||||
|
if not self.to_date or getdate(date) > getdate(self.to_date):
|
||||||
|
self.to_date = date
|
||||||
|
|
||||||
|
def get_sales_forecast_data(self):
|
||||||
|
if not self.sales_forecast:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = {"parent": self.sales_forecast}
|
||||||
|
if self.select_items:
|
||||||
|
items = [d.item_code for d in self.select_items if d.item_code]
|
||||||
|
filters["item_code"] = ("in", items)
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"Sales Forecast Item",
|
||||||
|
filters=filters,
|
||||||
|
pluck="delivery_date",
|
||||||
|
order_by="delivery_date asc",
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_item_details(self, data):
|
||||||
|
items = [item[0] for item in data if item[0]]
|
||||||
|
item_details = self.get_item_details(items)
|
||||||
|
|
||||||
|
for key in data:
|
||||||
|
item_data = data[key]
|
||||||
|
item_code = key[0]
|
||||||
|
if item_code in item_details:
|
||||||
|
item_data.update(item_details[item_code])
|
||||||
|
|
||||||
|
def get_item_details(self, items):
|
||||||
|
doctype = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
doctype.name.as_("item_code"),
|
||||||
|
doctype.default_bom.as_("bom_no"),
|
||||||
|
doctype.item_name,
|
||||||
|
)
|
||||||
|
.where(doctype.name.isin(items))
|
||||||
|
)
|
||||||
|
|
||||||
|
item_details = query.run(as_dict=True)
|
||||||
|
item_wise_details = frappe._dict({})
|
||||||
|
|
||||||
|
if not item_details:
|
||||||
|
return item_wise_details
|
||||||
|
|
||||||
|
for row in item_details:
|
||||||
|
row.cumulative_lead_time = self.get_cumulative_lead_time(row.item_code, row.bom_no)
|
||||||
|
|
||||||
|
for row in item_details:
|
||||||
|
item_wise_details.setdefault(row.item_code, row)
|
||||||
|
|
||||||
|
return item_wise_details
|
||||||
|
|
||||||
|
def get_cumulative_lead_time(self, item_code, bom_no, time_in_days=0):
|
||||||
|
if not time_in_days:
|
||||||
|
time_in_days = get_item_lead_time(item_code)
|
||||||
|
|
||||||
|
bom_materials = frappe.get_all(
|
||||||
|
"BOM Item",
|
||||||
|
filters={"parent": bom_no, "docstatus": 1},
|
||||||
|
fields=["item_code", "bom_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in bom_materials:
|
||||||
|
if row.bom_no:
|
||||||
|
time_in_days += self.get_cumulative_lead_time(row.item_code, row.bom_no)
|
||||||
|
else:
|
||||||
|
lead_time = get_item_lead_time(row.item_code)
|
||||||
|
time_in_days += lead_time
|
||||||
|
|
||||||
|
return time_in_days
|
||||||
|
|
||||||
|
def get_demand_data(self):
|
||||||
|
sales_order_data = self.get_sales_orders_data()
|
||||||
|
material_request_data = self.get_material_requests_data()
|
||||||
|
|
||||||
|
return sales_order_data + material_request_data
|
||||||
|
|
||||||
|
def get_material_requests_data(self):
|
||||||
|
if not self.material_requests:
|
||||||
|
return []
|
||||||
|
|
||||||
|
doctype = frappe.qb.DocType("Material Request Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
doctype.item_code,
|
||||||
|
doctype.warehouse,
|
||||||
|
doctype.stock_uom,
|
||||||
|
doctype.schedule_date.as_("delivery_date"),
|
||||||
|
doctype.parent.as_("material_request"),
|
||||||
|
doctype.stock_qty.as_("qty"),
|
||||||
|
)
|
||||||
|
.orderby(doctype.schedule_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.material_requests:
|
||||||
|
material_requests = [m.material_request for m in self.material_requests if m.material_request]
|
||||||
|
query = query.where(doctype.parent.isin(material_requests))
|
||||||
|
|
||||||
|
if self.from_date:
|
||||||
|
query = query.where(doctype.schedule_date >= self.from_date)
|
||||||
|
|
||||||
|
if self.to_date:
|
||||||
|
query = query.where(doctype.schedule_date <= self.to_date)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
def get_sales_orders_data(self):
|
||||||
|
sales_order_schedules = self.get_sales_order_schedules()
|
||||||
|
ignore_orders = []
|
||||||
|
if sales_order_schedules:
|
||||||
|
for row in sales_order_schedules:
|
||||||
|
if row.sales_order not in ignore_orders:
|
||||||
|
ignore_orders.append(row.sales_order)
|
||||||
|
|
||||||
|
sales_orders = self.get_items_from_sales_orders(ignore_orders)
|
||||||
|
|
||||||
|
return sales_orders + sales_order_schedules
|
||||||
|
|
||||||
|
def get_items_from_sales_orders(self, ignore_orders=None):
|
||||||
|
doctype = frappe.qb.DocType("Sales Order Item")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
doctype.item_code,
|
||||||
|
doctype.warehouse,
|
||||||
|
doctype.stock_uom,
|
||||||
|
doctype.delivery_date,
|
||||||
|
doctype.name.as_("sales_order"),
|
||||||
|
doctype.stock_qty.as_("qty"),
|
||||||
|
)
|
||||||
|
.where(doctype.docstatus == 1)
|
||||||
|
.orderby(doctype.delivery_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.from_date:
|
||||||
|
query = query.where(doctype.delivery_date >= self.from_date)
|
||||||
|
|
||||||
|
if self.to_date:
|
||||||
|
query = query.where(doctype.delivery_date <= self.to_date)
|
||||||
|
|
||||||
|
if self.sales_orders:
|
||||||
|
names = [s.sales_order for s in self.sales_orders if s.sales_order]
|
||||||
|
if ignore_orders:
|
||||||
|
names = [name for name in names if name not in ignore_orders]
|
||||||
|
|
||||||
|
query = query.where(doctype.parent.isin(names))
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
def get_sales_order_schedules(self):
|
||||||
|
doctype = frappe.qb.DocType("Delivery Schedule Item")
|
||||||
|
query = frappe.qb.from_(doctype).select(
|
||||||
|
doctype.item_code,
|
||||||
|
doctype.warehouse,
|
||||||
|
doctype.stock_uom,
|
||||||
|
doctype.delivery_date,
|
||||||
|
doctype.sales_order,
|
||||||
|
doctype.stock_qty.as_("qty"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.sales_orders:
|
||||||
|
names = [s.sales_order for s in self.sales_orders if s.sales_order]
|
||||||
|
query = query.where(doctype.sales_order.isin(names))
|
||||||
|
|
||||||
|
if self.from_date:
|
||||||
|
query = query.where(doctype.delivery_date >= self.from_date)
|
||||||
|
|
||||||
|
if self.to_date:
|
||||||
|
query = query.where(doctype.delivery_date <= self.to_date)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
def get_item_wise_mps_data(self, data):
|
||||||
|
item_wise_data = frappe._dict({})
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
key = (item.item_code, item.delivery_date)
|
||||||
|
|
||||||
|
if key not in item_wise_data:
|
||||||
|
item_wise_data[key] = frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"delivery_date": item.delivery_date,
|
||||||
|
"stock_uom": item.stock_uom,
|
||||||
|
"qty": 0.0,
|
||||||
|
"cumulative_lead_time": 0.0,
|
||||||
|
"order_release_date": item.delivery_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
item_details = item_wise_data[key]
|
||||||
|
item_details.qty += item.qty
|
||||||
|
|
||||||
|
return item_wise_data
|
||||||
|
|
||||||
|
def add_mps_data(self, data):
|
||||||
|
data = frappe._dict(sorted(data.items(), key=lambda x: x[0][1]))
|
||||||
|
|
||||||
|
for key in data:
|
||||||
|
row = data[key]
|
||||||
|
row.cumulative_lead_time = math.ceil(row.cumulative_lead_time)
|
||||||
|
row.order_release_date = add_days(row.delivery_date, -row.cumulative_lead_time)
|
||||||
|
if getdate(row.order_release_date) < getdate(today()):
|
||||||
|
continue
|
||||||
|
|
||||||
|
row.planned_qty = row.qty
|
||||||
|
row.uom = row.stock_uom
|
||||||
|
row.warehouse = row.warehouse or self.parent_warehouse
|
||||||
|
self.append("items", row)
|
||||||
|
|
||||||
|
def get_distinct_items(self, data):
|
||||||
|
items = []
|
||||||
|
for item in data:
|
||||||
|
if item.item_code not in items:
|
||||||
|
items.append(item.item_code)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def fetch_materials_requests(self, **data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = parse_json(data)
|
||||||
|
|
||||||
|
self.set("material_requests", [])
|
||||||
|
materials_requests = self.get_material_requests(data)
|
||||||
|
if not materials_requests:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("No open Material Requests found for the given criteria."),
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in materials_requests:
|
||||||
|
self.append(
|
||||||
|
"material_requests",
|
||||||
|
{
|
||||||
|
"material_request": row.name,
|
||||||
|
"material_request_date": row.transaction_date,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.is_new():
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_material_requests(self, data):
|
||||||
|
doctype = frappe.qb.DocType("Material Request")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
doctype.name,
|
||||||
|
doctype.transaction_date,
|
||||||
|
)
|
||||||
|
.where((doctype.docstatus == 1) & (doctype.status.notin(["Closed", "Completed"])))
|
||||||
|
.orderby(doctype.schedule_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get("material_request_type"):
|
||||||
|
query = query.where(doctype.material_request_type == data.get("material_request_type"))
|
||||||
|
|
||||||
|
if data.get("from_date"):
|
||||||
|
query = query.where(doctype.transaction_date >= data.get("from_date"))
|
||||||
|
|
||||||
|
if data.get("to_date"):
|
||||||
|
query = query.where(doctype.transaction_date <= data.get("to_date"))
|
||||||
|
|
||||||
|
if self.from_date:
|
||||||
|
query = query.where(doctype.schedule_date >= self.from_date)
|
||||||
|
|
||||||
|
if self.to_date:
|
||||||
|
query = query.where(doctype.schedule_date <= self.to_date)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def fetch_sales_orders(self, **data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = parse_json(data)
|
||||||
|
|
||||||
|
self.set("sales_orders", [])
|
||||||
|
sales_orders = self.get_sales_orders(data)
|
||||||
|
if not sales_orders:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in sales_orders:
|
||||||
|
self.append(
|
||||||
|
"sales_orders",
|
||||||
|
{
|
||||||
|
"sales_order": row.name,
|
||||||
|
"sales_order_date": row.transaction_date,
|
||||||
|
"delivery_date": row.delivery_date,
|
||||||
|
"customer": row.customer,
|
||||||
|
"status": row.status,
|
||||||
|
"grand_total": row.grand_total,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.is_new():
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_sales_orders(self, kwargs):
|
||||||
|
doctype = frappe.qb.DocType("Sales Order")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
doctype.name,
|
||||||
|
doctype.transaction_date,
|
||||||
|
doctype.delivery_date,
|
||||||
|
doctype.customer,
|
||||||
|
doctype.status,
|
||||||
|
doctype.grand_total,
|
||||||
|
)
|
||||||
|
.where((doctype.docstatus == 1) & (doctype.status.notin(["Closed", "Completed"])))
|
||||||
|
.orderby(doctype.delivery_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("customer"):
|
||||||
|
query = query.where(doctype.customer == kwargs.get("customer"))
|
||||||
|
|
||||||
|
if kwargs.get("from_date"):
|
||||||
|
query = query.where(doctype.transaction_date >= kwargs.get("from_date"))
|
||||||
|
|
||||||
|
if kwargs.get("to_date"):
|
||||||
|
query = query.where(doctype.transaction_date <= kwargs.get("to_date"))
|
||||||
|
|
||||||
|
if kwargs.get("delivery_from_date"):
|
||||||
|
query = query.where(doctype.delivery_date >= kwargs.get("delivery_from_date"))
|
||||||
|
|
||||||
|
if kwargs.get("delivery_to_date"):
|
||||||
|
query = query.where(doctype.delivery_date <= kwargs.get("to_delivery_date"))
|
||||||
|
|
||||||
|
if items := self.get_items_for_mps():
|
||||||
|
doctype_item = frappe.qb.DocType("Sales Order Item")
|
||||||
|
query = query.join(doctype_item).on(doctype_item.parent == doctype.name)
|
||||||
|
query = query.where(doctype_item.item_code.isin(items))
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
def get_items_for_mps(self):
|
||||||
|
if not self.select_items:
|
||||||
|
return
|
||||||
|
|
||||||
|
return [d.item_code for d in self.select_items if d.item_code]
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
self.enqueue_mrp_creation()
|
||||||
|
|
||||||
|
def enqueue_mrp_creation(self):
|
||||||
|
frappe.enqueue_doc("Master Production Schedule", self.name, "make_mrp", queue="long", timeout=1800)
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
_("MRP Log documents are being created in the background."),
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_lead_time(item_code):
|
||||||
|
doctype = frappe.qb.DocType("Item Lead Time")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(
|
||||||
|
((doctype.manufacturing_time_in_mins / 1440) + doctype.purchase_time + doctype.buffer_time).as_(
|
||||||
|
"cumulative_lead_time"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(doctype.item_code == item_code)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = query.run(as_dict=True)
|
||||||
|
if result:
|
||||||
|
return result[0].cumulative_lead_time or 0
|
||||||
|
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestMasterProductionSchedule(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for MasterProductionSchedule.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-08-12 17:09:08.171687",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"delivery_date",
|
||||||
|
"cumulative_lead_time",
|
||||||
|
"order_release_date",
|
||||||
|
"planned_qty",
|
||||||
|
"warehouse",
|
||||||
|
"item_name",
|
||||||
|
"bom_no",
|
||||||
|
"uom"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "delivery_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Delivery Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "planned_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Planned Qty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "order_release_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Start Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "bom_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "BOM No",
|
||||||
|
"options": "BOM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "UOM",
|
||||||
|
"options": "UOM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "cumulative_lead_time",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Lead Time",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-09-02 19:41:27.167095",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Master Production Schedule Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class MasterProductionScheduleItem(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
bom_no: DF.Link | None
|
||||||
|
cumulative_lead_time: DF.Int
|
||||||
|
delivery_date: DF.Date | None
|
||||||
|
item_code: DF.Link | None
|
||||||
|
item_name: DF.Data | None
|
||||||
|
order_release_date: DF.Date | None
|
||||||
|
parent: DF.Data
|
||||||
|
parentfield: DF.Data
|
||||||
|
parenttype: DF.Data
|
||||||
|
planned_qty: DF.Float
|
||||||
|
uom: DF.Link | None
|
||||||
|
warehouse: DF.Link | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"sales_order_date",
|
"sales_order_date",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"customer",
|
"customer",
|
||||||
"grand_total"
|
"grand_total",
|
||||||
|
"status"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -58,18 +59,26 @@
|
|||||||
"print_width": "120px",
|
"print_width": "120px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"width": "120px"
|
"width": "120px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:20.746852",
|
"modified": "2025-08-21 15:16:13.828240",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sales Order",
|
"name": "Production Plan Sales Order",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ProductionPlanSalesOrder(Document):
|
|||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
sales_order: DF.Link
|
sales_order: DF.Link
|
||||||
sales_order_date: DF.Date | None
|
sales_order_date: DF.Date | None
|
||||||
|
status: DF.Data | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
frappe.ui.form.on("Sales Forecast", {
|
||||||
|
refresh(frm) {
|
||||||
|
frm.trigger("set_query_filters");
|
||||||
|
frm.trigger("set_custom_buttons");
|
||||||
|
},
|
||||||
|
|
||||||
|
set_query_filters(frm) {
|
||||||
|
frm.set_query("parent_warehouse", (doc) => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_group: 1,
|
||||||
|
company: doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("item_code", "items", () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
disabled: 0,
|
||||||
|
is_stock_item: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
generate_demand(frm) {
|
||||||
|
frm.call({
|
||||||
|
method: "generate_demand",
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
callback: function (r) {
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_custom_buttons(frm) {
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.status === "Planned") {
|
||||||
|
frm.add_custom_button(__("Create MPS"), () => {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.manufacturing.doctype.sales_forecast.sales_forecast.create_mps",
|
||||||
|
frm: frm,
|
||||||
|
});
|
||||||
|
}).addClass("btn-primary");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("Sales Forecast Item", {
|
||||||
|
adjust_qty(frm, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
row.demand_qty = row.forecast_qty + row.adjust_qty;
|
||||||
|
frappe.model.set_value(cdt, cdn, "demand_qty", row.demand_qty);
|
||||||
|
},
|
||||||
|
});
|
||||||
252
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json
Normal file
252
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "naming_series:",
|
||||||
|
"creation": "2025-08-12 17:20:16.012501",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"company",
|
||||||
|
"posting_date",
|
||||||
|
"forecasting_method",
|
||||||
|
"column_break_xdcy",
|
||||||
|
"from_date",
|
||||||
|
"frequency",
|
||||||
|
"demand_number",
|
||||||
|
"section_break_rrrx",
|
||||||
|
"column_break_klws",
|
||||||
|
"selected_items",
|
||||||
|
"column_break_mtqw",
|
||||||
|
"parent_warehouse",
|
||||||
|
"section_break_cmgo",
|
||||||
|
"generate_demand",
|
||||||
|
"items",
|
||||||
|
"section_break_kuzf",
|
||||||
|
"amended_from",
|
||||||
|
"column_break_laqr",
|
||||||
|
"status",
|
||||||
|
"connections_tab"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "SF.YY.-.######",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "SF.YY.-.######",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xdcy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Posting Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_rrrx",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item and Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_klws",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "selected_items",
|
||||||
|
"fieldtype": "Table MultiSelect",
|
||||||
|
"label": "Select Items",
|
||||||
|
"options": "Sales Forecast Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_mtqw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse.",
|
||||||
|
"fieldname": "parent_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Parent Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cmgo",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 1,
|
||||||
|
"fieldname": "items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Items",
|
||||||
|
"options": "Sales Forecast Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "generate_demand",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Generate Demand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Sales Forecast",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "6",
|
||||||
|
"fieldname": "demand_number",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Number of Weeks / Months",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "connections_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Connections",
|
||||||
|
"show_dashboard": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_kuzf",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_laqr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Planned\nMPS Generated",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Holt-Winters",
|
||||||
|
"fieldname": "forecasting_method",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Forecasting Method",
|
||||||
|
"options": "Holt-Winters\nManual"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Monthly",
|
||||||
|
"fieldname": "frequency",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Frequency",
|
||||||
|
"options": "Weekly\nMonthly",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-09-21 13:24:34.720794",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Sales Forecast",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Manufacturing User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Manufacturing Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
228
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py
Normal file
228
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas as pd
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import DateFormat, Sum, YearWeek
|
||||||
|
from frappe.utils import add_to_date, cint, date_diff, flt
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
|
|
||||||
|
class SalesForecast(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.sales_forecast_item.sales_forecast_item import SalesForecastItem
|
||||||
|
|
||||||
|
amended_from: DF.Link | None
|
||||||
|
company: DF.Link
|
||||||
|
demand_number: DF.Int
|
||||||
|
forecasting_method: DF.Literal["Holt-Winters", "Manual"]
|
||||||
|
frequency: DF.Literal["Weekly", "Monthly"]
|
||||||
|
from_date: DF.Date
|
||||||
|
items: DF.Table[SalesForecastItem]
|
||||||
|
naming_series: DF.Literal["SF.YY.-.######"]
|
||||||
|
parent_warehouse: DF.Link
|
||||||
|
posting_date: DF.Date | None
|
||||||
|
selected_items: DF.TableMultiSelect[SalesForecastItem]
|
||||||
|
status: DF.Literal["Planned", "MPS Generated"]
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
self.validate_demand_qty()
|
||||||
|
|
||||||
|
def validate_demand_qty(self):
|
||||||
|
if self.forecasting_method == "Manual":
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
demand_qty = row.forecast_qty + flt(row.adjust_qty)
|
||||||
|
if row.demand_qty != demand_qty:
|
||||||
|
row.demand_qty = demand_qty
|
||||||
|
|
||||||
|
def get_sales_data(self):
|
||||||
|
to_date = self.from_date
|
||||||
|
from_date = add_to_date(to_date, years=-3)
|
||||||
|
|
||||||
|
doctype = frappe.qb.DocType("Sales Order")
|
||||||
|
child_doctype = frappe.qb.DocType("Sales Order Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.inner_join(child_doctype)
|
||||||
|
.on(child_doctype.parent == doctype.name)
|
||||||
|
.select(child_doctype.item_code, Sum(child_doctype.qty).as_("qty"), doctype.transaction_date)
|
||||||
|
.where((doctype.docstatus == 1) & (doctype.transaction_date.between(from_date, to_date)))
|
||||||
|
.groupby(child_doctype.item_code)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.selected_items:
|
||||||
|
items = [item.item_code for item in self.selected_items]
|
||||||
|
query = query.where(child_doctype.item_code.isin(items))
|
||||||
|
|
||||||
|
if self.parent_warehouse:
|
||||||
|
warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
|
||||||
|
query = query.where(child_doctype.warehouse.isin(warehouses))
|
||||||
|
|
||||||
|
query = query.groupby(doctype.transaction_date)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
def generate_manual_demand(self):
|
||||||
|
forecast_demand = []
|
||||||
|
for row in self.selected_items:
|
||||||
|
item_details = frappe.db.get_value(
|
||||||
|
"Item", row.item_code, ["item_name", "stock_uom as uom"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for index in range(self.demand_number):
|
||||||
|
if self.horizon_type == "Monthly":
|
||||||
|
delivery_date = add_to_date(self.from_date, months=index + 1)
|
||||||
|
else:
|
||||||
|
delivery_date = add_to_date(self.from_date, weeks=index + 1)
|
||||||
|
|
||||||
|
forecast_demand.append(
|
||||||
|
{
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"delivery_date": delivery_date,
|
||||||
|
"item_name": item_details.item_name,
|
||||||
|
"uom": item_details.uom,
|
||||||
|
"demand_qty": 0.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for demand in forecast_demand:
|
||||||
|
self.append("items", demand)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def generate_demand(self):
|
||||||
|
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
||||||
|
|
||||||
|
self.set("items", [])
|
||||||
|
|
||||||
|
if self.forecasting_method == "Manual":
|
||||||
|
self.generate_manual_demand()
|
||||||
|
return
|
||||||
|
|
||||||
|
sales_data = self.get_sales_data()
|
||||||
|
if not sales_data:
|
||||||
|
frappe.throw(_("No sales data found for the selected items."))
|
||||||
|
|
||||||
|
itemwise_data = self.group_sales_data_by_item(sales_data)
|
||||||
|
|
||||||
|
for item_code, data in itemwise_data.items():
|
||||||
|
seasonal_periods = self.get_seasonal_periods(data)
|
||||||
|
pd_sales_data = pd.DataFrame({"item": data.item, "date": data.date, "qty": data.qty})
|
||||||
|
|
||||||
|
resample_val = "M" if self.horizon_type == "Monthly" else "W"
|
||||||
|
_sales_data = pd_sales_data.set_index("date").resample(resample_val).sum()["qty"]
|
||||||
|
|
||||||
|
model = ExponentialSmoothing(
|
||||||
|
_sales_data, trend="add", seasonal="mul", seasonal_periods=seasonal_periods
|
||||||
|
)
|
||||||
|
|
||||||
|
fit = model.fit()
|
||||||
|
forecast = fit.forecast(self.demand_number)
|
||||||
|
|
||||||
|
forecast_data = forecast.to_dict()
|
||||||
|
if forecast_data:
|
||||||
|
self.add_sales_forecast_item(item_code, forecast_data)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def add_sales_forecast_item(self, item_code, forecast_data):
|
||||||
|
item_details = frappe.db.get_value(
|
||||||
|
"Item", item_code, ["item_name", "stock_uom as uom", "name as item_code"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
uom_whole_number = frappe.get_cached_value("UOM", item_details.uom, "must_be_whole_number")
|
||||||
|
|
||||||
|
for date, qty in forecast_data.items():
|
||||||
|
if uom_whole_number:
|
||||||
|
qty = round(qty)
|
||||||
|
|
||||||
|
item_details.update(
|
||||||
|
{
|
||||||
|
"delivery_date": date,
|
||||||
|
"forecast_qty": qty,
|
||||||
|
"demand_qty": qty,
|
||||||
|
"warehouse": self.parent_warehouse,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.append("items", item_details)
|
||||||
|
|
||||||
|
def get_seasonal_periods(self, data):
|
||||||
|
days = date_diff(data["end_date"], data["start_date"])
|
||||||
|
if self.horizon_type == "Monthly":
|
||||||
|
months = (days / 365) * 12
|
||||||
|
seasonal_periods = cint(months / 2)
|
||||||
|
if seasonal_periods > 12:
|
||||||
|
seasonal_periods = 12
|
||||||
|
else:
|
||||||
|
weeks = days / 7
|
||||||
|
seasonal_periods = cint(weeks / 2)
|
||||||
|
if seasonal_periods > 52:
|
||||||
|
seasonal_periods = 52
|
||||||
|
|
||||||
|
return seasonal_periods
|
||||||
|
|
||||||
|
def group_sales_data_by_item(self, sales_data):
|
||||||
|
"""
|
||||||
|
Group sales data by item code and calculate total quantity sold.
|
||||||
|
"""
|
||||||
|
itemwise_data = frappe._dict({})
|
||||||
|
for row in sales_data:
|
||||||
|
if row.item_code not in itemwise_data:
|
||||||
|
itemwise_data[row.item_code] = frappe._dict(
|
||||||
|
{
|
||||||
|
"start_date": row.transaction_date,
|
||||||
|
"item": [],
|
||||||
|
"date": [],
|
||||||
|
"qty": [],
|
||||||
|
"end_date": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
item_data = itemwise_data[row.item_code]
|
||||||
|
item_data["item"].append(row.item_code)
|
||||||
|
item_data["date"].append(pd.to_datetime(row.transaction_date))
|
||||||
|
item_data["qty"].append(row.qty)
|
||||||
|
item_data["end_date"] = row.transaction_date
|
||||||
|
|
||||||
|
return itemwise_data
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_mps(source_name, target_doc=None):
|
||||||
|
def postprocess(source, doc):
|
||||||
|
doc.naming_series = "MPS.YY.-.######"
|
||||||
|
|
||||||
|
doc = get_mapped_doc(
|
||||||
|
"Sales Forecast",
|
||||||
|
source_name,
|
||||||
|
{
|
||||||
|
"Sales Forecast": {
|
||||||
|
"doctype": "Master Production Schedule",
|
||||||
|
"validation": {"docstatus": ["=", 1]},
|
||||||
|
"field_map": {
|
||||||
|
"name": "sales_forecast",
|
||||||
|
"from_date": "from_date",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
target_doc,
|
||||||
|
postprocess,
|
||||||
|
)
|
||||||
|
|
||||||
|
return doc
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
return {
|
||||||
|
"fieldname": "demand_planning",
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"label": _("MPS"),
|
||||||
|
"items": ["Master Production Schedule"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
frappe.listview_settings["Sales Forecast"] = {
|
||||||
|
add_fields: ["status"],
|
||||||
|
get_indicator: function (doc) {
|
||||||
|
if (doc.status === "Planned") {
|
||||||
|
// Closed
|
||||||
|
return [__("Planned"), "orange", "status,=,Planned"];
|
||||||
|
} else if (doc.status === "MPS Generated") {
|
||||||
|
// on hold
|
||||||
|
return [__("MPS Generated"), "green", "status,=,MPS Generated"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestSalesForecast(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for SalesForecast.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-08-18 20:57:19.816490",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"uom",
|
||||||
|
"delivery_date",
|
||||||
|
"forecast_qty",
|
||||||
|
"adjust_qty",
|
||||||
|
"demand_qty",
|
||||||
|
"warehouse"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fetch_from": "item_code.sales_uom",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "delivery_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Delivery Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "forecast_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Forecast Qty",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "adjust_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Adjust Qty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 3,
|
||||||
|
"fieldname": "demand_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Demand Qty",
|
||||||
|
"non_negative": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-08-18 21:59:38.859082",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Sales Forecast Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class SalesForecastItem(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
adjust_qty: DF.Float
|
||||||
|
delivery_date: DF.Date | None
|
||||||
|
demand_qty: DF.Float
|
||||||
|
forecast_qty: DF.Float
|
||||||
|
item_code: DF.Link
|
||||||
|
item_name: DF.Data | None
|
||||||
|
parent: DF.Data
|
||||||
|
parentfield: DF.Data
|
||||||
|
parenttype: DF.Data
|
||||||
|
uom: DF.Link | None
|
||||||
|
warehouse: DF.Link | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -3005,6 +3005,36 @@ class TestWorkOrder(IntegrationTestCase):
|
|||||||
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
|
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_allow_additional_material_transfer(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||||
|
make_stock_entry as make_stock_entry_test_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50)
|
||||||
|
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
|
||||||
|
for row in wo_order.required_items:
|
||||||
|
make_stock_entry_test_record(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=row.source_warehouse,
|
||||||
|
qty=row.required_qty * 2,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2))
|
||||||
|
stock_entry.insert()
|
||||||
|
stock_entry.submit()
|
||||||
|
|
||||||
|
wo_order.reload()
|
||||||
|
self.assertEqual(wo_order.material_transferred_for_manufacturing, 2)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
|
||||||
|
stock_entry.insert()
|
||||||
|
stock_entry.submit()
|
||||||
|
|
||||||
|
wo_order.reload()
|
||||||
|
self.assertEqual(wo_order.material_transferred_for_manufacturing, 3)
|
||||||
|
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0)
|
||||||
|
|
||||||
|
|
||||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||||
|
|||||||
@@ -732,6 +732,19 @@ erpnext.work_order = {
|
|||||||
let pending_to_transfer = frm.doc.required_items.some(
|
let pending_to_transfer = frm.doc.required_items.some(
|
||||||
(item) => flt(item.transferred_qty) < flt(item.required_qty)
|
(item) => flt(item.transferred_qty) < flt(item.required_qty)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let transfer_extra_materials_percentage =
|
||||||
|
frm.doc.__onload?.transfer_extra_materials_percentage;
|
||||||
|
let allowed_qty = 0;
|
||||||
|
let transfer_extra_materials = false;
|
||||||
|
if (!pending_to_transfer && transfer_extra_materials_percentage) {
|
||||||
|
allowed_qty = frm.doc.qty + (transfer_extra_materials_percentage / 100) * frm.doc.qty;
|
||||||
|
|
||||||
|
if (allowed_qty > frm.doc.material_transferred_for_manufacturing) {
|
||||||
|
transfer_extra_materials = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pending_to_transfer && frm.doc.status != "Stopped") {
|
if (pending_to_transfer && frm.doc.status != "Stopped") {
|
||||||
frm.has_start_btn = true;
|
frm.has_start_btn = true;
|
||||||
frm.add_custom_button(__("Create Pick List"), function () {
|
frm.add_custom_button(__("Create Pick List"), function () {
|
||||||
@@ -742,6 +755,14 @@ erpnext.work_order = {
|
|||||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
||||||
});
|
});
|
||||||
start_btn.addClass("btn-primary");
|
start_btn.addClass("btn-primary");
|
||||||
|
} else if (transfer_extra_materials && allowed_qty) {
|
||||||
|
let qty = allowed_qty - flt(frm.doc.material_transferred_for_manufacturing);
|
||||||
|
|
||||||
|
if (qty > 0) {
|
||||||
|
frm.add_custom_button(__("Transfer Extra Material"), function () {
|
||||||
|
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture", qty);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -967,19 +988,35 @@ erpnext.work_order = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
make_se: function (frm, purpose) {
|
make_se: function (frm, purpose, qty) {
|
||||||
this.show_prompt_for_qty_input(frm, purpose)
|
if (qty) {
|
||||||
.then((data) => {
|
frappe
|
||||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||||
work_order_id: frm.doc.name,
|
work_order_id: frm.doc.name,
|
||||||
purpose: purpose,
|
purpose: purpose,
|
||||||
qty: data.qty,
|
qty: qty,
|
||||||
|
})
|
||||||
|
.then((stock_entry) => {
|
||||||
|
frappe.model.sync(stock_entry);
|
||||||
|
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||||
});
|
});
|
||||||
})
|
} else {
|
||||||
.then((stock_entry) => {
|
this.show_prompt_for_qty_input(frm, purpose)
|
||||||
frappe.model.sync(stock_entry);
|
.then((data) => {
|
||||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
return frappe.xcall(
|
||||||
});
|
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
|
||||||
|
{
|
||||||
|
work_order_id: frm.doc.name,
|
||||||
|
purpose: purpose,
|
||||||
|
qty: data.qty,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((stock_entry) => {
|
||||||
|
frappe.model.sync(stock_entry);
|
||||||
|
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {
|
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user