Merge pull request #39758 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
af8af2b36e
@ -118,6 +118,7 @@ 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:
|
||||
@ -188,6 +189,24 @@ 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()
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2013-06-24 15:49:57",
|
||||
"description": "Settings for Accounts",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"editable_grid": 1,
|
||||
@ -462,7 +461,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-20 09:37:47.650347",
|
||||
"modified": "2024-01-30 14:04:26.553554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -9,6 +9,7 @@ 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):
|
||||
@ -52,6 +53,17 @@ 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:
|
||||
|
@ -5,7 +5,9 @@
|
||||
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
|
||||
|
||||
@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if include_pos_transactions:
|
||||
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,
|
||||
)
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
|
@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
|
||||
frappe.db.delete(dt)
|
||||
clear_loan_transactions()
|
||||
make_pos_profile()
|
||||
add_transactions()
|
||||
add_vouchers()
|
||||
|
||||
# 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)
|
||||
|
||||
# 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):
|
||||
@ -219,7 +227,9 @@ def clear_loan_transactions():
|
||||
frappe.db.delete("Loan Repayment")
|
||||
|
||||
|
||||
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
def create_bank_account(
|
||||
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
|
||||
):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
pass
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
bank_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "Checking Account",
|
||||
"account_name": bank_account_name,
|
||||
"bank": bank_name,
|
||||
"account": account_name,
|
||||
"account": gl_account,
|
||||
}
|
||||
).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",
|
||||
@ -253,7 +277,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1200,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -265,7 +289,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1700,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -277,7 +301,7 @@ def add_transactions():
|
||||
"date": "2018-10-26",
|
||||
"withdrawal": 690,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -289,7 +313,7 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"deposit": 3900,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -301,13 +325,13 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"withdrawal": 109080,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
|
||||
def add_vouchers():
|
||||
def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@ -323,7 +347,7 @@ def add_vouchers():
|
||||
|
||||
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
@ -342,14 +366,14 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
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="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Herr G Nov 18"
|
||||
pe.reference_date = "2018-11-01"
|
||||
pe.insert()
|
||||
@ -380,10 +404,10 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
|
||||
pi.cash_bank_account = "_Test Bank - _TC"
|
||||
pi.cash_bank_account = gl_account
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.paid_amount = 690
|
||||
@ -392,7 +416,7 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.insert()
|
||||
@ -415,16 +439,12 @@ def add_vouchers():
|
||||
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": "_Test Bank - _TC"}
|
||||
)
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
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": "_Test Bank - _TC", "amount": 109080}
|
||||
)
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"autoname": "field:year",
|
||||
"creation": "2013-01-22 16:50:25",
|
||||
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
|
||||
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -82,10 +82,11 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -122,5 +123,6 @@
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "name",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -13,16 +13,9 @@ 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,
|
||||
InvalidAccountDimensionError,
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@ -98,7 +91,6 @@ 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)
|
||||
|
||||
@ -208,42 +200,6 @@ 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"
|
||||
|
@ -1,173 +1,77 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"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
|
||||
},
|
||||
"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,
|
||||
"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": "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": "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
|
||||
"fieldname": "percentages",
|
||||
"fieldtype": "Table",
|
||||
"label": "Monthly Distribution Percentages",
|
||||
"oldfieldname": "budget_distribution_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Monthly Distribution Percentage"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
],
|
||||
"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",
|
||||
"permissions": [
|
||||
{
|
||||
"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,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"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
|
||||
"permlevel": 2,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -946,19 +946,19 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
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
|
||||
else:
|
||||
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()
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
|
||||
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
|
||||
self.difference_amount = flt(
|
||||
self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
|
||||
self.difference_amount - total_deductions, self.precision("difference_amount")
|
||||
)
|
||||
|
||||
def get_included_taxes(self):
|
||||
|
@ -4,9 +4,13 @@
|
||||
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
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
|
||||
create_bank_account,
|
||||
create_gl_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_payment_entry,
|
||||
make_payment_order,
|
||||
@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
|
||||
class TestPaymentOrder(unittest.TestCase):
|
||||
class TestPaymentOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_bank_account()
|
||||
# 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
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for bt in frappe.get_all("Payment Order"):
|
||||
doc = frappe.get_doc("Payment Order", bt.name)
|
||||
doc.cancel()
|
||||
doc.delete()
|
||||
frappe.db.rollback()
|
||||
|
||||
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="_Test Bank - _TC"
|
||||
"Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
|
||||
)
|
||||
payment_entry.reference_no = "_Test_Payment_Order"
|
||||
payment_entry.reference_date = getdate()
|
||||
payment_entry.party_bank_account = "Checking Account - Citi Bank"
|
||||
payment_entry.party_bank_account = self.bank_account
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
||||
doc = create_payment_order_against_payment_entry(
|
||||
payment_entry, "Payment Entry", self.bank_account
|
||||
)
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
|
||||
self.assertEqual(reference_doc.amount, 250)
|
||||
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
|
||||
payment_order = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account="Checking Account - Citi Bank",
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
)
|
||||
doc = make_payment_order(ref_doc.name, payment_order)
|
||||
|
@ -80,13 +80,16 @@
|
||||
"target_warehouse",
|
||||
"quality_inspection",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"serial_no",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_tlhi",
|
||||
"serial_no",
|
||||
"column_break_ciit",
|
||||
"batch_no",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@ -628,13 +631,13 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break5",
|
||||
@ -649,14 +652,14 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_tax_rate",
|
||||
@ -824,17 +827,33 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_tlhi",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ciit",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:33:22.585715",
|
||||
"modified": "2024-02-04 16:36:25.665743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
|
||||
target_warehouse: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
weight_uom: DF.Link | None
|
||||
|
@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
get_pricing_rule_items,
|
||||
)
|
||||
|
||||
if isinstance(item_details, str):
|
||||
item_details = json.loads(item_details)
|
||||
item_details = frappe._dict(item_details)
|
||||
|
||||
for d in get_applied_pricing_rules(pricing_rules):
|
||||
if not d or not frappe.db.exists("Pricing Rule", d):
|
||||
continue
|
||||
|
@ -120,18 +120,6 @@ 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)
|
||||
@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
)
|
||||
|
||||
filters = get_common_filters(doc)
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
|
@ -696,6 +696,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
|
@ -62,16 +62,19 @@
|
||||
"rm_supp_cost",
|
||||
"warehouse_section",
|
||||
"warehouse",
|
||||
"from_warehouse",
|
||||
"quality_inspection",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"serial_no",
|
||||
"use_serial_batch_fields",
|
||||
"col_br_wh",
|
||||
"from_warehouse",
|
||||
"quality_inspection",
|
||||
"rejected_warehouse",
|
||||
"rejected_serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"section_break_rqbe",
|
||||
"serial_no",
|
||||
"rejected_serial_no",
|
||||
"column_break_vbbb",
|
||||
"batch_no",
|
||||
"manufacture_details",
|
||||
"manufacturer",
|
||||
"column_break_13",
|
||||
@ -440,13 +443,11 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@ -454,21 +455,18 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "rejected_serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Rejected Serial No",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting",
|
||||
@ -891,7 +889,7 @@
|
||||
"label": "Apply TDS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -901,7 +899,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "rejected_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@ -916,16 +914,31 @@
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1",
|
||||
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "section_break_rqbe",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vbbb",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-21 19:46:25.537861",
|
||||
"modified": "2024-02-04 14:11:52.742228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
|
||||
stock_uom_rate: DF.Currency
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
|
@ -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 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.",
|
||||
"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.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -77,7 +77,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-16 16:15:29.059370",
|
||||
"modified": "2024-01-30 13:08:09.537242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges Template",
|
||||
|
@ -446,6 +446,7 @@ class SalesInvoice(SellingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
|
@ -83,14 +83,17 @@
|
||||
"quality_inspection",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"incoming_rate",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"serial_no",
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_eoec",
|
||||
"serial_no",
|
||||
"column_break_ytgd",
|
||||
"batch_no",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@ -600,12 +603,11 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@ -621,13 +623,12 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
@ -891,6 +892,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -904,12 +906,27 @@
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "section_break_eoec",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ytgd",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-29 13:03:14.121298",
|
||||
"modified": "2024-02-04 11:52:16.106541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
|
||||
target_warehouse: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
weight_uom: DF.Link | None
|
||||
|
@ -8,6 +8,7 @@
|
||||
"default",
|
||||
"mode_of_payment",
|
||||
"amount",
|
||||
"reference_no",
|
||||
"column_break_3",
|
||||
"account",
|
||||
"type",
|
||||
@ -75,11 +76,16 @@
|
||||
"hidden": 1,
|
||||
"label": "Default",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference No"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 12:45:39.986598",
|
||||
"modified": "2024-01-23 16:20:06.436979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
@ -87,5 +93,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -23,6 +23,7 @@ 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
|
||||
|
||||
|
@ -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 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.",
|
||||
"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.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -79,7 +79,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-16 16:14:52.061672",
|
||||
"modified": "2024-01-30 13:07:28.801104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges Template",
|
||||
|
@ -13,9 +13,13 @@ 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(
|
||||
@ -355,6 +359,7 @@ 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)
|
||||
@ -362,6 +367,7 @@ 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)
|
||||
|
||||
|
||||
@ -700,3 +706,39 @@ 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,
|
||||
)
|
||||
|
@ -203,8 +203,14 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_err",
|
||||
"label": __("Ignore Exchange Rate Revaluation Journals"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -241,6 +241,19 @@ 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")
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
|
||||
@ -148,3 +148,105 @@ 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]))
|
||||
|
@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
|
||||
"options": erpnext.get_presentation_currency_list()
|
||||
},
|
||||
{
|
||||
"fieldname": "with_period_closing_entry",
|
||||
"label": __("Period Closing Entry"),
|
||||
"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"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
|
@ -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),
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
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):
|
||||
if not flt(filters.with_period_closing_entry_for_opening):
|
||||
if doctype == "Account Closing Balance":
|
||||
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
|
@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
|
||||
self.create_target_asset()
|
||||
|
||||
def on_submit(self):
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
@ -18,9 +18,12 @@
|
||||
"amount",
|
||||
"batch_and_serial_no_section",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"column_break_13",
|
||||
"batch_no",
|
||||
"section_break_bfqc",
|
||||
"serial_no",
|
||||
"column_break_mbuv",
|
||||
"batch_no",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
@ -39,13 +42,13 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"no_copy": 1,
|
||||
"options": "Batch",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
@ -102,12 +105,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
@ -148,18 +151,34 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_bfqc",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_mbuv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 01:10:17.947952",
|
||||
"modified": "2024-02-04 16:41:09.239762",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Stock Item",
|
||||
|
@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document):
|
||||
serial_no: DF.SmallText | None
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2013-06-25 11:04:03",
|
||||
"description": "Settings for Buying Module",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
@ -152,6 +151,7 @@
|
||||
},
|
||||
{
|
||||
"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-12 16:42:01.894346",
|
||||
"modified": "2024-01-31 13:34:18.101256",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -264,4 +264,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -453,6 +453,7 @@ 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)
|
||||
|
||||
@ -626,6 +627,7 @@ 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"""
|
||||
|
||||
|
@ -814,6 +814,30 @@ 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,
|
||||
@ -1113,6 +1137,7 @@ 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,
|
||||
},
|
||||
|
@ -545,7 +545,6 @@
|
||||
"fieldname": "blanket_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Blanket Order",
|
||||
"no_copy": 1,
|
||||
"options": "Blanket Order"
|
||||
},
|
||||
{
|
||||
@ -553,7 +552,6 @@
|
||||
"fieldname": "blanket_order_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Blanket Order Rate",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -917,7 +915,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-24 13:24:41.298416",
|
||||
"modified": "2024-02-05 11:23:24.859435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -693,7 +693,7 @@ class AccountsController(TransactionBase):
|
||||
if self.get("is_subcontracted"):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
|
@ -724,17 +724,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions, bin_conditions = [], []
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
query = """select `tabWarehouse`.name,
|
||||
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}`,
|
||||
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
|
||||
order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
|
||||
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
|
||||
),
|
||||
|
@ -602,7 +602,7 @@ class SellingController(StockController):
|
||||
if self.doctype in ["Sales Order", "Quotation"]:
|
||||
for item in self.items:
|
||||
item.gross_profit = flt(
|
||||
((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
|
||||
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
|
||||
)
|
||||
|
||||
def set_customer_address(self):
|
||||
|
@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_type_of_transaction,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
|
||||
|
||||
@ -126,6 +129,81 @@ class StockController(AccountsController):
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def make_bundle_using_old_serial_batch_fields(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
# To handle test cases
|
||||
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
|
||||
return
|
||||
|
||||
table_name = "items"
|
||||
if self.doctype == "Asset Capitalization":
|
||||
table_name = "stock_items"
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
|
||||
continue
|
||||
|
||||
if not row.use_serial_batch_fields and (
|
||||
row.serial_no or row.batch_no or row.get("rejected_serial_no")
|
||||
):
|
||||
frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
|
||||
|
||||
if row.use_serial_batch_fields and (
|
||||
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
|
||||
):
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
qty = row.qty
|
||||
type_of_transaction = "Inward"
|
||||
else:
|
||||
qty = row.stock_qty
|
||||
type_of_transaction = get_type_of_transaction(self, row)
|
||||
|
||||
sn_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"qty": qty,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"company": self.company,
|
||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
|
||||
"batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
|
||||
"batch_no": row.batch_no,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
if sn_doc.is_rejected:
|
||||
row.rejected_serial_and_batch_bundle = sn_doc.name
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": sn_doc.name,
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.serial_and_batch_bundle = sn_doc.name
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": sn_doc.name,
|
||||
"serial_no": "",
|
||||
"batch_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def set_use_serial_batch_fields(self):
|
||||
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
|
||||
for row in self.items:
|
||||
row.use_serial_batch_fields = 1
|
||||
|
||||
def get_gl_entries(
|
||||
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
|
||||
):
|
||||
|
@ -97,6 +97,7 @@ 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"),
|
||||
|
@ -955,6 +955,14 @@ 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(
|
||||
@ -1035,6 +1043,12 @@ 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):
|
||||
|
256
erpnext/manufacturing/doctype/plant_floor/plant_floor.js
Normal file
256
erpnext/manufacturing/doctype/plant_floor/plant_floor.js
Normal file
@ -0,0 +1,256 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
erpnext/manufacturing/doctype/plant_floor/plant_floor.json
Normal file
97
erpnext/manufacturing/doctype/plant_floor/plant_floor.json
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
129
erpnext/manufacturing/doctype/plant_floor/plant_floor.py
Normal file
129
erpnext/manufacturing/doctype/plant_floor/plant_floor.py
Normal file
@ -0,0 +1,129 @@
|
||||
# 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)
|
@ -0,0 +1,61 @@
|
||||
{% $.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>
|
||||
{% }); %}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPlantFloor(FrappeTestCase):
|
||||
pass
|
@ -1334,10 +1334,10 @@ def get_sales_orders(self):
|
||||
)
|
||||
|
||||
date_field_mapper = {
|
||||
"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,
|
||||
"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,
|
||||
}
|
||||
|
||||
for field, value in date_field_mapper.items():
|
||||
|
@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
|
||||
|
||||
|
||||
def validate_operation_data(row):
|
||||
if row.get("qty") <= 0:
|
||||
if flt(row.get("qty")) <= 0:
|
||||
frappe.throw(
|
||||
_("Quantity to Manufacture can not be zero for the operation {0}").format(
|
||||
frappe.bold(row.get("operation"))
|
||||
)
|
||||
)
|
||||
|
||||
if row.get("qty") > row.get("pending_qty"):
|
||||
if flt(row.get("qty")) > flt(row.get("pending_qty")):
|
||||
frappe.throw(
|
||||
_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format(
|
||||
frappe.bold(row.get("operation")),
|
||||
|
@ -2,6 +2,28 @@
|
||||
// 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())
|
||||
{
|
||||
@ -54,3 +76,243 @@ 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");
|
||||
}
|
||||
}
|
@ -8,10 +8,24 @@
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dashboard_tab",
|
||||
"workstation_dashboard",
|
||||
"details_tab",
|
||||
"workstation_name",
|
||||
"production_capacity",
|
||||
"column_break_3",
|
||||
"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",
|
||||
"over_heads",
|
||||
"hour_rate_electricity",
|
||||
"hour_rate_consumable",
|
||||
@ -24,7 +38,9 @@
|
||||
"description",
|
||||
"working_hours_section",
|
||||
"holiday_list",
|
||||
"working_hours"
|
||||
"working_hours",
|
||||
"total_working_hours",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -120,9 +136,10 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Run parallel job cards in a workstation",
|
||||
"fieldname": "production_capacity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Production Capacity",
|
||||
"label": "Job Capacity",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -145,12 +162,97 @@
|
||||
{
|
||||
"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": "2022-11-04 17:39:01.549346",
|
||||
"modified": "2023-11-30 12:43:35.808845",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Workstation",
|
||||
|
@ -11,7 +11,11 @@ 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,
|
||||
)
|
||||
@ -60,6 +64,23 @@ 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 = (
|
||||
@ -143,6 +164,141 @@ 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():
|
||||
@ -201,3 +357,52 @@ 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
|
||||
|
@ -0,0 +1,125 @@
|
||||
<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>
|
@ -1,5 +1,16 @@
|
||||
|
||||
frappe.listview_settings['Workstation'] = {
|
||||
// add_fields: ["status"],
|
||||
// filters:[["status","=", "Open"]]
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
@ -1,150 +1,58 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"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": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"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": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "End Time",
|
||||
"reqd": 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
|
||||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "hours",
|
||||
"fieldtype": "Float",
|
||||
"label": "Hours",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"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
|
||||
],
|
||||
"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": []
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
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
|
||||
);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -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\":\"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 & 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\":\"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 & 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": "2023-08-08 22:28:39.633891",
|
||||
"modified": "2024-01-30 21:49:58.577218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
@ -336,6 +336,13 @@
|
||||
"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",
|
||||
|
@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
super.setup();
|
||||
let me = this;
|
||||
|
||||
this.set_fields_onload_for_line_item();
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", {
|
||||
items_add: function(frm, cdt, cdn) {
|
||||
debugger
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if (!item.warehouse && frm.doc.set_warehouse) {
|
||||
item.warehouse = frm.doc.set_warehouse;
|
||||
@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
item.from_warehouse = frm.doc.set_from_warehouse;
|
||||
}
|
||||
|
||||
if (item.docstatus === 0
|
||||
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
|
||||
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
|
||||
) {
|
||||
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
|
||||
}
|
||||
});
|
||||
@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
set_fields_onload_for_line_item() {
|
||||
if (this.frm.is_new && this.frm.doc?.items) {
|
||||
this.frm.doc.items.forEach(item => {
|
||||
if (item.docstatus === 0
|
||||
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
|
||||
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
|
||||
) {
|
||||
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toggle_enable_for_stock_uom(field) {
|
||||
@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.doc.doctype === 'Delivery Note') {
|
||||
show_batch_dialog = 1;
|
||||
}
|
||||
|
||||
if (show_batch_dialog && item.use_serial_batch_fields === 1) {
|
||||
show_batch_dialog = 0;
|
||||
}
|
||||
|
||||
item.barcode = null;
|
||||
|
||||
|
||||
@ -502,6 +528,7 @@ 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,
|
||||
@ -705,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
item.serial_no = item.serial_no.replace(/,/g, '\n');
|
||||
item.conversion_factor = item.conversion_factor || 1;
|
||||
refresh_field("serial_no", item.name, item.parentfield);
|
||||
if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
|
||||
if (!doc.is_return) {
|
||||
setTimeout(() => {
|
||||
me.update_qty(cdt, cdn);
|
||||
}, 10000);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -798,14 +825,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
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) {
|
||||
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.indexOf(me.frm.doc.doctype) != -1) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1200,8 +1227,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
// item.pricing_rules = ''
|
||||
frappe.run_serially([
|
||||
() => this.remove_pricing_rule(item),
|
||||
() => this.remove_pricing_rule_for_item(item),
|
||||
() => this.conversion_factor(doc, cdt, cdn, true),
|
||||
() => this.apply_price_list(item, true), //reapply price list before applying pricing rule
|
||||
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
|
||||
() => this.apply_pricing_rule(item, true)
|
||||
]);
|
||||
@ -1240,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
sync_bundle_data() {
|
||||
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
|
||||
|
||||
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||
barcode_scanner.sync_bundle_data();
|
||||
barcode_scanner.remove_item_from_localstorage();
|
||||
}
|
||||
}
|
||||
|
||||
before_save(doc) {
|
||||
this.sync_bundle_data();
|
||||
}
|
||||
|
||||
service_start_date(frm, cdt, cdn) {
|
||||
var child = locals[cdt][cdn];
|
||||
|
||||
@ -1448,8 +1462,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
ignore_pricing_rule() {
|
||||
if(this.frm.doc.ignore_pricing_rule) {
|
||||
var me = this;
|
||||
var item_list = [];
|
||||
let me = this;
|
||||
let item_list = [];
|
||||
|
||||
$.each(this.frm.doc["items"] || [], function(i, d) {
|
||||
if (d.item_code) {
|
||||
@ -1488,6 +1502,34 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
remove_pricing_rule_for_item(item) {
|
||||
let me = this;
|
||||
return this.frm.call({
|
||||
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
|
||||
args: {
|
||||
pricing_rules: item.pricing_rules,
|
||||
item_details: {
|
||||
"doctype": item.doctype,
|
||||
"name": item.name,
|
||||
"item_code": item.item_code,
|
||||
"pricing_rules": item.pricing_rules,
|
||||
"parenttype": item.parenttype,
|
||||
"parent": item.parent,
|
||||
"price_list_rate": item.price_list_rate
|
||||
},
|
||||
item_code: item.item_code,
|
||||
rate: item.price_list_rate,
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
me.remove_pricing_rule(r.message);
|
||||
me.calculate_taxes_and_totals();
|
||||
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
apply_pricing_rule(item, calculate_taxes_and_totals) {
|
||||
var me = this;
|
||||
var args = this._get_args(item);
|
||||
@ -1712,8 +1754,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.set_value("plc_conversion_rate", "");
|
||||
}
|
||||
|
||||
var me = this;
|
||||
var args = this._get_args(item);
|
||||
let me = this;
|
||||
let args = this._get_args(item);
|
||||
if (!((args.items && args.items.length) || args.price_list)) {
|
||||
return;
|
||||
}
|
||||
@ -1755,7 +1797,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
|
||||
|
||||
if(item.remove_free_item) {
|
||||
var items = [];
|
||||
let items = [];
|
||||
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if(d.item_code != item.remove_free_item || !d.is_free_item) {
|
||||
@ -1873,7 +1915,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.net_rate;
|
||||
item_rates[item.name] = item.base_net_rate;
|
||||
item_tax_templates[item.name] = item.item_tax_template;
|
||||
}
|
||||
});
|
||||
|
@ -5,6 +5,8 @@ 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";
|
||||
|
157
erpnext/public/js/plant_floor_visual/visual_plant.js
Normal file
157
erpnext/public/js/plant_floor_visual/visual_plant.js
Normal file
@ -0,0 +1,157 @@
|
||||
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;
|
19
erpnext/public/js/templates/visual_plant_floor_template.html
Normal file
19
erpnext/public/js/templates/visual_plant_floor_template.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% $.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>
|
||||
{% }); %}
|
@ -1,12 +1,15 @@
|
||||
erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
constructor(opts) {
|
||||
this.frm = opts.frm;
|
||||
// frappe.flags.trigger_from_barcode_scanner is used for custom scripts
|
||||
|
||||
// field from which to capture input of scanned data
|
||||
this.scan_field_name = opts.scan_field_name || "scan_barcode";
|
||||
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
|
||||
|
||||
this.barcode_field = opts.barcode_field || "barcode";
|
||||
this.serial_no_field = opts.serial_no_field || "serial_no";
|
||||
this.batch_no_field = opts.batch_no_field || "batch_no";
|
||||
this.uom_field = opts.uom_field || "uom";
|
||||
this.qty_field = opts.qty_field || "qty";
|
||||
// field name on row which defines max quantity to be scanned e.g. picklist
|
||||
@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.frm.has_items = false;
|
||||
}
|
||||
|
||||
if (serial_no) {
|
||||
this.is_duplicate_serial_no(row, item_code, serial_no)
|
||||
.then((is_duplicate) => {
|
||||
if (!is_duplicate) {
|
||||
this.run_serially_tasks(row, data, resolve);
|
||||
} else {
|
||||
this.clean_up();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.run_serially_tasks(row, data, resolve);
|
||||
if (this.is_duplicate_serial_no(row, serial_no)) {
|
||||
this.clean_up();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_selector_trigger_flag(data),
|
||||
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.clean_up(),
|
||||
() => this.revert_selector_flag(),
|
||||
() => resolve(row)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
run_serially_tasks(row, data, resolve) {
|
||||
const {item_code, barcode, batch_no, serial_no, uom} = data;
|
||||
// batch and serial selector is reduandant when all info can be added by scan
|
||||
// this flag on item row is used by transaction.js to avoid triggering selector
|
||||
set_selector_trigger_flag(data) {
|
||||
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.clean_up(),
|
||||
() => {
|
||||
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
|
||||
this.frm.save();
|
||||
}
|
||||
const require_selecting_batch = has_batch_no && !batch_no;
|
||||
const require_selecting_serial = has_serial_no && !serial_no;
|
||||
|
||||
frappe.flags.trigger_from_barcode_scanner = false;
|
||||
},
|
||||
() => resolve(row),
|
||||
]);
|
||||
if (!(require_selecting_batch || require_selecting_serial)) {
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
revert_selector_flag() {
|
||||
frappe.flags.hide_serial_batch_dialog = false;
|
||||
frappe.flags.trigger_from_barcode_scanner = false;
|
||||
}
|
||||
|
||||
set_item(row, item_code, barcode, batch_no, serial_no) {
|
||||
return new Promise(resolve => {
|
||||
const increment = async (value = 1) => {
|
||||
const item_data = {item_code: item_code};
|
||||
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
||||
const item_data = {item_code: item_code, use_serial_batch_fields: 1.0};
|
||||
frappe.flags.trigger_from_barcode_scanner = true;
|
||||
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
||||
await frappe.model.set_value(row.doctype, row.name, item_data);
|
||||
return value;
|
||||
};
|
||||
@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
||||
increment(value).then((value) => resolve(value));
|
||||
});
|
||||
} else if (this.frm.has_items) {
|
||||
this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
|
||||
} else {
|
||||
increment().then((value) => resolve(value));
|
||||
}
|
||||
@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
frappe.model.set_value(row.doctype, row.name, item_data);
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
|
||||
() => this.set_barcode(row, this.dialog.get_value("barcode")),
|
||||
() => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
|
||||
() => this.set_serial_no(row, this.dialog.get_value("serial_no")),
|
||||
() => this.add_child_for_remaining_qty(row),
|
||||
() => this.clean_up()
|
||||
]);
|
||||
@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
}
|
||||
|
||||
async set_serial_and_batch(row, item_code, serial_no, batch_no) {
|
||||
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
|
||||
this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
|
||||
} else if(row.serial_and_batch_bundle) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
|
||||
args: {
|
||||
bundle_id: row.serial_and_batch_bundle,
|
||||
serial_no: serial_no,
|
||||
batch_no: batch_no,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
async set_serial_no(row, serial_no) {
|
||||
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
|
||||
const existing_serial_nos = row[this.serial_no_field];
|
||||
let new_serial_nos = "";
|
||||
|
||||
get_key_for_localstorage() {
|
||||
let parts = this.frm.doc.name.split("-");
|
||||
return parts[parts.length - 1] + this.frm.doc.doctype;
|
||||
}
|
||||
|
||||
update_localstorage_scanned_data() {
|
||||
let docname = this.frm.doc.name
|
||||
if (localStorage[docname]) {
|
||||
let items = JSON.parse(localStorage[docname]);
|
||||
let existing_items = this.frm.doc.items.map(d => d.item_code);
|
||||
if (!existing_items.length) {
|
||||
localStorage.removeItem(docname);
|
||||
return;
|
||||
if (!!existing_serial_nos) {
|
||||
new_serial_nos = existing_serial_nos + "\n" + serial_no;
|
||||
} else {
|
||||
new_serial_nos = serial_no;
|
||||
}
|
||||
|
||||
for (let item_code in items) {
|
||||
if (!existing_items.includes(item_code)) {
|
||||
delete items[item_code];
|
||||
}
|
||||
}
|
||||
|
||||
localStorage[docname] = JSON.stringify(items);
|
||||
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
||||
}
|
||||
}
|
||||
|
||||
async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
|
||||
let docname = this.frm.doc.name
|
||||
|
||||
let entries = JSON.parse(localStorage.getItem(docname));
|
||||
if (!entries) {
|
||||
entries = {};
|
||||
}
|
||||
|
||||
let key = item_code;
|
||||
if (!entries[key]) {
|
||||
entries[key] = [];
|
||||
}
|
||||
|
||||
let existing_row = [];
|
||||
if (!serial_no && batch_no) {
|
||||
existing_row = entries[key].filter((e) => e.batch_no === batch_no);
|
||||
if (existing_row.length) {
|
||||
existing_row[0].qty += 1;
|
||||
}
|
||||
} else if (serial_no) {
|
||||
existing_row = entries[key].filter((e) => e.serial_no === serial_no);
|
||||
if (existing_row.length) {
|
||||
frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing_row.length) {
|
||||
entries[key].push({
|
||||
"serial_no": serial_no,
|
||||
"batch_no": batch_no,
|
||||
"qty": 1
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem(docname, JSON.stringify(entries));
|
||||
|
||||
// Auto remove from localstorage after 1 hour
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem(docname);
|
||||
}, 3600000)
|
||||
}
|
||||
|
||||
remove_item_from_localstorage() {
|
||||
let docname = this.frm.doc.name;
|
||||
if (localStorage[docname]) {
|
||||
localStorage.removeItem(docname);
|
||||
}
|
||||
}
|
||||
|
||||
async sync_bundle_data() {
|
||||
let docname = this.frm.doc.name;
|
||||
|
||||
if (localStorage[docname]) {
|
||||
let entries = JSON.parse(localStorage[docname]);
|
||||
if (entries) {
|
||||
for (let entry in entries) {
|
||||
let row = this.frm.doc.items.filter((item) => {
|
||||
if (item.item_code === entry) {
|
||||
return true;
|
||||
}
|
||||
})[0];
|
||||
|
||||
if (row) {
|
||||
this.create_serial_and_batch_bundle(row, entries, entry)
|
||||
.then(() => {
|
||||
if (!entries) {
|
||||
localStorage.removeItem(docname);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create_serial_and_batch_bundle(row, entries, key) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
|
||||
args: {
|
||||
entries: entries[key],
|
||||
child_row: row,
|
||||
doc: this.frm.doc,
|
||||
warehouse: row.warehouse,
|
||||
do_not_save: 1
|
||||
},
|
||||
callback: function(r) {
|
||||
row.serial_and_batch_bundle = r.message.name;
|
||||
delete entries[key];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async set_barcode_uom(row, uom) {
|
||||
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
|
||||
}
|
||||
}
|
||||
|
||||
async set_batch_no(row, batch_no) {
|
||||
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
|
||||
}
|
||||
}
|
||||
|
||||
async set_barcode(row, barcode) {
|
||||
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
||||
@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
}
|
||||
|
||||
async is_duplicate_serial_no(row, item_code, serial_no) {
|
||||
let is_duplicate = false;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
|
||||
is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
|
||||
if (is_duplicate) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
is_duplicate_serial_no(row, serial_no) {
|
||||
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
|
||||
|
||||
resolve(is_duplicate);
|
||||
} else if (row.serial_and_batch_bundle) {
|
||||
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
|
||||
if (r.message) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
|
||||
is_duplicate = r.message;
|
||||
resolve(is_duplicate);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
check_duplicate_serial_no_in_db(row, serial_no, response) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
|
||||
args: {
|
||||
serial_no: serial_no,
|
||||
bundle_id: row.serial_and_batch_bundle
|
||||
},
|
||||
callback(r) {
|
||||
response(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
|
||||
let docname = this.frm.doc.name
|
||||
let entries = JSON.parse(localStorage.getItem(docname));
|
||||
|
||||
if (!entries) {
|
||||
return false;
|
||||
if (is_duplicate) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
|
||||
let existing_row = [];
|
||||
if (entries[item_code]) {
|
||||
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
|
||||
}
|
||||
|
||||
return existing_row.length;
|
||||
return is_duplicate;
|
||||
}
|
||||
|
||||
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
|
||||
@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
show_alert(msg, indicator, duration=3) {
|
||||
frappe.show_alert({message: msg, indicator: indicator}, duration);
|
||||
}
|
||||
};
|
||||
};
|
@ -490,3 +490,53 @@ 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;
|
||||
}
|
@ -230,6 +230,7 @@ class Customer(TransactionBase):
|
||||
|
||||
if self.flags.is_new_doc:
|
||||
self.link_lead_address_and_contact()
|
||||
self.copy_communication()
|
||||
|
||||
self.update_customer_groups()
|
||||
|
||||
@ -287,6 +288,17 @@ 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(
|
||||
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-06-20 11:53:21",
|
||||
"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",
|
||||
"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",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@ -77,7 +77,7 @@
|
||||
"icon": "fa fa-sitemap",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-22 15:20:46.805114",
|
||||
"modified": "2024-01-30 13:57:04.951788",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Product Bundle",
|
||||
|
@ -904,6 +904,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
target.run_method("set_use_serial_batch_fields")
|
||||
|
||||
if source.company_address:
|
||||
target.update({"company_address": source.company_address})
|
||||
@ -1024,6 +1025,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
target.run_method("set_use_serial_batch_fields")
|
||||
|
||||
if source.company_address:
|
||||
target.update({"company_address": source.company_address})
|
||||
@ -1606,7 +1608,11 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"Sales Order",
|
||||
source_name,
|
||||
{
|
||||
"Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
|
||||
"Sales Order": {
|
||||
"doctype": "Pick List",
|
||||
"field_map": {"set_warehouse": "parent_warehouse"},
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
|
||||
|
@ -4,7 +4,7 @@
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:item_group_name",
|
||||
"creation": "2013-03-28 10:35:29",
|
||||
"description": "Item Classification",
|
||||
"description": "An Item Group is a way to classify items based on types.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -135,7 +135,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 3,
|
||||
"modified": "2023-10-12 13:44:13.611287",
|
||||
"modified": "2024-01-30 14:08:38.485616",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Item Group",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:sales_person_name",
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"description": "All Sales Transactions can be tagged against multiple **Sales Persons** so that you can set and monitor targets.",
|
||||
"description": "All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -145,10 +145,11 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-18 18:11:13.968024",
|
||||
"modified": "2024-01-30 13:57:26.436991",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Sales Person",
|
||||
"naming_rule": "By fieldname",
|
||||
"nsm_parent_field": "parent_sales_person",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@ -181,5 +182,6 @@
|
||||
"search_fields": "parent_sales_person",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"description": "Standard Terms and Conditions that can be added to Sales and Purchases.\n\nExamples:\n\n1. Validity of the offer.\n1. Payment Terms (In Advance, On Credit, part advance etc).\n1. What is extra (or payable by the Customer).\n1. Safety / usage warning.\n1. Warranty if any.\n1. Returns Policy.\n1. Terms of shipping, if applicable.\n1. Ways of addressing disputes, indemnity, liability, etc.\n1. Address and Contact of your Company.",
|
||||
"description": "Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -77,7 +77,7 @@
|
||||
"icon": "icon-legal",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-01 14:33:39.246532",
|
||||
"modified": "2024-01-30 12:47:52.325531",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Terms and Conditions",
|
||||
|
@ -398,6 +398,8 @@ class DeliveryNote(SellingController):
|
||||
self.check_credit_limit()
|
||||
elif self.issue_credit_note:
|
||||
self.make_return_invoice()
|
||||
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
self.update_stock_ledger()
|
||||
|
@ -189,7 +189,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
serial_nos = [
|
||||
"OSN-1",
|
||||
"OSN-2",
|
||||
@ -228,6 +227,8 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
)
|
||||
|
||||
se_doc.items[0].serial_no = "\n".join(serial_nos)
|
||||
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
se_doc.submit()
|
||||
|
||||
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
|
||||
@ -283,6 +284,8 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
self.assertFalse(serial_no in returned_serial_nos1)
|
||||
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_sales_return_for_non_bundled_items_partial(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@ -1552,7 +1555,7 @@ def create_delivery_note(**args):
|
||||
dn.return_against = args.return_against
|
||||
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
type_of_transaction = args.type_of_transaction or "Outward"
|
||||
|
||||
if dn.is_return:
|
||||
@ -1594,6 +1597,9 @@ def create_delivery_note(**args):
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"target_warehouse": args.target_warehouse,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
"serial_no": args.serial_no if args.use_serial_batch_fields else None,
|
||||
"batch_no": args.batch_no if args.use_serial_batch_fields else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -80,8 +80,11 @@
|
||||
"section_break_40",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"column_break_eaoe",
|
||||
"section_break_qyjv",
|
||||
"serial_no",
|
||||
"column_break_rxvc",
|
||||
"batch_no",
|
||||
"available_qty_section",
|
||||
"actual_batch_qty",
|
||||
@ -850,6 +853,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -859,6 +863,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
@ -874,27 +879,40 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_qyjv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rxvc",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:37:38.638144",
|
||||
"modified": "2024-02-04 14:10:31.750340",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
@ -82,6 +82,7 @@ class DeliveryNoteItem(Document):
|
||||
target_warehouse: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
weight_uom: DF.Link | None
|
||||
|
@ -3,7 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-02 16:29:48",
|
||||
"description": "Multiple Item prices.",
|
||||
"description": "Log the selling and buying rate of an Item",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@ -220,7 +220,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-15 08:26:04.041861",
|
||||
"modified": "2024-01-30 14:02:19.304854",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Price",
|
||||
|
@ -149,6 +149,13 @@ class LandedCostVoucher(Document):
|
||||
self.get("items")[item_count - 1].applicable_charges += diff
|
||||
|
||||
def validate_applicable_charges_for_item(self):
|
||||
if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher."
|
||||
)
|
||||
)
|
||||
|
||||
based_on = self.distribute_charges_based_on.lower()
|
||||
|
||||
if based_on != "distribute manually":
|
||||
|
@ -462,6 +462,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
postprocess,
|
||||
)
|
||||
|
||||
doclist.set_onload("load_after_mapping", False)
|
||||
return doclist
|
||||
|
||||
|
||||
|
@ -20,9 +20,12 @@
|
||||
"uom",
|
||||
"section_break_9",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
"serial_no",
|
||||
"use_serial_batch_fields",
|
||||
"column_break_11",
|
||||
"serial_and_batch_bundle",
|
||||
"section_break_bgys",
|
||||
"serial_no",
|
||||
"column_break_qlha",
|
||||
"batch_no",
|
||||
"actual_batch_qty",
|
||||
"section_break_13",
|
||||
@ -118,10 +121,10 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@ -131,8 +134,7 @@
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
@ -259,6 +261,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -267,16 +270,32 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_bgys",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qlha",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-28 13:16:38.460806",
|
||||
"modified": "2024-02-04 16:30:44.263964",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
|
@ -47,6 +47,7 @@ class PackedItem(Document):
|
||||
serial_no: DF.Text | None
|
||||
target_warehouse: DF.Link | None
|
||||
uom: DF.Link | None
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', {
|
||||
frm.set_query('parent_warehouse', () => {
|
||||
return {
|
||||
filters: {
|
||||
'is_group': 1,
|
||||
'company': frm.doc.company
|
||||
}
|
||||
};
|
||||
|
@ -51,7 +51,7 @@
|
||||
"description": "Items under this warehouse will be suggested",
|
||||
"fieldname": "parent_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Parent Warehouse",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@ -188,7 +188,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-24 10:33:43.244476",
|
||||
"modified": "2024-02-01 16:17:44.877426",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||
from frappe.utils import cint, floor, flt
|
||||
from frappe.utils import ceil, cint, floor, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
@ -122,11 +122,42 @@ class PickList(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_serial_and_batch_bundle()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_status()
|
||||
self.update_bundle_picked_qty()
|
||||
self.update_reference_qty()
|
||||
self.update_sales_order_picking_status()
|
||||
|
||||
def make_bundle_using_old_serial_batch_fields(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for row in self.locations:
|
||||
if not row.serial_no and not row.batch_no:
|
||||
continue
|
||||
|
||||
if not row.use_serial_batch_fields and (row.serial_no or row.batch_no):
|
||||
frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
|
||||
|
||||
if row.use_serial_batch_fields and (not row.serial_and_batch_bundle):
|
||||
sn_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"qty": row.stock_qty,
|
||||
"type_of_transaction": "Outward",
|
||||
"company": self.company,
|
||||
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
|
||||
"batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
|
||||
"batch_no": row.batch_no,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
row.serial_and_batch_bundle = sn_doc.name
|
||||
row.db_set("serial_and_batch_bundle", sn_doc.name)
|
||||
|
||||
def on_update_after_submit(self) -> None:
|
||||
if self.has_reserved_stock():
|
||||
msg = _(
|
||||
@ -156,6 +187,7 @@ class PickList(Document):
|
||||
{"is_cancelled": 1, "voucher_no": ""},
|
||||
)
|
||||
|
||||
frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
|
||||
def on_update(self):
|
||||
@ -324,7 +356,6 @@ class PickList(Document):
|
||||
locations_replica = self.get("locations")
|
||||
|
||||
# reset
|
||||
self.remove_serial_and_batch_bundle()
|
||||
self.delete_key("locations")
|
||||
updated_locations = frappe._dict()
|
||||
for item_doc in items:
|
||||
@ -639,13 +670,19 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
||||
if not stock_qty:
|
||||
break
|
||||
|
||||
serial_nos = None
|
||||
if item_location.serial_nos:
|
||||
serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)])
|
||||
|
||||
locations.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": qty,
|
||||
"stock_qty": stock_qty,
|
||||
"warehouse": item_location.warehouse,
|
||||
"serial_and_batch_bundle": item_location.serial_and_batch_bundle,
|
||||
"serial_no": serial_nos,
|
||||
"batch_no": item_location.batch_no,
|
||||
"use_serial_batch_fields": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -681,7 +718,15 @@ def get_available_item_locations(
|
||||
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
|
||||
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
|
||||
|
||||
if has_serial_no:
|
||||
if has_batch_no and has_serial_no:
|
||||
locations = get_available_item_locations_for_serial_and_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
)
|
||||
elif has_serial_no:
|
||||
locations = get_available_item_locations_for_serialized_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
||||
)
|
||||
@ -724,6 +769,47 @@ def get_available_item_locations(
|
||||
return locations
|
||||
|
||||
|
||||
def get_available_item_locations_for_serial_and_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
):
|
||||
# Get batch nos by FIFO
|
||||
locations = get_available_item_locations_for_batched_item(
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
)
|
||||
|
||||
if locations:
|
||||
sn = frappe.qb.DocType("Serial No")
|
||||
conditions = (sn.item_code == item_code) & (sn.company == company)
|
||||
|
||||
for location in locations:
|
||||
location.qty = (
|
||||
required_qty if location.qty > required_qty else location.qty
|
||||
) # if extra qty in batch
|
||||
|
||||
serial_nos = (
|
||||
frappe.qb.from_(sn)
|
||||
.select(sn.name)
|
||||
.where(
|
||||
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
|
||||
)
|
||||
.orderby(sn.creation)
|
||||
.limit(ceil(location.qty + total_picked_qty))
|
||||
).run(as_dict=True)
|
||||
|
||||
serial_nos = [sn.name for sn in serial_nos]
|
||||
location.serial_nos = serial_nos
|
||||
location.qty = len(serial_nos)
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def get_available_item_locations_for_serialized_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
):
|
||||
@ -757,28 +843,16 @@ def get_available_item_locations_for_serialized_item(
|
||||
picked_qty -= 1
|
||||
|
||||
locations = []
|
||||
|
||||
for warehouse, serial_nos in warehouse_serial_nos_map.items():
|
||||
qty = len(serial_nos)
|
||||
|
||||
bundle_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"voucher_type": "Pick List",
|
||||
"total_qty": qty * -1,
|
||||
"serial_nos": serial_nos,
|
||||
"type_of_transaction": "Outward",
|
||||
"company": company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
locations.append(
|
||||
{
|
||||
"qty": qty,
|
||||
"warehouse": warehouse,
|
||||
"item_code": item_code,
|
||||
"serial_and_batch_bundle": bundle_doc.name,
|
||||
"serial_nos": serial_nos,
|
||||
}
|
||||
)
|
||||
|
||||
@ -808,29 +882,17 @@ def get_available_item_locations_for_batched_item(
|
||||
warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
|
||||
|
||||
for warehouse, batches in warehouse_wise_batches.items():
|
||||
qty = sum(batches.values())
|
||||
|
||||
bundle_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"voucher_type": "Pick List",
|
||||
"total_qty": qty * -1,
|
||||
"batches": batches,
|
||||
"type_of_transaction": "Outward",
|
||||
"company": company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
locations.append(
|
||||
{
|
||||
"qty": qty,
|
||||
"warehouse": warehouse,
|
||||
"item_code": item_code,
|
||||
"serial_and_batch_bundle": bundle_doc.name,
|
||||
}
|
||||
)
|
||||
for batch_no, qty in batches.items():
|
||||
locations.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": qty,
|
||||
"warehouse": warehouse,
|
||||
"item_code": item_code,
|
||||
"batch_no": batch_no,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return locations
|
||||
|
||||
|
@ -217,6 +217,8 @@ class TestPickList(FrappeTestCase):
|
||||
)
|
||||
|
||||
pick_list.save()
|
||||
pick_list.submit()
|
||||
|
||||
self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
|
||||
self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
|
||||
self.assertEqual(pick_list.locations[0].qty, 5)
|
||||
@ -239,7 +241,7 @@ class TestPickList(FrappeTestCase):
|
||||
pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
|
||||
|
||||
pr1.load_from_db()
|
||||
oldest_batch_no = pr1.items[0].batch_no
|
||||
oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
|
||||
|
||||
pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)
|
||||
|
||||
@ -302,6 +304,8 @@ class TestPickList(FrappeTestCase):
|
||||
}
|
||||
)
|
||||
pick_list.set_item_locations()
|
||||
pick_list.submit()
|
||||
pick_list.reload()
|
||||
|
||||
self.assertEqual(
|
||||
get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
|
||||
@ -310,6 +314,7 @@ class TestPickList(FrappeTestCase):
|
||||
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
|
||||
)
|
||||
|
||||
pick_list.cancel()
|
||||
pr1.cancel()
|
||||
pr2.cancel()
|
||||
|
||||
@ -671,29 +676,22 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
pl.submit()
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 25.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["qty", "batch_no"],
|
||||
filters={"parent": loc.serial_and_batch_bundle},
|
||||
)
|
||||
|
||||
for d in data:
|
||||
self.assertEqual(d.batch_no, "PICKLT-000001")
|
||||
self.assertEqual(d.qty, 25.0 * -1)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
||||
pl = create_pick_list(so1.name)
|
||||
pl1 = create_pick_list(so1.name)
|
||||
pl1.submit()
|
||||
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.qty, 10.0)
|
||||
for loc in pl1.locations:
|
||||
self.assertEqual(loc.qty, 5.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
data = frappe.get_all(
|
||||
@ -709,8 +707,7 @@ class TestPickList(FrappeTestCase):
|
||||
elif d.batch_no == "PICKLT-000002":
|
||||
self.assertEqual(d.qty, 5.0 * -1)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
pl1.cancel()
|
||||
pl.cancel()
|
||||
|
||||
def test_picklist_for_serial_item(self):
|
||||
@ -723,6 +720,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
pl.submit()
|
||||
picked_serial_nos = []
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
@ -736,13 +734,11 @@ class TestPickList(FrappeTestCase):
|
||||
picked_serial_nos = [d.serial_no for d in data]
|
||||
self.assertEqual(len(picked_serial_nos), 25)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
||||
pl = create_pick_list(so1.name)
|
||||
pl1 = create_pick_list(so1.name)
|
||||
pl1.submit()
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
for loc in pl1.locations:
|
||||
self.assertEqual(loc.qty, 10.0)
|
||||
self.assertTrue(loc.serial_and_batch_bundle)
|
||||
|
||||
@ -756,8 +752,7 @@ class TestPickList(FrappeTestCase):
|
||||
for d in data:
|
||||
self.assertTrue(d.serial_no not in picked_serial_nos)
|
||||
|
||||
pl.save()
|
||||
pl.submit()
|
||||
pl1.cancel()
|
||||
pl.cancel()
|
||||
|
||||
def test_picklist_with_bundles(self):
|
||||
|
@ -24,8 +24,11 @@
|
||||
"serial_no_and_batch_section",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
"serial_no",
|
||||
"use_serial_batch_fields",
|
||||
"column_break_20",
|
||||
"section_break_ecxc",
|
||||
"serial_no",
|
||||
"column_break_belw",
|
||||
"batch_no",
|
||||
"column_break_15",
|
||||
"sales_order",
|
||||
@ -72,19 +75,17 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "serial_no",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "batch_no",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@ -195,6 +196,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -204,6 +206,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
@ -218,11 +221,26 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_ecxc",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_belw",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-26 12:54:15.785962",
|
||||
"modified": "2024-02-04 16:12:16.257951",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
|
@ -37,6 +37,7 @@ class PickListItem(Document):
|
||||
stock_reserved_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
uom: DF.Link | None
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
@ -1,434 +1,134 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:price_list_name",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-25 11:35:09",
|
||||
"custom": 0,
|
||||
"description": "Price List Master",
|
||||
"docstatus": 0,
|
||||
"description": "A Price List is a collection of Item Prices either Selling, Buying, or both",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"sb_1",
|
||||
"price_list_name",
|
||||
"currency",
|
||||
"buying",
|
||||
"selling",
|
||||
"price_not_uom_dependent",
|
||||
"column_break_3",
|
||||
"countries"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Enabled",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "sb_1",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "price_list_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Price List Name",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "price_list_name",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Currency",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Currency",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "buying",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Buying",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Buying"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "selling",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Selling",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Selling"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"default": "0",
|
||||
"fieldname": "price_not_uom_dependent",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Price Not UOM Dependent",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Price Not UOM Dependent"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "countries",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Applicable for Countries",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Price List Country",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Price List Country"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-tags",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2019-06-24 17:16:28.027302",
|
||||
"modified": "2024-01-30 14:39:26.328837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Price List",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 0,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 1,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Master Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Master Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Manufacturing User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"role": "Manufacturing User"
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"search_fields": "currency",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"states": []
|
||||
}
|
@ -368,6 +368,7 @@ class PurchaseReceipt(BuyingController):
|
||||
else:
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
||||
# depends upon updated ordered qty in PO
|
||||
@ -1358,16 +1359,16 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
for lcv in landed_cost_vouchers:
|
||||
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
|
||||
|
||||
based_on_field = None
|
||||
# Use amount field for total item cost for manually cost distributed LCVs
|
||||
if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
|
||||
based_on_field = "amount"
|
||||
else:
|
||||
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
|
||||
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
|
||||
|
||||
total_item_cost = 0
|
||||
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
total_item_cost += item.get(based_on_field)
|
||||
if based_on_field:
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
total_item_cost += item.get(based_on_field)
|
||||
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
if item.receipt_document == purchase_document:
|
||||
|
@ -2193,6 +2193,93 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr_doc.reload()
|
||||
self.assertFalse(pr_doc.items[0].from_warehouse)
|
||||
|
||||
def test_use_serial_batch_fields_for_serial_nos(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
|
||||
item_code = make_item(
|
||||
"_Test Use Serial Fields Item Serial Item",
|
||||
properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"},
|
||||
).name
|
||||
|
||||
serial_nos = [
|
||||
"SNU-TSFISI-000011",
|
||||
"SNU-TSFISI-000012",
|
||||
"SNU-TSFISI-000013",
|
||||
"SNU-TSFISI-000014",
|
||||
"SNU-TSFISI-000015",
|
||||
]
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
serial_no="\n".join(serial_nos),
|
||||
use_serial_batch_fields=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.items[0].use_serial_batch_fields, 1)
|
||||
self.assertFalse(pr.items[0].serial_no)
|
||||
self.assertTrue(pr.items[0].serial_and_batch_bundle)
|
||||
|
||||
sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle)
|
||||
|
||||
for row in sbb_doc.entries:
|
||||
self.assertTrue(row.serial_no in serial_nos)
|
||||
|
||||
serial_nos.remove("SNU-TSFISI-000015")
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
serial_no="\n".join(serial_nos),
|
||||
qty=4,
|
||||
warehouse=pr.items[0].warehouse,
|
||||
use_serial_batch_fields=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
sr.reload()
|
||||
|
||||
serial_nos = get_serial_nos(sr.items[0].current_serial_no)
|
||||
self.assertEqual(len(serial_nos), 5)
|
||||
self.assertEqual(sr.items[0].current_qty, 5)
|
||||
|
||||
new_serial_nos = get_serial_nos(sr.items[0].serial_no)
|
||||
self.assertEqual(len(new_serial_nos), 4)
|
||||
self.assertEqual(sr.items[0].qty, 4)
|
||||
self.assertEqual(sr.items[0].use_serial_batch_fields, 1)
|
||||
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
|
||||
self.assertFalse(sr.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(sr.items[0].current_serial_no)
|
||||
sr.submit()
|
||||
|
||||
sr.reload()
|
||||
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
|
||||
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
||||
|
||||
serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status")
|
||||
|
||||
self.assertTrue(serial_no_status != "Active")
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code,
|
||||
qty=4,
|
||||
serial_no="\n".join(new_serial_nos),
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
self.assertTrue(dn.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(dn.items[0].qty, 4)
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle)
|
||||
for row in doc.entries:
|
||||
self.assertTrue(row.serial_no in new_serial_nos)
|
||||
|
||||
for sn in new_serial_nos:
|
||||
serial_no_status = frappe.db.get_value("Serial No", sn, "status")
|
||||
self.assertTrue(serial_no_status != "Active")
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
@ -2361,7 +2448,7 @@ def make_purchase_receipt(**args):
|
||||
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
|
||||
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
batches = {}
|
||||
if args.get("batch_no"):
|
||||
batches = frappe._dict({args.batch_no: qty})
|
||||
@ -2403,6 +2490,9 @@ def make_purchase_receipt(**args):
|
||||
"cost_center": args.cost_center
|
||||
or frappe.get_cached_value("Company", pr.company, "cost_center"),
|
||||
"asset_location": args.location or "Test Location",
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields or 0,
|
||||
"serial_no": args.serial_no if args.use_serial_batch_fields else "",
|
||||
"batch_no": args.batch_no if args.use_serial_batch_fields else "",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -94,6 +94,7 @@
|
||||
"section_break_45",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"add_serial_batch_for_rejected_qty",
|
||||
"rejected_serial_and_batch_bundle",
|
||||
@ -1003,6 +1004,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@ -1020,24 +1022,22 @@
|
||||
{
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "rejected_serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Rejected Serial No",
|
||||
"read_only": 1
|
||||
"label": "Rejected Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "rejected_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@ -1045,11 +1045,13 @@
|
||||
"options": "Serial and Batch Bundle"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_3vxt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@ -1058,6 +1060,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
@ -1098,12 +1101,18 @@
|
||||
"read_only": 1,
|
||||
"report_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-25 22:32:09.801965",
|
||||
"modified": "2024-02-04 11:48:06.653771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document):
|
||||
supplier_part_no: DF.Data | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
|
@ -286,6 +286,7 @@ def repost(doc):
|
||||
repost_gl_entries(doc)
|
||||
|
||||
doc.set_status("Completed")
|
||||
remove_attached_file(doc.name)
|
||||
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
@ -314,6 +315,13 @@ def repost(doc):
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def remove_attached_file(docname):
|
||||
if file_name := frappe.db.get_value(
|
||||
"File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
|
||||
):
|
||||
frappe.delete_doc("File", file_name, delete_permanently=True)
|
||||
|
||||
|
||||
def repost_sl_entries(doc):
|
||||
if doc.based_on == "Transaction":
|
||||
repost_future_sle(
|
||||
|
@ -424,3 +424,38 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, riv.save)
|
||||
doc.cancel()
|
||||
|
||||
def test_remove_attached_file(self):
|
||||
item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1})
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
rate=100,
|
||||
posting_date=add_days(today(), days=-1),
|
||||
)
|
||||
|
||||
if docname := frappe.db.exists("Repost Item Valuation", {"voucher_no": pr1.name}):
|
||||
self.assertFalse(
|
||||
frappe.db.get_value(
|
||||
"File",
|
||||
{"attached_to_doctype": "Repost Item Valuation", "attached_to_name": docname},
|
||||
"name",
|
||||
)
|
||||
)
|
||||
else:
|
||||
repost_entries = create_item_wise_repost_entries(pr1.doctype, pr1.name)
|
||||
for entry in repost_entries:
|
||||
self.assertFalse(
|
||||
frappe.db.get_value(
|
||||
"File",
|
||||
{"attached_to_doctype": "Repost Item Valuation", "attached_to_name": entry.name},
|
||||
"name",
|
||||
)
|
||||
)
|
||||
|
@ -1117,7 +1117,7 @@ def parse_serial_nos(data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
|
||||
return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()]
|
||||
return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers(
|
||||
|
||||
|
||||
def get_type_of_transaction(parent_doc, child_row):
|
||||
type_of_transaction = child_row.type_of_transaction
|
||||
type_of_transaction = child_row.get("type_of_transaction")
|
||||
if parent_doc.get("doctype") == "Stock Entry":
|
||||
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
|
||||
|
||||
@ -1384,6 +1384,8 @@ def get_available_serial_nos(kwargs):
|
||||
|
||||
filters = {"item_code": kwargs.item_code}
|
||||
|
||||
# ignore_warehouse is used for backdated stock transactions
|
||||
# There might be chances that the serial no not exists in the warehouse during backdated stock transactions
|
||||
if not kwargs.get("ignore_warehouse"):
|
||||
filters["warehouse"] = ("is", "set")
|
||||
if kwargs.warehouse:
|
||||
@ -1677,7 +1679,10 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
|
||||
query = query.where(sb_entry.batch_no == kwargs.batch_no)
|
||||
|
||||
if kwargs.warehouse:
|
||||
query = query.where(sre.warehouse == kwargs.warehouse)
|
||||
if isinstance(kwargs.warehouse, list):
|
||||
query = query.where(sre.warehouse.isin(kwargs.warehouse))
|
||||
else:
|
||||
query = query.where(sre.warehouse == kwargs.warehouse)
|
||||
|
||||
if kwargs.ignore_voucher_nos:
|
||||
query = query.where(sre.name.notin(kwargs.ignore_voucher_nos))
|
||||
|
@ -136,6 +136,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
|
||||
def test_old_batch_valuation(self):
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
batch_item_code = "Old Batch Item Valuation 1"
|
||||
make_item(
|
||||
batch_item_code,
|
||||
@ -240,6 +241,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
bundle_doc.submit()
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_old_serial_no_valuation(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
@ -259,6 +261,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
|
||||
serial_no_id = "Old Serial No 1"
|
||||
if not frappe.db.exists("Serial No", serial_no_id):
|
||||
@ -320,6 +323,9 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
for row in bundle_doc.entries:
|
||||
self.assertEqual(flt(row.stock_value_difference, 2), -100.00)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_batch_not_belong_to_serial_no(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
@ -151,9 +151,7 @@ def get_serial_nos(serial_no):
|
||||
if isinstance(serial_no, list):
|
||||
return serial_no
|
||||
|
||||
return [
|
||||
s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
|
||||
]
|
||||
return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
|
||||
|
||||
|
||||
def clean_serial_no_string(serial_no: str) -> str:
|
||||
|
@ -274,6 +274,7 @@ class StockEntry(StockController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
self.update_work_order()
|
||||
self.validate_subcontract_order()
|
||||
|
@ -92,6 +92,9 @@ def make_stock_entry(**args):
|
||||
else:
|
||||
args.qty = cint(args.qty)
|
||||
|
||||
if args.serial_no or args.batch_no:
|
||||
args.use_serial_batch_fields = True
|
||||
|
||||
# purpose
|
||||
if not args.purpose:
|
||||
if args.source and args.target:
|
||||
@ -162,6 +165,7 @@ def make_stock_entry(**args):
|
||||
)
|
||||
|
||||
args.serial_no = serial_number
|
||||
|
||||
s.append(
|
||||
"items",
|
||||
{
|
||||
@ -177,6 +181,7 @@ def make_stock_entry(**args):
|
||||
"batch_no": args.batch_no,
|
||||
"cost_center": args.cost_center,
|
||||
"expense_account": args.expense_account,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
},
|
||||
)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user