Compare commits

..

1 Commits

Author SHA1 Message Date
Ankush Menat
28ce2958fa ci: Add fake passing tests when CI is skipped 2024-01-25 14:14:25 +05:30
127 changed files with 2759 additions and 4883 deletions

View File

@@ -7,7 +7,8 @@
<p>ERP made simple</p>
</p>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![UI](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml/badge.svg?branch=develop&event=schedule)](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)

View File

@@ -118,7 +118,6 @@ class Account(NestedSet):
self.validate_balance_must_be_debit_or_credit()
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
self.validate_receivable_payable_account_type()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -189,24 +188,6 @@ class Account(NestedSet):
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
def validate_receivable_payable_account_type(self):
doc_before_save = self.get_doc_before_save()
receivable_payable_types = ["Receivable", "Payable"]
if (
doc_before_save
and doc_before_save.account_type in receivable_payable_types
and doc_before_save.account_type != self.account_type
):
# check for ledger entries
if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1):
msg = _(
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
).format(
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
)
frappe.msgprint(msg)
self.add_comment("Comment", msg)
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()

View File

@@ -6,7 +6,6 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import (
InvalidAccountMergeError,
@@ -325,19 +324,6 @@ class TestAccount(unittest.TestCase):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
def test_account_balance(self):
from erpnext.accounts.utils import get_balance_on
if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Test Percent Account %5"
acc.parent_account = "Tax Assets - _TC"
acc.company = "_Test Company"
acc.insert()
balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate())
self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects

View File

@@ -1,6 +1,7 @@
{
"actions": [],
"creation": "2013-06-24 15:49:57",
"description": "Settings for Accounts",
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
@@ -461,7 +462,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-30 14:04:26.553554",
"modified": "2023-11-20 09:37:47.650347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -9,7 +9,6 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
from frappe.utils import comma_and, get_link_to_form
class BankAccount(Document):
@@ -53,17 +52,6 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
self.validate_account()
def validate_account(self):
if self.account:
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def validate_company(self):
if self.is_company_account and not self.company:

View File

@@ -5,9 +5,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
from pypika import Order
import erpnext
@@ -181,62 +179,39 @@ def get_payment_entries_for_bank_clearance(
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice")
acc = frappe.qb.DocType("Account")
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
pos_sales_invoices = (
frappe.qb.from_(si_payment)
.inner_join(si)
.on(si_payment.parent == si.name)
.inner_join(acc)
.on(si_payment.account == acc.name)
.select(
ConstantColumn("Sales Invoice").as_("payment_document"),
si.name.as_("payment_entry"),
si_payment.reference_no.as_("cheque_number"),
si_payment.amount.as_("debit"),
si.posting_date,
si.customer.as_("against_account"),
si_payment.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("credit"),
)
.where(
(si.docstatus == 1)
& (si_payment.account == account)
& (si.posting_date >= from_date)
& (si.posting_date <= to_date)
)
.orderby(si.posting_date)
.orderby(si.name, order=Order.desc)
).run(as_dict=True)
pi = frappe.qb.DocType("Purchase Invoice")
pos_purchase_invoices = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
).run(as_dict=True)
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
entries = (
list(payment_entries)

View File

@@ -80,8 +80,7 @@ class BankStatementImport(DataImport):
from frappe.utils.background_jobs import is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
run_now = frappe.flags.in_test or frappe.conf.developer_mode
if is_scheduler_inactive() and not run_now:
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
job_id = f"bank_statement_import::{self.name}"
@@ -98,7 +97,7 @@ class BankStatementImport(DataImport):
google_sheets_url=self.google_sheets_url,
bank=self.bank,
template_options=self.template_options,
now=run_now,
now=frappe.conf.developer_mode or frappe.flags.in_test,
)
return True

View File

@@ -32,16 +32,8 @@ class TestBankTransaction(FrappeTestCase):
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
bank_account = create_bank_account(
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
)
add_transactions(bank_account=bank_account)
add_vouchers(gl_account=gl_account)
add_transactions()
add_vouchers()
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
@@ -227,9 +219,7 @@ def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
def create_bank_account(
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
):
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
frappe.get_doc(
{
@@ -241,35 +231,21 @@ def create_bank_account(
pass
try:
bank_account = frappe.get_doc(
frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": bank_account_name,
"account_name": "Checking Account",
"bank": bank_name,
"account": gl_account,
"account": account_name,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return bank_account.name
def add_transactions():
create_bank_account()
def create_gl_account(gl_account_name="_Test Bank - _TC"):
gl_account = frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"parent_account": "Current Assets - _TC",
"account_type": "Bank",
"is_group": 0,
"account_name": gl_account_name,
}
).insert()
return gl_account.name
def add_transactions(bank_account="_Test Bank - _TC"):
doc = frappe.get_doc(
{
"doctype": "Bank Transaction",
@@ -277,7 +253,7 @@ def add_transactions(bank_account="_Test Bank - _TC"):
"date": "2018-10-23",
"deposit": 1200,
"currency": "INR",
"bank_account": bank_account,
"bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit()
@@ -289,7 +265,7 @@ def add_transactions(bank_account="_Test Bank - _TC"):
"date": "2018-10-23",
"deposit": 1700,
"currency": "INR",
"bank_account": bank_account,
"bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit()
@@ -301,7 +277,7 @@ def add_transactions(bank_account="_Test Bank - _TC"):
"date": "2018-10-26",
"withdrawal": 690,
"currency": "INR",
"bank_account": bank_account,
"bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit()
@@ -313,7 +289,7 @@ def add_transactions(bank_account="_Test Bank - _TC"):
"date": "2018-10-27",
"deposit": 3900,
"currency": "INR",
"bank_account": bank_account,
"bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit()
@@ -325,13 +301,13 @@ def add_transactions(bank_account="_Test Bank - _TC"):
"date": "2018-10-27",
"withdrawal": 109080,
"currency": "INR",
"bank_account": bank_account,
"bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit()
def add_vouchers(gl_account="_Test Bank - _TC"):
def add_vouchers():
try:
frappe.get_doc(
{
@@ -347,7 +323,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
@@ -366,14 +342,14 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
pass
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Herr G Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Herr G Nov 18"
pe.reference_date = "2018-11-01"
pe.insert()
@@ -404,10 +380,10 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
pi.cash_bank_account = gl_account
pi.cash_bank_account = "_Test Bank - _TC"
pi.insert()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.paid_amount = 690
@@ -416,7 +392,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
pe.submit()
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
@@ -439,12 +415,16 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
if not frappe.db.get_value(
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
):
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
mode_of_payment.append(
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
)
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
si.append(
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
)
si.insert()
si.submit()

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"autoname": "field:year",
"creation": "2013-01-22 16:50:25",
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -82,12 +82,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2024-01-30 12:35:38.645968",
"modified": "2024-01-17 13:06:01.608953",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"naming_rule": "By fieldname",
"owner": "Administrator",
"owner": "Administrator",
"permissions": [
{
"create": 1,
@@ -131,6 +130,5 @@
],
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC",
"states": []
"sort_order": "DESC"
}

View File

@@ -13,9 +13,16 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.exceptions import (
InvalidAccountCurrency,
InvalidAccountDimensionError,
MandatoryAccountDimensionError,
)
exclude_from_linked_with = True
@@ -91,6 +98,7 @@ class GLEntry(Document):
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
@@ -200,6 +208,42 @@ class GLEntry(Document):
)
)
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if self.account == account:
if value["is_mandatory"] and not self.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
else:
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
def check_pl_account(self):
if (
self.is_opening == "Yes"

View File

@@ -8,6 +8,6 @@ def get_data():
{"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
{"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
{"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
{"label": _("Stock"), "items": ["Item Group", "Item"]},
{"label": _("Stock"), "items": ["Item Groups", "Item"]},
],
}

View File

@@ -150,20 +150,6 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("submit", timeout=4600)
else:
return self._submit()
def cancel(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("cancel", timeout=4600)
else:
return self._cancel()
def on_submit(self):
self.validate_cheque_info()
self.check_credit_limit()
@@ -201,8 +187,8 @@ class JournalEntry(AccountsController):
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in advance_payment_doctypes:
@@ -1169,9 +1155,7 @@ class JournalEntry(AccountsController):
@frappe.whitelist()
def get_default_bank_cash_account(
company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
):
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
if mode_of_payment:
@@ -1209,7 +1193,7 @@ def get_default_bank_cash_account(
return frappe._dict(
{
"account": account,
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
"balance": get_balance_on(account),
"account_currency": account_details.account_currency,
"account_type": account_details.account_type,
}

View File

@@ -1,77 +1,173 @@
{
"actions": [],
"autoname": "field:distribution_id",
"creation": "2013-01-10 16:34:05",
"description": "Helps you distribute the Budget/Target across months if you have seasonality in your business.",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"distribution_id",
"fiscal_year",
"percentages"
],
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:distribution_id",
"beta": 0,
"creation": "2013-01-10 16:34:05",
"custom": 0,
"description": "**Monthly Distribution** helps you distribute the Budget/Target across months if you have seasonality in your business.",
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"description": "Name of the Monthly Distribution",
"fieldname": "distribution_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Distribution Name",
"oldfieldname": "distribution_id",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Name of the Monthly Distribution",
"fieldname": "distribution_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Distribution Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "distribution_id",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"search_index": 1
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fiscal_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"length": 0,
"no_copy": 0,
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "percentages",
"fieldtype": "Table",
"label": "Monthly Distribution Percentages",
"oldfieldname": "budget_distribution_details",
"oldfieldtype": "Table",
"options": "Monthly Distribution Percentage"
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "percentages",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Monthly Distribution Percentages",
"length": 0,
"no_copy": 0,
"oldfieldname": "budget_distribution_details",
"oldfieldtype": "Table",
"options": "Monthly Distribution Percentage",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"icon": "fa fa-bar-chart",
"idx": 1,
"links": [],
"modified": "2024-01-30 13:57:55.802744",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
"naming_rule": "By fieldname",
"owner": "Administrator",
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bar-chart",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-21 14:54:35.998761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
"name_case": "Title Case",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"permlevel": 2,
"read": 1,
"report": 1,
"role": "Accounts Manager"
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 2,
"print": 0,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

@@ -87,14 +87,12 @@
"status",
"custom_remarks",
"remarks",
"base_in_words",
"column_break_16",
"letter_head",
"print_heading",
"bank",
"bank_account_no",
"payment_order",
"in_words",
"subscription_section",
"auto_repeat",
"amended_from",
@@ -749,20 +747,6 @@
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
"read_only": 1
},
{
"fieldname": "base_in_words",
"fieldtype": "Small Text",
"label": "In Words (Company Currency)",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "in_words",
"fieldtype": "Small Text",
"label": "In Words",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,

View File

@@ -178,7 +178,6 @@ class PaymentEntry(AccountsController):
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
self.set_status()
self.set_total_in_words()
def on_submit(self):
if self.difference_amount:
@@ -787,21 +786,6 @@ class PaymentEntry(AccountsController):
self.db_set("status", self.status, update_modified=True)
def set_total_in_words(self):
from frappe.utils import money_in_words
if self.payment_type in ("Pay", "Internal Transfer"):
base_amount = abs(self.base_paid_amount)
amount = abs(self.paid_amount)
currency = self.paid_from_account_currency
elif self.payment_type == "Receive":
base_amount = abs(self.base_received_amount)
amount = abs(self.received_amount)
currency = self.paid_to_account_currency
self.base_in_words = money_in_words(base_amount, self.company_currency)
self.in_words = money_in_words(amount, currency)
def set_tax_withholding(self):
if self.party_type != "Supplier":
return
@@ -943,8 +927,8 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@@ -1032,19 +1016,19 @@ class PaymentEntry(AccountsController):
)
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
self.difference_amount = base_party_amount - self.base_received_amount
elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
self.difference_amount = self.base_paid_amount - base_party_amount
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
self.difference_amount = flt(
self.difference_amount - total_deductions, self.precision("difference_amount")
self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
)
def get_included_taxes(self):
@@ -1444,8 +1428,8 @@ class PaymentEntry(AccountsController):
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc(
@@ -2220,7 +2204,6 @@ def get_payment_entry(
party_type=None,
payment_type=None,
reference_date=None,
ignore_permissions=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2243,14 +2226,14 @@ def get_payment_entry(
)
# bank or cash
bank = get_bank_cash_account(doc, bank_account, ignore_permissions=ignore_permissions)
bank = get_bank_cash_account(doc, bank_account)
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
if party_type in ["Customer", "Supplier"] and not bank:
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
if party_bank_account:
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
bank = get_bank_cash_account(doc, account)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
@@ -2390,13 +2373,9 @@ def update_accounting_dimensions(pe, doc):
pe.set(dimension, doc.get(dimension))
def get_bank_cash_account(doc, bank_account, ignore_permissions=False):
def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account(
doc.company,
"Bank",
mode_of_payment=doc.get("mode_of_payment"),
account=bank_account,
ignore_permissions=ignore_permissions,
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
)
if not bank:

View File

@@ -4,13 +4,9 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
create_bank_account,
create_gl_account,
)
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
make_payment_order,
@@ -18,32 +14,28 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(FrappeTestCase):
class TestPaymentOrder(unittest.TestCase):
def setUp(self):
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
self.bank_account = create_bank_account(
gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
)
create_bank_account()
def tearDown(self):
frappe.db.rollback()
for bt in frappe.get_all("Payment Order"):
doc = frappe.get_doc("Payment Order", bt.name)
doc.cancel()
doc.delete()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry(
"Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
"Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
)
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = self.bank_account
payment_entry.party_bank_account = "Checking Account - Citi Bank"
payment_entry.insert()
payment_entry.submit()
doc = create_payment_order_against_payment_entry(
payment_entry, "Payment Entry", self.bank_account
)
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
reference_doc = doc.get("references")[0]
self.assertEqual(reference_doc.reference_name, payment_entry.name)
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
@@ -51,12 +43,14 @@ class TestPaymentOrder(FrappeTestCase):
self.assertEqual(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account=bank_account,
dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account="Checking Account - Citi Bank",
)
)
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()

View File

@@ -41,7 +41,6 @@
{
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Company",
"options": "Company",
"reqd": 1
@@ -230,7 +229,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-18 11:56:20.234667",
"modified": "2023-12-14 13:38:16.264013",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",

View File

@@ -25,10 +25,6 @@ frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){
})
frappe.ui.form.on("Payment Request", "refresh", function(frm) {
if(frm.doc.status == 'Failed'){
frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red");
}
if(frm.doc.payment_request_type == 'Inward' && frm.doc.payment_channel !== "Phone" &&
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
frm.add_custom_button(__('Resend Payment Email'), function(){

View File

@@ -7,7 +7,6 @@
"field_order": [
"payment_request_type",
"transaction_date",
"failed_reason",
"column_break_2",
"naming_series",
"mode_of_payment",
@@ -390,22 +389,13 @@
"options": "Payment Request",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "failed_reason",
"fieldtype": "Data",
"hidden": 1,
"label": "Reason for Failure",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-01-20 00:37:06.988919",
"modified": "2023-09-27 09:51:42.277638",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -443,4 +433,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -170,8 +170,8 @@ class PaymentRequest(Document):
self.request_phone_payment()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
@@ -216,8 +216,8 @@ class PaymentRequest(Document):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()

View File

@@ -16,9 +16,6 @@ frappe.listview_settings['Payment Request'] = {
else if(doc.status == "Paid") {
return [__("Paid"), "blue", "status,=,Paid"];
}
else if(doc.status == "Failed") {
return [__("Failed"), "red", "status,=,Failed"];
}
else if(doc.status == "Cancelled") {
return [__("Cancelled"), "red", "status,=,Cancelled"];
}

View File

@@ -11,6 +11,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
SalesInvoice,
get_bank_cash_account,
get_mode_of_payment_info,
update_multi_mode_option,
)
@@ -207,6 +208,7 @@ class POSInvoice(SalesInvoice):
self.validate_stock_availablility()
self.validate_return_items_qty()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
@@ -641,6 +643,11 @@ class POSInvoice(SalesInvoice):
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
def set_account_for_mode_of_payment(self):
for pay in self.payments:
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@frappe.whitelist()
def create_payment_request(self):
for pay in self.payments:

View File

@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
inv.save()
self.assertEqual(inv.net_total, 4298.24)
self.assertEqual(inv.net_total, 4298.25)
self.assertEqual(inv.grand_total, 4900.00)
def test_tax_calculation_with_multiple_items(self):

View File

@@ -351,7 +351,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
inv.load_from_db()
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.status, "Return")
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
finally:
frappe.set_user("Administrator")

View File

@@ -120,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -132,8 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
if doc.ignore_exchange_rate_revaluation_journals:
filters.update({"ignore_err": True})
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))

View File

@@ -1253,7 +1253,6 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
@@ -1613,7 +1612,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-01-26 10:46:00.469053",
"modified": "2023-11-29 15:35:44.697496",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -1995,21 +1995,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def test_debit_note_with_account_mismatch(self):
new_creditors = create_account(
parent_account="Accounts Payable - _TC",
account_name="Creditors 2",
company="_Test Company",
account_type="Payable",
)
pi = make_purchase_invoice(qty=1, rate=1000)
dr_note = make_purchase_invoice(
qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True
)
dr_note.credit_to = new_creditors
self.assertRaises(frappe.ValidationError, dr_note.save)
def test_debit_note_without_item(self):
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
pi.items[0].item_code = ""

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -77,7 +77,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2024-01-30 13:08:09.537242",
"modified": "2022-05-16 16:15:29.059370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",

View File

@@ -421,8 +421,7 @@ class SalesInvoice(SellingController):
self.calculate_taxes_and_totals()
def before_save(self):
self.set_account_for_mode_of_payment()
self.set_paid_amount()
set_account_for_mode_of_payment(self)
def on_submit(self):
self.validate_pos_paid_amount()
@@ -713,6 +712,9 @@ class SalesInvoice(SellingController):
):
data.sales_invoice = sales_invoice
def on_update(self):
self.set_paid_amount()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
@@ -743,11 +745,6 @@ class SalesInvoice(SellingController):
self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount
def set_account_for_mode_of_payment(self):
for payment in self.payments:
if not payment.account:
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
for data in self.timesheets:
if data.time_sheet:
@@ -2116,6 +2113,12 @@ def make_sales_return(source_name, target_doc=None):
return make_return_doc("Sales Invoice", source_name, target_doc)
def set_account_for_mode_of_payment(self):
for data in self.payments:
if not data.account:
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
parties = frappe.db.get_all(

View File

@@ -323,8 +323,7 @@ class TestSalesInvoice(FrappeTestCase):
si.insert()
# with inclusive tax
self.assertEqual(si.items[0].net_amount, 3947.37)
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
self.assertEqual(si.net_total, 3947.37)
self.assertEqual(si.grand_total, 5000)
@@ -668,7 +667,7 @@ class TestSalesInvoice(FrappeTestCase):
62.5,
625.0,
50,
499.98,
499.97600115194473,
],
"_Test Item Home Desktop 200": [
190.66,
@@ -679,7 +678,7 @@ class TestSalesInvoice(FrappeTestCase):
190.66,
953.3,
150,
750,
749.9968530500239,
],
}
@@ -692,21 +691,20 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(d.get(k), expected_values[d.item_code][i])
# check net total
self.assertEqual(si.base_net_total, si.net_total)
self.assertEqual(si.net_total, 1249.98)
self.assertEqual(si.net_total, 1249.97)
self.assertEqual(si.total, 1578.3)
# check tax calculation
expected_values = {
"keys": ["tax_amount", "total"],
"_Test Account Excise Duty - _TC": [140, 1389.98],
"_Test Account Education Cess - _TC": [2.8, 1392.78],
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
"_Test Account CST - _TC": [27.88, 1422.06],
"_Test Account VAT - _TC": [156.25, 1578.31],
"_Test Account Customs Duty - _TC": [125, 1703.31],
"_Test Account Shipping Charges - _TC": [100, 1803.31],
"_Test Account Discount - _TC": [-180.33, 1622.98],
"_Test Account Excise Duty - _TC": [140, 1389.97],
"_Test Account Education Cess - _TC": [2.8, 1392.77],
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
"_Test Account CST - _TC": [27.88, 1422.05],
"_Test Account VAT - _TC": [156.25, 1578.30],
"_Test Account Customs Duty - _TC": [125, 1703.30],
"_Test Account Shipping Charges - _TC": [100, 1803.30],
"_Test Account Discount - _TC": [-180.33, 1622.97],
}
for d in si.get("taxes"):
@@ -742,7 +740,7 @@ class TestSalesInvoice(FrappeTestCase):
"base_rate": 2500,
"base_amount": 25000,
"net_rate": 40,
"net_amount": 399.98,
"net_amount": 399.9808009215558,
"base_net_rate": 2000,
"base_net_amount": 19999,
},
@@ -756,7 +754,7 @@ class TestSalesInvoice(FrappeTestCase):
"base_rate": 7500,
"base_amount": 37500,
"net_rate": 118.01,
"net_amount": 590.05,
"net_amount": 590.0531205155963,
"base_net_rate": 5900.5,
"base_net_amount": 29502.5,
},
@@ -794,13 +792,8 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.base_grand_total, 60795)
self.assertEqual(si.grand_total, 1215.90)
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
self.assertEqual(si.rounding_adjustment, 0.10)
self.assertEqual(si.base_rounding_adjustment, 5.0)
else:
self.assertEqual(si.rounding_adjustment, 0.0)
self.assertEqual(si.base_rounding_adjustment, 0.0)
self.assertEqual(si.rounding_adjustment, 0.01)
self.assertEqual(si.base_rounding_adjustment, 0.50)
def test_outstanding(self):
w = self.make()
@@ -1550,19 +1543,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
account_name="Debtors 2",
company="_Test Company",
account_type="Receivable",
)
si = create_sales_invoice(qty=1, rate=1000)
cr_note = create_sales_invoice(
qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True
)
self.assertRaises(frappe.ValidationError, cr_note.save)
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
asset = create_asset(item_code="Macbook Pro")
@@ -2102,7 +2082,7 @@ class TestSalesInvoice(FrappeTestCase):
def test_rounding_adjustment_2(self):
si = create_sales_invoice(rate=400, do_not_save=True)
for rate in [400.25, 600.30, 100.65]:
for rate in [400, 600, 100]:
si.append(
"items",
{
@@ -2128,18 +2108,17 @@ class TestSalesInvoice(FrappeTestCase):
)
si.save()
si.submit()
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.net_total, 1272.20)
self.assertEqual(si.grand_total, 1501.20)
self.assertEqual(si.total_taxes_and_charges, 229)
self.assertEqual(si.rounding_adjustment, -0.20)
self.assertEqual(si.net_total, 1271.19)
self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
expected_values = [
["_Test Account Service Tax - _TC", 0.0, 114.50],
["_Test Account VAT - _TC", 0.0, 114.50],
[si.debit_to, 1501, 0.0],
["Round Off - _TC", 0.20, 0.0],
["Sales - _TC", 0.0, 1272.20],
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
[si.debit_to, 1500, 0.0],
["Round Off - _TC", 0.01, 0.01],
["Sales - _TC", 0.0, 1271.18],
]
gl_entries = frappe.db.sql(
@@ -2197,8 +2176,7 @@ class TestSalesInvoice(FrappeTestCase):
si.save()
si.submit()
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.net_total, 4007.15)
self.assertEqual(si.net_total, 4007.16)
self.assertEqual(si.grand_total, 4488.02)
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
@@ -2210,7 +2188,7 @@ class TestSalesInvoice(FrappeTestCase):
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0.0],
["Round Off - _TC", 0.02, 0.01],
]
)

View File

@@ -8,7 +8,6 @@
"default",
"mode_of_payment",
"amount",
"reference_no",
"column_break_3",
"account",
"type",
@@ -76,16 +75,11 @@
"hidden": 1,
"label": "Default",
"read_only": 1
},
{
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
}
],
"istable": 1,
"links": [],
"modified": "2024-01-23 16:20:06.436979",
"modified": "2020-08-03 12:45:39.986598",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
@@ -93,6 +87,5 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"sort_order": "DESC"
}

View File

@@ -23,7 +23,6 @@ class SalesInvoicePayment(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
reference_no: DF.Data | None
type: DF.ReadOnly | None
# end: auto-generated types

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.",
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -79,7 +79,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2024-01-30 13:07:28.801104",
"modified": "2022-05-16 16:14:52.061672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",

View File

@@ -11,6 +11,6 @@ def get_data():
},
"transactions": [
{"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
{"label": _("References"), "items": ["POS Profile", "Subscription", "Tax Rule"]},
{"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]},
],
}

View File

@@ -13,13 +13,9 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
def make_gl_entries(
@@ -359,7 +355,6 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
process_debit_credit_difference(gl_map)
dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
@@ -367,7 +362,6 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map:
validate_allowed_dimensions(entry, dimension_filter_map)
make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -706,39 +700,3 @@ def set_as_cancel(voucher_type, voucher_no):
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no),
)
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if gl_entry.account == account:
if value["is_mandatory"] and not gl_entry.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(gl_entry.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(gl_entry.account),
),
InvalidAccountDimensionError,
)
else:
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(gl_entry.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(gl_entry.account),
),
InvalidAccountDimensionError,
)

View File

@@ -1,3 +0,0 @@
<h3>{{ _("Fiscal Year") }}</h3>
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p>

View File

@@ -11,21 +11,19 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message_type": "HTML",
"modified": "2023-11-17 08:54:51.532104",
"message": "<h3>{{_(\"Fiscal Year\")}}</h3>\n\n<p>{{ _(\"New fiscal year created :- \") }} {{ doc.name }}</p>",
"modified": "2018-04-25 14:30:38.588534",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Notification for new fiscal year",
"owner": "Administrator",
"recipients": [
{
"receiver_by_role": "Accounts User"
"email_by_role": "Accounts User"
},
{
"receiver_by_role": "Accounts Manager"
"email_by_role": "Accounts Manager"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "Notification for new fiscal year {{ doc.name }}"
}
}

View File

@@ -0,0 +1,3 @@
<h3>{{_("Fiscal Year")}}</h3>
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p>

View File

@@ -5,7 +5,7 @@
from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -576,8 +576,6 @@ class ReceivablePayableReport(object):
def get_future_payments_from_payment_entry(self):
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
return (
frappe.qb.from_(pe)
.inner_join(pe_ref)
@@ -589,11 +587,6 @@ class ReceivablePayableReport(object):
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
ifelse(
pe.payment_type == "Receive",
pe.source_exchange_rate * pe_ref.allocated_amount,
pe.target_exchange_rate * pe_ref.allocated_amount,
).as_("future_amount_in_base_currency"),
)
.where(
(pe.docstatus < 2)
@@ -630,24 +623,13 @@ class ReceivablePayableReport(object):
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
"future_amount_in_base_currency"
)
)
query = query.select(
Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
).as_("future_amount")
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
)
query = query.having(qb.Field("future_amount") > 0)
@@ -663,19 +645,14 @@ class ReceivablePayableReport(object):
row.remaining_balance = row.outstanding
row.future_amount = 0.0
for future in self.future_payments.get((row.voucher_no, row.party), []):
if self.filters.in_party_currency:
future_amount_field = "future_amount"
else:
future_amount_field = "future_amount_in_base_currency"
if row.remaining_balance > 0 and future.get(future_amount_field):
if future.get(future_amount_field) > row.outstanding:
if row.remaining_balance > 0 and future.future_amount:
if future.future_amount > row.outstanding:
row.future_amount = row.outstanding
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
future.future_amount = future.future_amount - row.outstanding
row.remaining_balance = 0
else:
row.future_amount += future.get(future_amount_field)
future[future_amount_field] = 0
row.future_amount += future.future_amount
future.future_amount = 0
row.remaining_balance = row.outstanding - row.future_amount
row.setdefault("future_ref", []).append(

View File

@@ -772,92 +772,3 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
report_output = sorted(report_output, key=lambda x: x[0])
self.assertEqual(expected_data, report_output)
def test_future_payments_on_foreign_currency(self):
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
si = self.create_sales_invoice(do_not_submit=True)
si.posting_date = add_days(today(), -1)
si.customer = self.customer2
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si.save().submit()
# full payment in USD
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.base_received_amount = 7500
pe.received_amount = 7500
pe.source_exchange_rate = 75
pe.save().submit()
filters = frappe._dict(
{
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_future_payments": True,
"in_party_currency": False,
}
)
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, 500.0, 7500.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
filters.in_party_currency = True
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 0.0, 100.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
pe.cancel()
# partial payment in USD on a future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.base_received_amount = 6750
pe.received_amount = 6750
pe.source_exchange_rate = 75
pe.paid_amount = 90 # in USD
pe.references[0].allocated_amount = 90
pe.save().submit()
filters.in_party_currency = False
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
filters.in_party_currency = True
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 10.0, 90.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)

View File

@@ -8,20 +8,6 @@ frappe.query_reports["Balance Sheet"] = $.extend(
erpnext.utils.add_dimensions("Balance Sheet", 10);
frappe.query_reports["Balance Sheet"]["filters"].push(
{
"fieldname": "selected_view",
"label": __("Select View"),
"fieldtype": "Select",
"options": [
{ "value": "Report", "label": __("Report View") },
{ "value": "Growth", "label": __("Growth View") }
],
"default": "Report",
"reqd": 1
},
);
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),

View File

@@ -203,14 +203,8 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check"
},
{
"fieldname": "ignore_err",
"label": __("Ignore Exchange Rate Revaluation Journals"),
"fieldtype": "Check"
}
]
}

View File

@@ -241,19 +241,6 @@ def get_conditions(filters):
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_voucher_no)s")
if filters.get("ignore_err"):
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": filters.get("company"),
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today
from frappe.utils import today
from erpnext.accounts.report.general_ledger.general_ledger import execute
@@ -148,105 +148,3 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[2]["credit"], 900)
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency
account_name = "Test Debtors USD"
company = "_Test Company"
account = frappe.get_doc(
{
"account_name": account_name,
"is_group": 0,
"company": company,
"root_type": "Asset",
"report_type": "Balance Sheet",
"account_currency": "USD",
"parent_account": "Accounts Receivable - _TC",
"account_type": "Receivable",
"doctype": "Account",
}
)
account.insert(ignore_if_duplicate=True)
# create a JV to debit 1000 USD at 75 exchange rate
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = company
jv.multi_currency = 1
jv.cost_center = "_Test Cost Center - _TC"
jv.set(
"accounts",
[
{
"account": account.name,
"party_type": "Customer",
"party": "_Test Customer USD",
"debit_in_account_currency": 1000,
"credit_in_account_currency": 0,
"exchange_rate": 75,
"cost_center": "_Test Cost Center - _TC",
},
{
"account": "Cash - _TC",
"debit_in_account_currency": 0,
"credit_in_account_currency": 75000,
"cost_center": "_Test Cost Center - _TC",
},
],
)
jv.save()
jv.submit()
revaluation = frappe.new_doc("Exchange Rate Revaluation")
revaluation.posting_date = today()
revaluation.company = company
accounts = revaluation.get_accounts_data()
revaluation.extend("accounts", accounts)
row = revaluation.accounts[0]
row.new_exchange_rate = 83
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
revaluation.set_total_gain_loss()
revaluation = revaluation.save().submit()
# post journal entry for Revaluation doc
frappe.db.set_value(
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
revaluation_jv = revaluation.make_jv_for_revaluation()
revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC"
revaluation_jv.save()
revaluation_jv.submit()
# With ignore_err enabled
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"ignore_err": True,
}
)
)
self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data]))
# Without ignore_err enabled
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"ignore_err": False,
}
)
)
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))

View File

@@ -8,21 +8,6 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend(
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
"fieldname": "selected_view",
"label": __("Select View"),
"fieldtype": "Select",
"options": [
{ "value": "Report", "label": __("Report View") },
{ "value": "Growth", "label": __("Growth View") },
{ "value": "Margin", "label": __("Margin View") },
],
"default": "Report",
"reqd": 1
},
);
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),

View File

@@ -354,6 +354,9 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if filters.get("to_date"):
query = query.where(gle.posting_date <= filters.get("to_date"))
if bank_accounts:
query = query.where(gle.against.notin(bank_accounts))
if filters.get("party"):
party = [filters.get("party")]
jv_condition = gle.against.isin(party) | (
@@ -365,14 +368,7 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
if bank_accounts:
query = query.where(
gle.against.notin(bank_accounts) & (gle.account.isin(tds_accounts) & jv_condition)
| gle.party.isin(party)
)
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
return query

View File

@@ -5,8 +5,9 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_tax_withholding_category,
@@ -16,7 +17,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.clear_old_entries()
@@ -26,15 +27,11 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
def test_tax_withholding_for_customers(self):
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
]
@@ -44,15 +41,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.rate,
voucher.base_total,
voucher.tax_amount,
voucher.grand_total,
)
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
self.assertEqual(voucher.section_code, voucher_expected_values[1])
self.assertEqual(voucher.rate, voucher_expected_values[2])
self.assertEqual(voucher.base_total, voucher_expected_values[3])
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
def tearDown(self):
self.clear_old_entries()
@@ -115,32 +109,3 @@ def create_tcs_payment_entry():
)
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
)
jv.insert()
return jv.submit()

View File

@@ -78,14 +78,8 @@ frappe.query_reports["Trial Balance"] = {
"options": erpnext.get_presentation_currency_list()
},
{
"fieldname": "with_period_closing_entry_for_opening",
"label": __("With Period Closing Entry For Opening Balances"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "with_period_closing_entry_for_current_period",
"label": __("Period Closing Entry For Current Period"),
"fieldname": "with_period_closing_entry",
"label": __("Period Closing Entry"),
"fieldtype": "Check",
"default": 1
},

View File

@@ -116,7 +116,7 @@ def get_data(filters):
max_rgt,
filters,
gl_entries_by_account,
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
ignore_closing_entries=not flt(filters.with_period_closing_entry),
ignore_opening_entries=True,
)
@@ -249,7 +249,7 @@ def get_opening_balance(
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
if not flt(filters.with_period_closing_entry_for_opening):
if not flt(filters.with_period_closing_entry):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:

View File

@@ -237,7 +237,7 @@ def get_balance_on(
)
else:
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center),))
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
if account:
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
@@ -258,7 +258,7 @@ def get_balance_on(
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
in_account_currency = False
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
if account_type:
accounts = frappe.db.get_all(
@@ -278,11 +278,11 @@ def get_balance_on(
if party_type and party:
cond.append(
"""gle.party_type = %s and gle.party = %s """
% (frappe.db.escape(party_type), frappe.db.escape(party))
% (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
)
if company:
cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
if account or (party_type and party) or account_type:
precision = get_currency_precision()
@@ -348,7 +348,7 @@ def get_count_on(account, fieldname, date):
% (acc.lft, acc.rgt)
)
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
entries = frappe.db.sql(
"""
@@ -622,8 +622,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# Update Advance Paid in SO/PO since they might be getting unlinked
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if jv_detail.get("reference_type") in advance_payment_doctypes:
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
@@ -699,8 +699,8 @@ def update_reference_in_payment_entry(
# Update Advance Paid in SO/PO since they are getting unlinked
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if existing_row.get("reference_doctype") in advance_payment_doctypes:
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name

View File

@@ -571,16 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red'
});
}
frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
var asset_quantity = is_grouped_asset ? item.qty : 1;
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
});
},
set_depreciation_rate: function(frm, row) {

View File

@@ -1,6 +1,7 @@
{
"actions": [],
"creation": "2013-06-25 11:04:03",
"description": "Settings for Buying Module",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
@@ -151,7 +152,6 @@
},
{
"default": "1",
"depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
@@ -214,7 +214,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-31 13:34:18.101256",
"modified": "2024-01-12 16:42:01.894346",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -457,7 +457,6 @@ class PurchaseOrder(BuyingController):
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.update_blanket_order()
self.notify_update()
clear_doctype_notifications(self)
@@ -645,7 +644,6 @@ class PurchaseOrder(BuyingController):
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""

View File

@@ -822,30 +822,6 @@ class TestPurchaseOrder(FrappeTestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
def test_blanket_order_on_po_close_and_open(self):
# Step - 1: Create Blanket Order
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
# Step - 2: Create Purchase Order
po = create_purchase_order(
item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
)
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
# Step - 3: Close Purchase Order
po.update_status("Closed")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 0)
# Step - 4: Re-Open Purchase Order
po.update_status("Re-open")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1172,7 +1148,6 @@ def create_purchase_order(**args):
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
"against_blanket_order": args.against_blanket_order,
"against_blanket": args.against_blanket,
"material_request": args.material_request,
"material_request_item": args.material_request_item,
},

View File

@@ -545,6 +545,7 @@
"fieldname": "blanket_order",
"fieldtype": "Link",
"label": "Blanket Order",
"no_copy": 1,
"options": "Blanket Order"
},
{
@@ -552,6 +553,7 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -915,7 +917,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-05 11:23:24.859435",
"modified": "2023-11-24 13:24:41.298416",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -202,7 +202,6 @@ class AccountsController(TransactionBase):
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
self.validate_return_against_account()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
if invalid_advances := [
@@ -351,20 +350,6 @@ class AccountsController(TransactionBase):
for bundle in bundles:
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
def validate_return_against_account(self):
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against
):
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
cr_dr_account = self.get(cr_dr_account_field)
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
frappe.throw(
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
)
)
def validate_deferred_income_expense_account(self):
field_map = {
"Sales Invoice": "deferred_revenue_account",
@@ -693,7 +678,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
@@ -1808,9 +1793,9 @@ class AccountsController(TransactionBase):
def set_total_advance_paid(self):
ple = frappe.qb.DocType("Payment Ledger Entry")
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
party = self.customer
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
party = self.supplier
advance = (
frappe.qb.from_(ple)
@@ -1876,9 +1861,9 @@ class AccountsController(TransactionBase):
"docstatus": 1,
},
)
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
new_status = "Requested" if prs else "Not Requested"
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
new_status = "Initiated" if prs else "Not Initiated"
if new_status == self.advance_payment_status:

View File

@@ -729,24 +729,17 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters)
warehouse_field = "name"
meta = frappe.get_meta("Warehouse")
if meta.get("show_title_field_in_link") and meta.get("title_field"):
searchfield = meta.get("title_field")
warehouse_field = meta.get("title_field")
query = """select `tabWarehouse`.`{warehouse_field}`,
query = """select `tabWarehouse`.name,
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where
`tabWarehouse`.`{key}` like {txt}
{fcond} {mcond}
order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
order by ifnull(`tabBin`.actual_qty, 0) desc
limit
{page_len} offset {start}
""".format(
warehouse_field=warehouse_field,
bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
),

View File

@@ -599,7 +599,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
)
def set_customer_address(self):

View File

@@ -99,8 +99,7 @@ status_map = {
],
"Purchase Receipt": [
["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
from typing import List, Tuple
import frappe
from frappe import _, bold
from frappe import _
from frappe.utils import cint, flt, get_link_to_form, getdate
import erpnext
@@ -697,9 +697,6 @@ class StockController(AccountsController):
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
if self.get("is_internal_supplier"):
self.validate_internal_transfer_qty()
else:
self.validate_internal_transfer_warehouse()
@@ -738,116 +735,6 @@ class StockController(AccountsController):
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
frappe.throw(_("Packed Items cannot be transferred internally"))
def validate_internal_transfer_qty(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
if not item_wise_transfer_qty:
return
item_wise_received_qty = self.get_item_wise_inter_received_qty()
precision = frappe.get_precision(self.doctype + " Item", "qty")
over_receipt_allowance = frappe.db.get_single_value(
"Stock Settings", "over_delivery_receipt_allowance"
)
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
}.get(self.doctype)
for key, transferred_qty in item_wise_transfer_qty.items():
recevied_qty = flt(item_wise_received_qty.get(key), precision)
if over_receipt_allowance:
transferred_qty = transferred_qty + flt(
transferred_qty * over_receipt_allowance / 100, precision
)
if recevied_qty > flt(transferred_qty, precision):
frappe.throw(
_("For Item {0} cannot be received more than {1} qty against the {2} {3}").format(
bold(key[1]),
bold(flt(transferred_qty, precision)),
bold(parent_doctype),
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
)
)
def get_item_wise_inter_transfer_qty(self):
reference_field = "inter_company_reference"
if self.doctype == "Purchase Invoice":
reference_field = "inter_company_invoice_reference"
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
}.get(self.doctype)
child_doctype = parent_doctype + " Item"
parent_tab = frappe.qb.DocType(parent_doctype)
child_tab = frappe.qb.DocType(child_doctype)
query = (
frappe.qb.from_(parent_doctype)
.inner_join(child_tab)
.on(child_tab.parent == parent_tab.name)
.select(
child_tab.name,
child_tab.item_code,
child_tab.qty,
)
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
)
data = query.run(as_dict=True)
item_wise_transfer_qty = defaultdict(float)
for row in data:
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
return item_wise_transfer_qty
def get_item_wise_inter_received_qty(self):
child_doctype = self.doctype + " Item"
parent_tab = frappe.qb.DocType(self.doctype)
child_tab = frappe.qb.DocType(child_doctype)
query = (
frappe.qb.from_(self.doctype)
.inner_join(child_tab)
.on(child_tab.parent == parent_tab.name)
.select(
child_tab.item_code,
child_tab.qty,
)
.where(parent_tab.docstatus < 2)
)
if self.doctype == "Purchase Invoice":
query = query.select(
child_tab.sales_invoice_item.as_("name"),
)
query = query.where(
parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference
)
else:
query = query.select(
child_tab.delivery_note_item.as_("name"),
)
query = query.where(parent_tab.inter_company_reference == self.inter_company_reference)
data = query.run(as_dict=True)
item_wise_transfer_qty = defaultdict(float)
for row in data:
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
return item_wise_transfer_qty
def validate_putaway_capacity(self):
# if over receipt is attempted while 'apply putaway rule' is disabled
# and if rule was applied on the transaction, validate it.

View File

@@ -260,22 +260,18 @@ class SubcontractingController(StockController):
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, receipt_items):
fields = [
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
"parent as voucher_no",
]
if self.subcontract_data.receipt_supplied_items_field != "Purchase Receipt Item Supplied":
fields.append("serial_and_batch_bundle")
return frappe.get_all(
self.subcontract_data.receipt_supplied_items_field,
fields=fields,
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"serial_and_batch_bundle",
"batch_no",
"consumed_qty",
"main_item_code",
"parent as voucher_no",
],
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
)
@@ -885,9 +881,7 @@ class SubcontractingController(StockController):
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"voucher_detail_no": item.name,
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"serial_no": item.get("serial_no"),
"batch_no": item.get("batch_no"),
"serial_and_batch_bundle": item.serial_and_batch_bundle,
}
)

View File

@@ -8,7 +8,6 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
@@ -75,7 +74,7 @@ class calculate_taxes_and_totals(object):
self.calculate_net_total()
self.calculate_tax_withholding_net_total()
self.calculate_taxes()
self.adjust_grand_total_for_inclusive_tax()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
self._cleanup()
self.calculate_total_net_weight()
@@ -98,7 +97,6 @@ class calculate_taxes_and_totals(object):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
"net_rate": item.net_rate or item.rate,
"base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
@@ -281,7 +279,7 @@ class calculate_taxes_and_totals(object):
):
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount"))
item.net_amount = flt(amount / (1 + cumulated_tax_fraction))
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage"))
@@ -518,12 +516,7 @@ class calculate_taxes_and_totals(object):
tax.base_tax_amount = round(tax.base_tax_amount, 0)
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
@deprecated
def manipulate_grand_total_for_inclusive_tax(self):
# for backward compatablility - if in case used by an external application
return self.adjust_grand_total_for_inclusive_tax()
def adjust_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
last_tax = self.doc.get("taxes")[-1]
@@ -545,21 +538,17 @@ class calculate_taxes_and_totals(object):
diff = flt(diff, self.doc.precision("rounding_adjustment"))
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.doc.grand_total_diff = diff
else:
self.doc.grand_total_diff = 0
self.doc.rounding_adjustment = diff
def calculate_totals(self):
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
self.doc.get("grand_total_diff")
)
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
else:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
self.doc.precision("total_taxes_and_charges"),
)
else:
@@ -624,8 +613,8 @@ class calculate_taxes_and_totals(object):
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
# rounding adjustment should always be the difference vetween grand and rounded total
self.doc.rounding_adjustment = flt(
# if print_in_rate is set, we would have already calculated rounding adjustment
self.doc.rounding_adjustment += flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
@@ -843,6 +832,7 @@ class calculate_taxes_and_totals(object):
self.calculate_paid_amount()
def calculate_paid_amount(self):
paid_amount = base_paid_amount = 0.0
if self.doc.is_pos:

View File

@@ -481,8 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"]
advance_payment_receivable_doctypes = ["Sales Order"]
advance_payment_payable_doctypes = ["Purchase Order"]
advance_payment_customer_doctypes = ["Sales Order"]
advance_payment_supplier_doctypes = ["Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]

File diff suppressed because it is too large Load Diff

View File

@@ -176,10 +176,8 @@ class BOM(WebsiteGenerator):
def autoname(self):
# ignore amended documents while calculating current index
search_key = f"{self.doctype}-{self.item}%"
existing_boms = frappe.get_all(
"BOM", filters={"name": ("like", search_key), "amended_from": ["is", "not set"]}, pluck="name"
"BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name"
)
if existing_boms:

View File

@@ -955,14 +955,6 @@ class JobCard(Document):
if update_status:
self.db_set("status", self.status)
if self.status in ["Completed", "Work In Progress"]:
status = {
"Completed": "Off",
"Work In Progress": "Production",
}.get(self.status)
self.update_status_in_workstation(status)
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
@@ -1043,12 +1035,6 @@ class JobCard(Document):
return False
def update_status_in_workstation(self, status):
if not self.workstation:
return
frappe.db.set_value("Workstation", self.workstation, "status", status)
@frappe.whitelist()
def make_time_log(args):

View File

@@ -1,256 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Plant Floor", {
setup(frm) {
frm.trigger("setup_queries");
},
setup_queries(frm) {
frm.set_query("warehouse", (doc) => {
if (!doc.company) {
frappe.throw(__("Please select Company first"));
}
return {
filters: {
"is_group": 0,
"company": doc.company
}
}
});
},
refresh(frm) {
frm.trigger('prepare_stock_dashboard')
frm.trigger('prepare_workstation_dashboard')
},
prepare_workstation_dashboard(frm) {
let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper);
wrapper.empty();
frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({
wrapper: wrapper,
skip_filters: true,
plant_floor: frm.doc.name,
});
},
prepare_stock_dashboard(frm) {
if (!frm.doc.warehouse) {
return;
}
let wrapper = $(frm.fields_dict["stock_summary"].wrapper);
wrapper.empty();
frappe.visual_stock = new VisualStock({
wrapper: wrapper,
frm: frm,
});
},
});
class VisualStock {
constructor(opts) {
Object.assign(this, opts);
this.make();
}
make() {
this.prepare_filters();
this.prepare_stock_summary({
start:0
});
}
prepare_filters() {
this.wrapper.append(`
<div class="row">
<div class="col-sm-12 filter-section section-body">
</div>
</div>
`);
this.item_filter = frappe.ui.form.make_control({
df: {
fieldtype: "Link",
fieldname: "item_code",
placeholder: __("Item"),
options: "Item",
onchange: () => this.prepare_stock_summary({
start:0,
item_code: this.item_filter.value
})
},
parent: this.wrapper.find('.filter-section'),
render_input: true,
});
this.item_filter.$wrapper.addClass('form-column col-sm-3');
this.item_filter.$wrapper.find('.clearfix').hide();
this.item_group_filter = frappe.ui.form.make_control({
df: {
fieldtype: "Link",
fieldname: "item_group",
placeholder: __("Item Group"),
options: "Item Group",
change: () => this.prepare_stock_summary({
start:0,
item_group: this.item_group_filter.value
})
},
parent: this.wrapper.find('.filter-section'),
render_input: true,
});
this.item_group_filter.$wrapper.addClass('form-column col-sm-3');
this.item_group_filter.$wrapper.find('.clearfix').hide();
}
prepare_stock_summary(args) {
let {start, item_code, item_group} = args;
this.get_stock_summary(start, item_code, item_group).then(stock_summary => {
this.wrapper.find('.stock-summary-container').remove();
this.wrapper.append(`<div class="col-sm-12 stock-summary-container" style="margin-bottom:20px"></div>`);
this.stock_summary = stock_summary.message;
this.render_stock_summary();
this.bind_events();
});
}
async get_stock_summary(start, item_code, item_group) {
let stock_summary = await frappe.call({
method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary",
args: {
warehouse: this.frm.doc.warehouse,
start: start,
item_code: item_code,
item_group: item_group
}
});
return stock_summary;
}
render_stock_summary() {
let template = frappe.render_template("stock_summary_template", {
stock_summary: this.stock_summary
});
this.wrapper.find('.stock-summary-container').append(template);
}
bind_events() {
this.wrapper.find('.btn-add').click((e) => {
this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
this.make_stock_entry([
{
label: __("For Item"),
fieldname: "item_code",
fieldtype: "Data",
read_only: 1,
default: this.item_code
},
{
label: __("Quantity"),
fieldname: "qty",
fieldtype: "Float",
reqd: 1
}
], __("Add Stock"), "Material Receipt")
});
this.wrapper.find('.btn-move').click((e) => {
this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
this.make_stock_entry([
{
label: __("For Item"),
fieldname: "item_code",
fieldtype: "Data",
read_only: 1,
default: this.item_code
},
{
label: __("Quantity"),
fieldname: "qty",
fieldtype: "Float",
reqd: 1
},
{
label: __("To Warehouse"),
fieldname: "to_warehouse",
fieldtype: "Link",
options: "Warehouse",
reqd: 1,
get_query: () => {
return {
filters: {
"is_group": 0,
"company": this.frm.doc.company
}
}
}
}
], __("Move Stock"), "Material Transfer")
});
}
make_stock_entry(fields, title, stock_entry_type) {
frappe.prompt(fields,
(values) => {
this.values = values;
this.stock_entry_type = stock_entry_type;
this.update_values();
this.frm.call({
method: "make_stock_entry",
doc: this.frm.doc,
args: {
kwargs: this.values,
},
callback: (r) => {
if (!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
})
}, __(title), __("Create")
);
}
update_values() {
if (!this.values.qty) {
frappe.throw(__("Quantity is required"));
}
let from_warehouse = "";
let to_warehouse = "";
if (this.stock_entry_type == "Material Receipt") {
to_warehouse = this.frm.doc.warehouse;
} else {
from_warehouse = this.frm.doc.warehouse;
to_warehouse = this.values.to_warehouse;
}
this.values = {
...this.values,
...{
"company": this.frm.doc.company,
"item_code": this.item_code,
"from_warehouse": from_warehouse,
"to_warehouse": to_warehouse,
"purpose": this.stock_entry_type,
}
}
}
}

View File

@@ -1,97 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:floor_name",
"creation": "2023-10-06 15:06:07.976066",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"workstations_tab",
"plant_dashboard",
"stock_summary_tab",
"stock_summary",
"details_tab",
"column_break_mvbx",
"floor_name",
"company",
"warehouse"
],
"fields": [
{
"fieldname": "floor_name",
"fieldtype": "Data",
"label": "Floor Name",
"unique": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "workstations_tab",
"fieldtype": "Tab Break",
"label": "Workstations"
},
{
"fieldname": "plant_dashboard",
"fieldtype": "HTML",
"label": "Plant Dashboard"
},
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Floor"
},
{
"fieldname": "column_break_mvbx",
"fieldtype": "Column Break"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.__islocal && doc.warehouse",
"fieldname": "stock_summary_tab",
"fieldtype": "Tab Break",
"label": "Stock Summary"
},
{
"fieldname": "stock_summary",
"fieldtype": "HTML",
"label": "Stock Summary"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-01-30 11:59:07.508535",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,129 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.query_builder import Order
from frappe.utils import get_link_to_form, nowdate, nowtime
class PlantFloor(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
company: DF.Link | None
floor_name: DF.Data | None
warehouse: DF.Link | None
# end: auto-generated types
@frappe.whitelist()
def make_stock_entry(self, kwargs):
if isinstance(kwargs, str):
kwargs = frappe.parse_json(kwargs)
if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.update(
{
"company": kwargs.company,
"from_warehouse": kwargs.from_warehouse,
"to_warehouse": kwargs.to_warehouse,
"purpose": kwargs.purpose,
"stock_entry_type": kwargs.purpose,
"posting_date": nowdate(),
"posting_time": nowtime(),
"items": self.get_item_details(kwargs),
}
)
stock_entry.set_missing_values()
return stock_entry
def get_item_details(self, kwargs) -> list[dict]:
item_details = frappe.db.get_value(
"Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True
)
item_details.update(
{
"qty": kwargs.qty,
"uom": item_details.stock_uom,
"item_code": kwargs.item_code,
"conversion_factor": 1,
"s_warehouse": kwargs.from_warehouse,
"t_warehouse": kwargs.to_warehouse,
}
)
return [item_details]
@frappe.whitelist()
def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
stock_details = get_stock_details(
warehouse, start=start, item_code=item_code, item_group=item_group
)
max_count = 0.0
for d in stock_details:
d.actual_or_pending = (
d.projected_qty
+ d.reserved_qty
+ d.reserved_qty_for_production
+ d.reserved_qty_for_sub_contract
)
d.pending_qty = 0
d.total_reserved = (
d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract
)
if d.actual_or_pending > d.actual_qty:
d.pending_qty = d.actual_or_pending - d.actual_qty
d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count)
max_count = d.max_count
d.item_link = get_link_to_form("Item", d.item_code)
return stock_details
def get_stock_details(warehouse, start=0, item_code=None, item_group=None):
item_table = frappe.qb.DocType("Item")
bin_table = frappe.qb.DocType("Bin")
query = (
frappe.qb.from_(bin_table)
.inner_join(item_table)
.on(bin_table.item_code == item_table.name)
.select(
bin_table.item_code,
bin_table.actual_qty,
bin_table.projected_qty,
bin_table.reserved_qty,
bin_table.reserved_qty_for_production,
bin_table.reserved_qty_for_sub_contract,
bin_table.reserved_qty_for_production_plan,
bin_table.reserved_stock,
item_table.item_name,
item_table.item_group,
item_table.image,
)
.where(bin_table.warehouse == warehouse)
.limit(20)
.offset(start)
.orderby(bin_table.actual_qty, order=Order.desc)
)
if item_code:
query = query.where(bin_table.item_code == item_code)
if item_group:
query = query.where(item_table.item_group == item_group)
return query.run(as_dict=True)

View File

@@ -1,61 +0,0 @@
{% $.each(stock_summary, (idx, row) => { %}
<div class="row" style="border-bottom:1px solid var(--border-color); padding:4px 5px; margin-top: 3px;margin-bottom: 3px;">
<div class="col-sm-1">
{% if(row.image) { %}
<img style="width:50px;height:50px;" src="{{row.image}}">
{% } else { %}
<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(row.item_code, 2)}}</div>
{% } %}
</div>
<div class="col-sm-3">
{% if (row.item_code === row.item_name) { %}
{{row.item_link}}
{% } else { %}
{{row.item_link}}
<p>
{{row.item_name}}
</p>
{% } %}
</div>
<div class="col-sm-1" title="{{ __('Actual Qty') }}">
{{ frappe.format(row.actual_qty, { fieldtype: "Float"})}}
</div>
<div class="col-sm-1" title="{{ __('Reserved Stock') }}">
{{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}}
</div>
<div class="col-sm-4 small">
<span class="inline-graph">
<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
<span class="inline-graph-count">{{ row.total_reserved }}</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner"
style="width: {{ cint(Math.abs(row.total_reserved)/row.max_count * 100) || 5 }}%">
</span>
</span>
</span>
<span class="inline-graph-half" title="{{ __("Actual Qty {0} / Waiting Qty {1}", [row.actual_qty, row.pending_qty]) }}">
<span class="inline-graph-count">
{{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }}
</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner dark"
style="width: {{ cint(row.actual_qty/row.max_count * 100) }}%">
</span>
{% if row.pending_qty > 0 %}
<span class="inline-graph-bar-inner" title="{{ __("Projected Qty") }}"
style="width: {{ cint(row.pending_qty/row.max_count * 100) }}%">
</span>
{% endif %}
</span>
</span>
</span>
</div>
<div class="col-sm-1">
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">Add</button>
</div>
<div class="col-sm-1">
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">Move</button>
</div>
</div>
{% }); %}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPlantFloor(FrappeTestCase):
pass

View File

@@ -1334,10 +1334,10 @@ def get_sales_orders(self):
)
date_field_mapper = {
"from_date": so.transaction_date >= self.from_date,
"to_date": so.transaction_date <= self.to_date,
"from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
"to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
"from_date": self.from_date >= so.transaction_date,
"to_date": self.to_date <= so.transaction_date,
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
}
for field, value in date_field_mapper.items():

View File

@@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
def validate_operation_data(row):
if flt(row.get("qty")) <= 0:
if row.get("qty") <= 0:
frappe.throw(
_("Quantity to Manufacture can not be zero for the operation {0}").format(
frappe.bold(row.get("operation"))
)
)
if flt(row.get("qty")) > flt(row.get("pending_qty")):
if row.get("qty") > row.get("pending_qty"):
frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
frappe.bold(row.get("operation")),

View File

@@ -2,28 +2,6 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Workstation", {
set_illustration_image(frm) {
let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image;
if (status_image_field) {
frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field);
}
},
refresh(frm) {
frm.trigger("set_illustration_image");
frm.trigger("prepapre_dashboard");
},
prepapre_dashboard(frm) {
let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper);
$parent.empty();
let workstation_dashboard = new WorkstationDashboard({
wrapper: $parent,
frm: frm
});
},
onload(frm) {
if(frm.is_new())
{
@@ -76,243 +54,3 @@ frappe.tour['Workstation'] = [
];
class WorkstationDashboard {
constructor({ wrapper, frm }) {
this.$wrapper = $(wrapper);
this.frm = frm;
this.prepapre_dashboard();
}
prepapre_dashboard() {
frappe.call({
method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards",
args: {
workstation: this.frm.doc.name
},
callback: (r) => {
if (r.message) {
this.job_cards = r.message;
this.render_job_cards();
}
}
});
}
render_job_cards() {
let template = frappe.render_template("workstation_job_card", {
data: this.job_cards
});
this.$wrapper.html(template);
this.prepare_timer();
this.toggle_job_card();
this.bind_events();
}
toggle_job_card() {
this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
$(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide")
if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide"))
$(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1"))
else
$(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1"))
});
}
bind_events() {
this.$wrapper.find(".make-material-request").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
this.make_material_request(job_card);
});
this.$wrapper.find(".btn-start").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
this.start_job(job_card);
});
this.$wrapper.find(".btn-complete").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
this.complete_job(job_card, pending_qty);
});
}
start_job(job_card) {
let me = this;
frappe.prompt([
{
fieldtype: 'Datetime',
label: __('Start Time'),
fieldname: 'start_time',
reqd: 1,
default: frappe.datetime.now_datetime()
},
{
label: __('Operator'),
fieldname: 'employee',
fieldtype: 'Link',
options: 'Employee',
}
], data => {
this.frm.call({
method: "start_job",
doc: this.frm.doc,
args: {
job_card: job_card,
from_time: data.start_time,
employee: data.employee,
},
callback(r) {
if (r.message) {
me.job_cards = [r.message];
me.prepare_timer()
me.update_job_card_details();
}
}
});
}, __("Enter Value"), __("Start Job"));
}
complete_job(job_card, qty_to_manufacture) {
let me = this;
let fields = [
{
fieldtype: 'Float',
label: __('Completed Quantity'),
fieldname: 'qty',
reqd: 1,
default: flt(qty_to_manufacture || 0)
},
{
fieldtype: 'Datetime',
label: __('End Time'),
fieldname: 'end_time',
default: frappe.datetime.now_datetime()
},
];
frappe.prompt(fields, data => {
if (data.qty <= 0) {
frappe.throw(__("Quantity should be greater than 0"));
}
this.frm.call({
method: "complete_job",
doc: this.frm.doc,
args: {
job_card: job_card,
qty: data.qty,
to_time: data.end_time,
},
callback: function(r) {
if (r.message) {
me.job_cards = [r.message];
me.prepare_timer()
me.update_job_card_details();
}
}
});
}, __("Enter Value"), __("Submit"));
}
make_material_request(job_card) {
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
args: {
source_name: job_card,
},
callback: (r) => {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
}
prepare_timer() {
this.job_cards.forEach((data) => {
if (data.time_logs?.length) {
data._current_time = this.get_current_time(data);
if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
this.updateStopwatch(data);
} else {
this.initialiseTimer(data);
}
}
});
}
update_job_card_details() {
let color_map = {
"Pending": "var(--bg-blue)",
"In Process": "var(--bg-yellow)",
"Submitted": "var(--bg-blue)",
"Open": "var(--bg-gray)",
"Closed": "var(--bg-green)",
"Work In Progress": "var(--bg-orange)",
}
this.job_cards.forEach((data) => {
let job_card_selector = this.$wrapper.find(`
[data-name='${data.name}']`
);
$(job_card_selector).find(".job-card-status").text(data.status);
$(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
if (data.status === "Work In Progress") {
$(job_card_selector).find(".btn-start").addClass("hide");
$(job_card_selector).find(".btn-complete").removeClass("hide");
} else if (data.status === "Completed") {
$(job_card_selector).find(".btn-start").addClass("hide");
$(job_card_selector).find(".btn-complete").addClass("hide");
}
});
}
initialiseTimer(data) {
setInterval(() => {
data._current_time += 1;
this.updateStopwatch(data);
}, 1000);
}
updateStopwatch(data) {
let increment = data._current_time;
let hours = Math.floor(increment / 3600);
let minutes = Math.floor((increment - (hours * 3600)) / 60);
let seconds = cint(increment - (hours * 3600) - (minutes * 60));
let job_card_selector = `[data-job-card='${data.name}']`
let timer_selector = this.$wrapper.find(job_card_selector)
$(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
$(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
$(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
}
get_current_time(data) {
let current_time = 0.0;
data.time_logs.forEach(d => {
if (d.to_time) {
if (d.time_in_mins) {
current_time += flt(d.time_in_mins, 2) * 60;
} else {
current_time += this.get_seconds_diff(d.to_time, d.from_time);
}
} else {
current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
}
});
return current_time;
}
get_seconds_diff(d1, d2) {
return moment(d1).diff(d2, "seconds");
}
}

View File

@@ -8,24 +8,10 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dashboard_tab",
"workstation_dashboard",
"details_tab",
"workstation_name",
"workstation_type",
"plant_floor",
"column_break_3",
"production_capacity",
"warehouse",
"production_capacity_section",
"parts_per_hour",
"workstation_status_tab",
"status",
"column_break_glcv",
"illustration_section",
"on_status_image",
"column_break_etmc",
"off_status_image",
"column_break_3",
"workstation_type",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
@@ -38,9 +24,7 @@
"description",
"working_hours_section",
"holiday_list",
"working_hours",
"total_working_hours",
"connections_tab"
"working_hours"
],
"fields": [
{
@@ -136,10 +120,9 @@
},
{
"default": "1",
"description": "Run parallel job cards in a workstation",
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Job Capacity",
"label": "Production Capacity",
"reqd": 1
},
{
@@ -162,97 +145,12 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"fieldname": "plant_floor",
"fieldtype": "Link",
"label": "Plant Floor",
"options": "Plant Floor"
},
{
"fieldname": "workstation_status_tab",
"fieldtype": "Tab Break",
"label": "Workstation Status"
},
{
"fieldname": "illustration_section",
"fieldtype": "Section Break",
"label": "Status Illustration"
},
{
"fieldname": "column_break_etmc",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup"
},
{
"fieldname": "column_break_glcv",
"fieldtype": "Column Break"
},
{
"fieldname": "on_status_image",
"fieldtype": "Attach Image",
"label": "Active Status"
},
{
"fieldname": "off_status_image",
"fieldtype": "Attach Image",
"label": "Inactive Status"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"fieldname": "production_capacity_section",
"fieldtype": "Section Break",
"label": "Production Capacity"
},
{
"fieldname": "parts_per_hour",
"fieldtype": "Float",
"label": "Parts Per Hour"
},
{
"fieldname": "total_working_hours",
"fieldtype": "Float",
"label": "Total Working Hours"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Job Cards"
},
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Details"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "workstation_dashboard",
"fieldtype": "HTML",
"label": "Workstation Dashboard"
}
],
"icon": "icon-wrench",
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2023-11-30 12:43:35.808845",
"modified": "2022-11-04 17:39:01.549346",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",

View File

@@ -11,11 +11,7 @@ from frappe.utils import (
comma_and,
flt,
formatdate,
get_link_to_form,
get_time,
get_url_to_form,
getdate,
time_diff_in_hours,
time_diff_in_seconds,
to_timedelta,
)
@@ -64,23 +60,6 @@ class Workstation(Document):
def before_save(self):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
self.set_total_working_hours()
def set_total_working_hours(self):
self.total_working_hours = 0.0
for row in self.working_hours:
self.validate_working_hours(row)
if row.start_time and row.end_time:
row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours"))
self.total_working_hours += row.hours
def validate_working_hours(self, row):
if not (row.start_time and row.end_time):
frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
if get_time(row.start_time) >= get_time(row.end_time):
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
def set_hour_rate(self):
self.hour_rate = (
@@ -164,141 +143,6 @@ class Workstation(Document):
return schedule_date
@frappe.whitelist()
def start_job(self, job_card, from_time, employee):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {"from_time": from_time, "employee": employee})
doc.save(ignore_permissions=True)
return doc
@frappe.whitelist()
def complete_job(self, job_card, qty, to_time):
doc = frappe.get_doc("Job Card", job_card)
for row in doc.time_logs:
if not row.to_time:
row.to_time = to_time
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
row.completed_qty = qty
doc.save(ignore_permissions=True)
doc.submit()
return doc
@frappe.whitelist()
def get_job_cards(workstation):
if frappe.has_permission("Job Card", "read"):
jc_data = frappe.get_all(
"Job Card",
fields=[
"name",
"production_item",
"work_order",
"operation",
"total_completed_qty",
"for_quantity",
"transferred_qty",
"status",
"expected_start_date",
"expected_end_date",
"time_required",
"wip_warehouse",
],
filters={
"workstation": workstation,
"docstatus": ("<", 2),
"status": ["not in", ["Completed", "Stopped"]],
},
order_by="expected_start_date, expected_end_date",
)
job_cards = [row.name for row in jc_data]
raw_materials = get_raw_materials(job_cards)
time_logs = get_time_logs(job_cards)
allow_excess_transfer = frappe.db.get_single_value(
"Manufacturing Settings", "job_card_excess_transfer"
)
for row in jc_data:
row.progress_percent = (
flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
)
row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
row.status_color = get_status_color(row.status)
row.job_card_link = get_link_to_form("Job Card", row.name)
row.work_order_link = get_link_to_form("Work Order", row.work_order)
row.raw_materials = raw_materials.get(row.name, [])
row.time_logs = time_logs.get(row.name, [])
row.make_material_request = False
if row.for_quantity > row.transferred_qty or allow_excess_transfer:
row.make_material_request = True
return jc_data
def get_status_color(status):
color_map = {
"Pending": "var(--bg-blue)",
"In Process": "var(--bg-yellow)",
"Submitted": "var(--bg-blue)",
"Open": "var(--bg-gray)",
"Closed": "var(--bg-green)",
"Work In Progress": "var(--bg-orange)",
}
return color_map.get(status, "var(--bg-blue)")
def get_raw_materials(job_cards):
raw_materials = {}
data = frappe.get_all(
"Job Card Item",
fields=[
"parent",
"item_code",
"item_group",
"uom",
"item_name",
"source_warehouse",
"required_qty",
"transferred_qty",
],
filters={"parent": ["in", job_cards]},
)
for row in data:
raw_materials.setdefault(row.parent, []).append(row)
return raw_materials
def get_time_logs(job_cards):
time_logs = {}
data = frappe.get_all(
"Job Card Time Log",
fields=[
"parent",
"name",
"employee",
"from_time",
"to_time",
"time_in_mins",
],
filters={"parent": ["in", job_cards], "parentfield": "time_logs"},
order_by="parent, idx",
)
for row in data:
time_logs.setdefault(row.parent, []).append(row)
return time_logs
@frappe.whitelist()
def get_default_holiday_list():
@@ -357,52 +201,3 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
+ "\n".join(applicable_holidays),
WorkstationHolidayError,
)
@frappe.whitelist()
def get_workstations(**kwargs):
kwargs = frappe._dict(kwargs)
_workstation = frappe.qb.DocType("Workstation")
query = (
frappe.qb.from_(_workstation)
.select(
_workstation.name,
_workstation.description,
_workstation.status,
_workstation.on_status_image,
_workstation.off_status_image,
)
.orderby(_workstation.workstation_type, _workstation.name)
.where(_workstation.plant_floor == kwargs.plant_floor)
)
if kwargs.workstation:
query = query.where(_workstation.name == kwargs.workstation)
if kwargs.workstation_type:
query = query.where(_workstation.workstation_type == kwargs.workstation_type)
if kwargs.workstation_status:
query = query.where(_workstation.status == kwargs.workstation_status)
data = query.run(as_dict=True)
color_map = {
"Production": "var(--green-600)",
"Off": "var(--gray-600)",
"Idle": "var(--gray-600)",
"Problem": "var(--red-600)",
"Maintenance": "var(--yellow-600)",
"Setup": "var(--blue-600)",
}
for d in data:
d.workstation_name = get_link_to_form("Workstation", d.name)
d.status_image = d.on_status_image
d.background_color = color_map.get(d.status, "var(--red-600)")
d.workstation_link = get_url_to_form("Workstation", d.name)
if d.status != "Production":
d.status_image = d.off_status_image
return data

View File

@@ -1,125 +0,0 @@
<style>
.job-card-link {
min-height: 100px;
}
.section-head-job-card {
margin-bottom: 0px;
padding-bottom: 0px;
}
</style>
<div style = "max-height: 400px; overflow-y: auto;">
{% $.each(data, (idx, d) => { %}
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
<div class="section-head section-head-job-card">
{{ d.operation }} - {{ d.production_item }}
<span class="ml-2 collapse-indicator-job mb-1" style="">
{{frappe.utils.icon("es-line-down", "sm", "mb-1")}}
</span>
</div>
<div class="row form-section" style="width:100%;margin-bottom:10px">
<div class="form-column col-sm-3">
<div class="frappe-control" title="{{__('Job Card')}}" style="text-decoration:underline">
{{ d.job_card_link }}
</div>
<div class="frappe-control" title="{{__('Work Order')}}" style="text-decoration:underline">
{{ d.work_order_link }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control timer" title="{{__('Timer')}}" style="text-align:center;font-size:14px;" data-job-card = {{escape(d.name)}}>
<span class="hours">00</span>
<span class="colon">:</span>
<span class="minutes">00</span>
<span class="colon">:</span>
<span class="seconds">00</span>
</div>
{% if(d.status === "Open") { %}
<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
</div>
{% } else { %}
<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
</div>
{% } %}
</div>
<div class="form-column col-sm-2">
<div class="frappe-control job-card-status" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
{{ d.status }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control" title="{{__('Qty to Manufacture')}}">
<div class="progress" title = "{{d.progress_title}}">
<div class="progress-bar progress-bar-success" style="width: {{d.progress_percent}}%">
</div>
</div>
</div>
<div class="frappe-control" style="text-align: center; font-size: 10px;">
{{ d.for_quantity }} / {{ d.total_completed_qty }}
</div>
</div>
<div class="form-column col-sm-2 text-center">
<button style="width: 85px;" class="btn btn-default btn-start {% if(d.status !== "Open") { %} hide {% } %}" job-card="{{d.name}}"> {{__("Start")}} </button>
<button style="width: 85px;" class="btn btn-default btn-complete {% if(d.status === "Open") { %} hide {% } %}" job-card="{{d.name}}" pending-qty="{{d.for_quantity - d.transferred_qty}}"> {{__("Complete")}} </button>
</div>
</div>
<div class="section-body section-body-job-card form-section hide">
<hr>
<div class="row">
<div class="form-column col-sm-2">
{{ __("Raw Materials") }}
</div>
{% if(d.make_material_request) { %}
<div class="form-column col-sm-10 text-right">
<button class="btn btn-default btn-xs make-material-request" job-card="{{d.name}}">{{ __("Material Request") }}</button>
</div>
{% } %}
</div>
{% if(d.raw_materials) { %}
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th style="width: 5%" class="table-sr">Sr</th>
<th style="width: 15%">{{ __("Item") }}</th>
<th style="width: 15%">{{ __("Warehouse") }}</th>
<th style="width: 10%">{{__("UOM")}}</th>
<th style="width: 15%">{{__("Item Group")}}</th>
<th style="width: 20%" >{{__("Required Qty")}}</th>
<th style="width: 20%" >{{__("Transferred Qty")}}</th>
</tr>
</thead>
<tbody>
{% $.each(d.raw_materials, (row_index, child_row) => { %}
<tr>
<td class="table-sr">{{ row_index+1 }}</td>
{% if(child_row.item_code === child_row.item_name) { %}
<td>{{ child_row.item_code }}</td>
{% } else { %}
<td>{{ child_row.item_code }}: {{child_row.item_name}}</td>
{% } %}
<td>{{ child_row.source_warehouse }}</td>
<td>{{ child_row.uom }}</td>
<td>{{ child_row.item_group }}</td>
<td>{{ child_row.required_qty }}</td>
<td>{{ child_row.transferred_qty }}</td>
</tr>
{% }); %}
</tbody>
{% } %}
</table>
</div>
</div>
{% }); %}
</div>

View File

@@ -1,16 +1,5 @@
frappe.listview_settings['Workstation'] = {
add_fields: ["status"],
get_indicator: function(doc) {
let color_map = {
"Production": "green",
"Off": "gray",
"Idle": "gray",
"Problem": "red",
"Maintenance": "yellow",
"Setup": "blue",
}
return [__(doc.status), color_map[doc.status], true];
}
// add_fields: ["status"],
// filters:[["status","=", "Open"]]
};

View File

@@ -1,58 +1,150 @@
{
"actions": [],
"creation": "2014-12-24 14:46:40.678236",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"start_time",
"hours",
"column_break_2",
"end_time",
"enabled"
],
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-12-24 14:46:40.678236",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time",
"reqd": 1
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time",
"reqd": 1
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enabled"
},
{
"fieldname": "hours",
"fieldtype": "Float",
"label": "Hours",
"read_only": 1
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"istable": 1,
"links": [],
"modified": "2023-10-25 14:48:29.697498",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Working Hour",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-12-13 05:02:36.754145",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Working Hour",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

@@ -11,21 +11,19 @@
"event": "Value Change",
"idx": 0,
"is_standard": 1,
"message_type": "HTML",
"message": "<b>Material Request Type</b>: {{ doc.material_request_type }}<br>\n<b>Company</b>: {{ doc.company }}\n\n<h3>Order Summary</h3>\n\n<table border=2 >\n <tr align=\"center\">\n <th>Item Name</th>\n <th>Received Quantity</th>\n </tr>\n {% for item in doc.items %}\n {% if frappe.utils.flt(item.received_qty, 2) > 0.0 %}\n <tr align=\"center\">\n <td>{{ item.item_code }}</td>\n <td>{{ frappe.utils.flt(item.received_qty, 2) }}</td>\n </tr>\n {% endif %}\n {% endfor %}\n</table>",
"method": "",
"modified": "2023-11-17 08:53:29.525296",
"modified": "2019-05-01 18:02:51.090037",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Receipt Notification",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "requested_by"
"email_by_document_field": "requested_by"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"sender_email": "",
"subject": "{{ doc.name }} has been received",
"value_changed": "status"
}
}

View File

@@ -1,12 +1,12 @@
<p><b>{{ _("Material Request Type") }}</b>: {{ doc.material_request_type }}<br>
<b>{{ _("Company") }}</b>: {{ doc.company }}</p>
<b>Material Request Type</b>: {{ doc.material_request_type }}<br>
<b>Company</b>: {{ doc.company }}
<h3>{{ _("Order Summary") }}</h3>
<h3>Order Summary</h3>
<table border=2 >
<tr align="center">
<th>{{ _("Item Name") }}</th>
<th>{{ _("Received Quantity") }}</th>
<th>Item Name</th>
<th>Received Quantity</th>
</tr>
{% for item in doc.items %}
{% if frappe.utils.flt(item.received_qty, 2) > 0.0 %}
@@ -16,4 +16,4 @@
</tr>
{% endif %}
{% endfor %}
</table>
</table>

View File

@@ -1,13 +0,0 @@
frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Visual Plant Floor',
single_column: true
});
frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor(
{wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page
);
}

View File

@@ -1,29 +0,0 @@
{
"content": null,
"creation": "2023-10-06 15:17:39.215300",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2023-10-06 15:18:00.622073",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "visual-plant-floor",
"owner": "Administrator",
"page_name": "visual-plant-floor",
"roles": [
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
},
{
"role": "Operator"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Visual Plant Floor"
}

View File

@@ -1,6 +1,6 @@
{
"charts": [],
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
"modified": "2024-01-30 21:49:58.577218",
"modified": "2023-08-08 22:28:39.633891",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,13 +336,6 @@
"type": "URL",
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Plant Floor",
"link_to": "Plant Floor",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",

View File

@@ -160,7 +160,7 @@ erpnext.accounts.taxes = {
let tax = frappe.get_doc(cdt, cdn);
try {
me.validate_taxes_and_charges(cdt, cdn);
me.validate_inclusive_tax(tax, frm);
me.validate_inclusive_tax(tax);
} catch(e) {
tax.included_in_print_rate = 0;
refresh_field("included_in_print_rate", tax.name, tax.parentfield);
@@ -170,8 +170,7 @@ erpnext.accounts.taxes = {
});
},
validate_inclusive_tax: function(tax, frm) {
this.frm = this.frm || frm;
validate_inclusive_tax: function(tax) {
let actual_type_error = function() {
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
frappe.throw(msg);
@@ -187,12 +186,12 @@ erpnext.accounts.taxes = {
if(tax.charge_type == "Actual") {
// inclusive tax cannot be of type Actual
actual_type_error();
} else if (tax.charge_type == "On Previous Row Amount" && this.frm &&
} else if(tax.charge_type == "On Previous Row Amount" &&
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate)
) {
// referred row should also be an inclusive tax
on_previous_row_error(tax.row_id);
} else if (tax.charge_type == "On Previous Row Total" && this.frm) {
} else if(tax.charge_type == "On Previous Row Total") {
var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
function(t) { return cint(t.included_in_print_rate) ? null : t; });
if(taxes_not_included.length > 0) {

View File

@@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.determine_exclusive_rate();
this.calculate_net_total();
this.calculate_taxes();
this.adjust_grand_total_for_inclusive_tax();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
this._cleanup();
}
@@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (!this.discount_amount_applied) {
erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name);
erpnext.accounts.taxes.validate_inclusive_tax(tax, this.frm);
erpnext.accounts.taxes.validate_inclusive_tax(tax);
}
frappe.model.round_floats_in(tax);
});
@@ -248,7 +248,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) {
var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty;
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), precision("net_amount", item));
item.net_amount = flt(amount / (1 + cumulated_tax_fraction));
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
@@ -303,8 +303,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount;
});
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
}
calculate_shipping_charges() {
@@ -523,17 +521,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
}
/**
* @deprecated Use adjust_grand_total_for_inclusive_tax instead.
*/
manipulate_grand_total_for_inclusive_tax() {
// for backward compatablility - if in case used by an external application
this.adjust_grand_total_for_inclusive_tax()
}
adjust_grand_total_for_inclusive_tax() {
var me = this;
// if fully inclusive taxes and diff
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
var any_inclusive_tax = false;
@@ -559,9 +548,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
diff = flt(diff, precision("rounding_adjustment"));
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.frm.doc.grand_total_diff = diff;
} else {
me.frm.doc.grand_total_diff = 0;
me.frm.doc.rounding_adjustment = diff;
}
}
}
@@ -572,7 +559,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
: this.frm.doc.net_total);
if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
@@ -632,7 +619,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
this.frm.doc.currency, precision("rounded_total"));
this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
this.frm.doc.rounding_adjustment += flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment"));
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
@@ -700,7 +687,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (total_for_discount_amount) {
$.each(this.frm._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item));
net_total += item.net_amount;
// discount amount rounding loss adjustment if no taxes

View File

@@ -502,7 +502,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
project: item.project || me.frm.doc.project,
qty: item.qty || 1,
net_rate: item.rate,
base_net_rate: item.base_net_rate,
stock_qty: item.stock_qty,
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
@@ -791,25 +790,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
if (company_doc){
if (company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
if (company_doc && company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
frappe.run_serially([
() => me.frm.script_manager.trigger("currency"),
() => me.update_item_tax_map(),
@@ -1903,7 +1901,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.base_net_rate;
item_rates[item.name] = item.net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});

View File

@@ -5,8 +5,6 @@ import "./sms_manager";
import "./utils/party";
import "./controllers/stock_controller";
import "./payment/payments";
import "./templates/visual_plant_floor_template.html";
import "./plant_floor_visual/visual_plant";
import "./controllers/taxes_and_totals";
import "./controllers/transaction";
import "./templates/item_selector.html";

View File

@@ -2,58 +2,7 @@ frappe.provide("erpnext.financial_statements");
erpnext.financial_statements = {
"filters": get_filters(),
"baseData": null,
"formatter": function(value, row, column, data, default_formatter, filter) {
if(frappe.query_report.get_filter_value("selected_view") == "Growth" && data && column.colIndex >= 3){
//Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns.
const lastAnnualValue = row[column.colIndex - 1].content;
const currentAnnualvalue = data[column.fieldname];
if(currentAnnualvalue == undefined) return 'NA'; //making this not applicable for undefined/null values
let annualGrowth = 0;
if(lastAnnualValue == 0 && currentAnnualvalue > 0){
//If the previous year value is 0 and the current value is greater than 0
annualGrowth = 1;
}
else if(lastAnnualValue > 0){
annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue;
}
const growthPercent = (Math.round(annualGrowth*10000)/100); //calculating the rounded off percentage
value = $(`<span>${((growthPercent >=0)? '+':'' )+growthPercent+'%'}</span>`);
if(growthPercent < 0){
value = $(value).addClass("text-danger");
}
else{
value = $(value).addClass("text-success");
}
value = $(value).wrap("<p></p>").parent().html();
return value;
}
else if(frappe.query_report.get_filter_value("selected_view") == "Margin" && data){
if(column.fieldname =="account" && data.account_name == __("Income")){
//Taking the total income from each column (for all the financial years) as the base (100%)
this.baseData = row;
}
if(column.colIndex >= 2){
//Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns.
const currentAnnualvalue = data[column.fieldname];
const baseValue = this.baseData[column.colIndex].content;
if(currentAnnualvalue == undefined || baseValue <= 0) return 'NA';
const marginPercent = Math.round((currentAnnualvalue/baseValue)*10000)/100;
value = $(`<span>${marginPercent+'%'}</span>`);
if(marginPercent < 0)
value = $(value).addClass("text-danger");
else
value = $(value).addClass("text-success");
value = $(value).wrap("<p></p>").parent().html();
return value;
}
}
if (data && column.fieldname=="account") {
value = data.account_name || value;
@@ -125,24 +74,22 @@ erpnext.financial_statements = {
});
});
if (report.page){
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
});
}
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
});
}
};

View File

@@ -1,157 +0,0 @@
class VisualPlantFloor {
constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) {
this.wrapper = wrapper;
this.plant_floor = plant_floor;
this.skip_filters = skip_filters;
this.make();
if (!this.skip_filters) {
this.page = page;
this.add_filter();
this.prepare_menu();
}
}
make() {
this.wrapper.append(`
<div class="plant-floor">
<div class="plant-floor-filter">
</div>
<div class="plant-floor-container col-sm-12">
</div>
</div>
`);
if (!this.skip_filters) {
this.filter_wrapper = this.wrapper.find('.plant-floor-filter');
this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization');
} else if(this.plant_floor) {
this.wrapper.find('.plant-floor').css('border', 'none');
this.prepare_data();
}
}
prepare_data() {
frappe.call({
method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
args: {
plant_floor: this.plant_floor,
},
callback: (r) => {
this.workstations = r.message;
this.render_workstations();
}
});
}
add_filter() {
this.plant_floor = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Plant Floor',
fieldname: 'plant_floor',
label: __('Plant Floor'),
reqd: 1,
onchange: () => {
this.render_plant_visualization();
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.plant_floor.$wrapper.addClass('form-column col-sm-2');
this.workstation_type = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Workstation Type',
fieldname: 'workstation_type',
label: __('Machine Type'),
onchange: () => {
this.render_plant_visualization();
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.workstation_type.$wrapper.addClass('form-column col-sm-2');
this.workstation = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Workstation',
fieldname: 'workstation',
label: __('Machine'),
onchange: () => {
this.render_plant_visualization();
},
get_query: () => {
if (this.workstation_type.get_value()) {
return {
filters: {
'workstation_type': this.workstation_type.get_value() || ''
}
}
}
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.workstation.$wrapper.addClass('form-column col-sm-2');
this.workstation_status = frappe.ui.form.make_control({
df: {
fieldtype: 'Select',
options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup',
fieldname: 'workstation_status',
label: __('Status'),
onchange: () => {
this.render_plant_visualization();
},
},
parent: this.filter_wrapper,
render_input: true,
});
}
render_plant_visualization() {
let plant_floor = this.plant_floor.get_value();
if (plant_floor) {
frappe.call({
method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
args: {
plant_floor: plant_floor,
workstation_type: this.workstation_type.get_value(),
workstation: this.workstation.get_value(),
workstation_status: this.workstation_status.get_value()
},
callback: (r) => {
this.workstations = r.message;
this.render_workstations();
}
});
}
}
render_workstations() {
this.wrapper.find('.plant-floor-container').empty();
let template = frappe.render_template("visual_plant_floor_template", {
workstations: this.workstations
});
$(template).appendTo(this.wrapper.find('.plant-floor-container'));
}
prepare_menu() {
this.page.add_menu_item(__('Refresh'), () => {
this.render_plant_visualization();
});
}
}
frappe.ui.VisualPlantFloor = VisualPlantFloor;

View File

@@ -1,19 +0,0 @@
{% $.each(workstations, (idx, row) => { %}
<div class="workstation-wrapper">
<div class="workstation-image">
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<a class="workstation-image-link" href="{{row.workstation_link}}">
{% if(row.status_image) { %}
<img class="workstation-image-cls" src="{{row.status_image}}">
{% } else { %}
<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row.name, 2)}}</div>
{% } %}
</a>
</div>
</div>
<div class="workstation-card text-center">
<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
<div>{{row.workstation_name}}</div>
</div>
</div>
{% }); %}

View File

@@ -490,53 +490,3 @@ body[data-route="pos"] {
.exercise-col {
padding: 10px;
}
.plant-floor, .workstation-wrapper, .workstation-card p {
border-radius: var(--border-radius-md);
border: 1px solid var(--border-color);
box-shadow: none;
background-color: var(--card-bg);
position: relative;
}
.plant-floor {
padding-bottom: 25px;
}
.plant-floor-filter {
padding-top: 10px;
display: flex;
flex-wrap: wrap;
}
.plant-floor-container {
display: grid;
grid-template-columns: repeat(6,minmax(0,1fr));
gap: var(--margin-xl);
}
@media screen and (max-width: 620px) {
.plant-floor-container {
grid-template-columns: repeat(2,minmax(0,1fr));
}
}
.plant-floor-container .workstation-card {
padding: 5px;
}
.plant-floor-container .workstation-image-link {
width: 100%;
font-size: 50px;
margin: var(--margin-sm);
min-height: 9rem;
}
.workstation-abbr {
display: flex;
background-color: var(--control-bg);
height:100%;
width:100%;
align-items: center;
justify-content: center;
}

View File

@@ -124,7 +124,6 @@ class Customer(TransactionBase):
),
title=_("Note"),
indicator="yellow",
alert=True,
)
return new_customer_name
@@ -230,7 +229,6 @@ class Customer(TransactionBase):
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
self.copy_communication()
self.update_customer_groups()
@@ -292,17 +290,6 @@ class Customer(TransactionBase):
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
def copy_communication(self):
if not self.lead_name or not frappe.db.get_single_value(
"CRM Settings", "carry_forward_communication_and_comments"
):
return
from erpnext.crm.utils import copy_comments, link_communications
copy_comments("Lead", self.lead_name, self)
link_communications("Lead", self.lead_name, self)
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(
@@ -572,14 +559,15 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
@frappe.whitelist()
def send_emails(customer, customer_outstanding, credit_limit, credit_controller_users_list):
if isinstance(credit_controller_users_list, str):
credit_controller_users_list = json.loads(credit_controller_users_list)
subject = _("Credit limit reached for customer {0}").format(customer)
def send_emails(args):
args = json.loads(args)
subject = _("Credit limit reached for customer {0}").format(args.get("customer"))
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
customer, customer_outstanding, credit_limit
args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit")
)
frappe.sendmail(
recipients=args.get("credit_controller_users_list"), subject=subject, message=message
)
frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
def get_customer_outstanding(

View File

@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2013-06-20 11:53:21",
"description": "Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item",
"description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -77,7 +77,7 @@
"icon": "fa fa-sitemap",
"idx": 1,
"links": [],
"modified": "2024-01-30 13:57:04.951788",
"modified": "2023-11-22 15:20:46.805114",
"modified_by": "Administrator",
"module": "Selling",
"name": "Product Bundle",

View File

@@ -346,8 +346,8 @@ def make_sales_order(source_name: str, target_doc=None):
return _make_sales_order(source_name, target_doc)
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
customer = _make_customer(source_name, ignore_permissions)
def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
customer = _make_customer(source_name, ignore_permissions, customer_group)
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@@ -391,6 +391,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
target.qty = balance_qty if balance_qty > 0 else 0
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
target.delivery_date = nowdate()
if obj.against_blanket_order:
target.against_blanket_order = obj.against_blanket_order
@@ -506,51 +507,50 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
return doclist
def _make_customer(source_name, ignore_permissions=False):
def _make_customer(source_name, ignore_permissions=False, customer_group=None):
quotation = frappe.db.get_value(
"Quotation",
source_name,
["order_type", "quotation_to", "party_name", "customer_name"],
as_dict=1,
"Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1
)
if quotation.quotation_to == "Customer":
return frappe.get_doc("Customer", quotation.party_name)
if quotation and quotation.get("party_name"):
if not frappe.db.exists("Customer", quotation.get("party_name")):
lead_name = quotation.get("party_name")
customer_name = frappe.db.get_value(
"Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True
)
if not customer_name:
from erpnext.crm.doctype.lead.lead import _make_customer
# If the Quotation is not to a Customer, it must be to a Lead.
# Check if a Customer already exists for the Lead.
existing_customer_for_lead = frappe.db.get_value("Customer", {"lead_name": quotation.party_name})
if existing_customer_for_lead:
return frappe.get_doc("Customer", existing_customer_for_lead)
customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
customer = frappe.get_doc(customer_doclist)
customer.flags.ignore_permissions = ignore_permissions
customer.customer_group = customer_group
# If no Customer exists for the Lead, create a new Customer.
return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions)
try:
customer.insert()
return customer
except frappe.NameError:
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
customer.run_method("autoname")
customer.name += "-" + lead_name
customer.insert()
return customer
else:
raise
except frappe.MandatoryError as e:
mandatory_fields = e.args[0].split(":")[1].split(",")
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
frappe.local.message_log = []
lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
message = (
_("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
)
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
message += _("Please create Customer from Lead {0}.").format(lead_link)
def create_customer_from_lead(lead_name, ignore_permissions=False):
from erpnext.crm.doctype.lead.lead import _make_customer
customer = _make_customer(lead_name, ignore_permissions=ignore_permissions)
customer.flags.ignore_permissions = ignore_permissions
try:
customer.insert()
return customer
except frappe.MandatoryError as e:
handle_mandatory_error(e, customer, lead_name)
def handle_mandatory_error(e, customer, lead_name):
from frappe.utils import get_link_to_form
mandatory_fields = e.args[0].split(":")[1].split(",")
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
frappe.local.message_log = []
message = (
_("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
)
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name))
frappe.throw(message, title=_("Mandatory Missing"))
frappe.throw(message, title=_("Mandatory Missing"))
else:
return customer_name
else:
return frappe.get_doc("Customer", quotation.get("party_name"))

View File

@@ -99,6 +99,7 @@ class TestQuotation(FrappeTestCase):
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
self.assertEqual(sales_order.customer, "_Test Customer")
sales_order.delivery_date = "2014-01-01"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.insert()
@@ -131,6 +132,7 @@ class TestQuotation(FrappeTestCase):
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
self.assertEqual(sales_order.customer, "_Test Customer")
sales_order.delivery_date = "2014-01-01"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.insert()

View File

@@ -118,7 +118,6 @@
"oldfieldtype": "Link",
"options": "Item",
"print_width": "150px",
"reqd": 1,
"width": "150px"
},
{
@@ -909,7 +908,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-01-25 14:24:00.330219",
"modified": "2023-11-24 13:24:55.756320",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -43,8 +43,7 @@ class SalesOrderItem(Document):
gross_profit: DF.Currency
image: DF.Attach | None
is_free_item: DF.Check
is_stock_item: DF.Check
item_code: DF.Link
item_code: DF.Link | None
item_group: DF.Link | None
item_name: DF.Data
item_tax_rate: DF.Code | None

Some files were not shown because too many files have changed in this diff Show More