chore: rebase
This commit is contained in:
commit
2d78dba66f
12
.github/workflows/server-tests-mariadb.yml
vendored
12
.github/workflows/server-tests-mariadb.yml
vendored
@ -31,6 +31,9 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
NODE_ENV: "production"
|
||||||
|
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -117,11 +120,11 @@ jobs:
|
|||||||
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --with-coverage --total-builds 4 --build-number ${{ matrix.container }}'
|
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 4 --build-number ${{ matrix.container }}'
|
||||||
env:
|
env:
|
||||||
TYPE: server
|
TYPE: server
|
||||||
CI_BUILD_ID: ${{ github.run_id }}
|
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
|
||||||
|
|
||||||
- name: Show bench output
|
- name: Show bench output
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
@ -129,6 +132,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload coverage data
|
- name: Upload coverage data
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.container }}
|
name: coverage-${{ matrix.container }}
|
||||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||||
@ -137,6 +141,7 @@ jobs:
|
|||||||
name: Coverage Wrap Up
|
name: Coverage Wrap Up
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -148,5 +153,6 @@ jobs:
|
|||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v2
|
||||||
with:
|
with:
|
||||||
name: MariaDB
|
name: MariaDB
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
@ -7,8 +7,7 @@
|
|||||||
<p>ERP made simple</p>
|
<p>ERP made simple</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||||
[](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
|
|
||||||
[](https://www.codetriage.com/frappe/erpnext)
|
[](https://www.codetriage.com/frappe/erpnext)
|
||||||
[](https://codecov.io/gh/frappe/erpnext)
|
[](https://codecov.io/gh/frappe/erpnext)
|
||||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||||
|
@ -118,6 +118,7 @@ class Account(NestedSet):
|
|||||||
self.validate_balance_must_be_debit_or_credit()
|
self.validate_balance_must_be_debit_or_credit()
|
||||||
self.validate_account_currency()
|
self.validate_account_currency()
|
||||||
self.validate_root_company_and_sync_account_to_children()
|
self.validate_root_company_and_sync_account_to_children()
|
||||||
|
self.validate_receivable_payable_account_type()
|
||||||
|
|
||||||
def validate_parent_child_account_type(self):
|
def validate_parent_child_account_type(self):
|
||||||
if self.parent_account:
|
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"
|
"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):
|
def validate_root_details(self):
|
||||||
doc_before_save = self.get_doc_before_save()
|
doc_before_save = self.get_doc_before_save()
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.account import (
|
from erpnext.accounts.doctype.account.account import (
|
||||||
InvalidAccountMergeError,
|
InvalidAccountMergeError,
|
||||||
@ -324,6 +325,19 @@ class TestAccount(unittest.TestCase):
|
|||||||
acc.account_currency = "USD"
|
acc.account_currency = "USD"
|
||||||
self.assertRaises(frappe.ValidationError, acc.save)
|
self.assertRaises(frappe.ValidationError, acc.save)
|
||||||
|
|
||||||
|
def test_account_balance(self):
|
||||||
|
from erpnext.accounts.utils import get_balance_on
|
||||||
|
|
||||||
|
if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"):
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.account_name = "Test Percent Account %5"
|
||||||
|
acc.parent_account = "Tax Assets - _TC"
|
||||||
|
acc.company = "_Test Company"
|
||||||
|
acc.insert()
|
||||||
|
|
||||||
|
balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate())
|
||||||
|
self.assertEqual(balance, 0)
|
||||||
|
|
||||||
|
|
||||||
def _make_test_records(verbose=None):
|
def _make_test_records(verbose=None):
|
||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
|
|||||||
load_address_and_contact,
|
load_address_and_contact,
|
||||||
)
|
)
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import comma_and, get_link_to_form
|
||||||
|
|
||||||
|
|
||||||
class BankAccount(Document):
|
class BankAccount(Document):
|
||||||
@ -52,6 +53,17 @@ class BankAccount(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_company()
|
self.validate_company()
|
||||||
self.validate_iban()
|
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):
|
def validate_company(self):
|
||||||
if self.is_company_account and not self.company:
|
if self.is_company_account and not self.company:
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, fmt_money, getdate
|
from frappe.utils import flt, fmt_money, getdate
|
||||||
|
from pypika import Order
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
|
||||||
@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
|
|
||||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||||
if include_pos_transactions:
|
if include_pos_transactions:
|
||||||
pos_sales_invoices = frappe.db.sql(
|
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||||
"""
|
si = frappe.qb.DocType("Sales Invoice")
|
||||||
select
|
acc = frappe.qb.DocType("Account")
|
||||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
|
||||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
|
||||||
account.account_currency, 0 as credit
|
|
||||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
|
||||||
where
|
|
||||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
|
||||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
|
||||||
order by
|
|
||||||
si.posting_date ASC, si.name DESC
|
|
||||||
""",
|
|
||||||
{"account": account, "from": from_date, "to": to_date},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
pos_purchase_invoices = frappe.db.sql(
|
pos_sales_invoices = (
|
||||||
"""
|
frappe.qb.from_(si_payment)
|
||||||
select
|
.inner_join(si)
|
||||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
.on(si_payment.parent == si.name)
|
||||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
.inner_join(acc)
|
||||||
account.account_currency, 0 as debit
|
.on(si_payment.account == acc.name)
|
||||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
.select(
|
||||||
where
|
ConstantColumn("Sales Invoice").as_("payment_document"),
|
||||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
si.name.as_("payment_entry"),
|
||||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
si_payment.reference_no.as_("cheque_number"),
|
||||||
order by
|
si_payment.amount.as_("debit"),
|
||||||
pi.posting_date ASC, pi.name DESC
|
si.posting_date,
|
||||||
""",
|
si.customer.as_("against_account"),
|
||||||
{"account": account, "from": from_date, "to": to_date},
|
si_payment.clearance_date,
|
||||||
as_dict=1,
|
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 = (
|
entries = (
|
||||||
list(payment_entries)
|
list(payment_entries)
|
||||||
|
@ -80,7 +80,8 @@ class BankStatementImport(DataImport):
|
|||||||
from frappe.utils.background_jobs import is_job_enqueued
|
from frappe.utils.background_jobs import is_job_enqueued
|
||||||
from frappe.utils.scheduler import is_scheduler_inactive
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
|
|
||||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||||
|
if is_scheduler_inactive() and not run_now:
|
||||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||||
|
|
||||||
job_id = f"bank_statement_import::{self.name}"
|
job_id = f"bank_statement_import::{self.name}"
|
||||||
@ -97,7 +98,7 @@ class BankStatementImport(DataImport):
|
|||||||
google_sheets_url=self.google_sheets_url,
|
google_sheets_url=self.google_sheets_url,
|
||||||
bank=self.bank,
|
bank=self.bank,
|
||||||
template_options=self.template_options,
|
template_options=self.template_options,
|
||||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
now=run_now,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
|
|||||||
frappe.db.delete(dt)
|
frappe.db.delete(dt)
|
||||||
clear_loan_transactions()
|
clear_loan_transactions()
|
||||||
make_pos_profile()
|
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.
|
# 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):
|
def test_linked_payments(self):
|
||||||
@ -219,7 +227,9 @@ def clear_loan_transactions():
|
|||||||
frappe.db.delete("Loan Repayment")
|
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:
|
try:
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
frappe.get_doc(
|
bank_account = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Bank Account",
|
"doctype": "Bank Account",
|
||||||
"account_name": "Checking Account",
|
"account_name": bank_account_name,
|
||||||
"bank": bank_name,
|
"bank": bank_name,
|
||||||
"account": account_name,
|
"account": gl_account,
|
||||||
}
|
}
|
||||||
).insert(ignore_if_duplicate=True)
|
).insert(ignore_if_duplicate=True)
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
pass
|
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(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Bank Transaction",
|
"doctype": "Bank Transaction",
|
||||||
@ -253,7 +277,7 @@ def add_transactions():
|
|||||||
"date": "2018-10-23",
|
"date": "2018-10-23",
|
||||||
"deposit": 1200,
|
"deposit": 1200,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"bank_account": "Checking Account - Citi Bank",
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
@ -265,7 +289,7 @@ def add_transactions():
|
|||||||
"date": "2018-10-23",
|
"date": "2018-10-23",
|
||||||
"deposit": 1700,
|
"deposit": 1700,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"bank_account": "Checking Account - Citi Bank",
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
@ -277,7 +301,7 @@ def add_transactions():
|
|||||||
"date": "2018-10-26",
|
"date": "2018-10-26",
|
||||||
"withdrawal": 690,
|
"withdrawal": 690,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"bank_account": "Checking Account - Citi Bank",
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
@ -289,7 +313,7 @@ def add_transactions():
|
|||||||
"date": "2018-10-27",
|
"date": "2018-10-27",
|
||||||
"deposit": 3900,
|
"deposit": 3900,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"bank_account": "Checking Account - Citi Bank",
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
@ -301,13 +325,13 @@ def add_transactions():
|
|||||||
"date": "2018-10-27",
|
"date": "2018-10-27",
|
||||||
"withdrawal": 109080,
|
"withdrawal": 109080,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"bank_account": "Checking Account - Citi Bank",
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
|
|
||||||
def add_vouchers():
|
def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||||
try:
|
try:
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
@ -323,7 +347,7 @@ def add_vouchers():
|
|||||||
|
|
||||||
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
|
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_no = "Conrad Oct 18"
|
||||||
pe.reference_date = "2018-10-24"
|
pe.reference_date = "2018-10-24"
|
||||||
pe.insert()
|
pe.insert()
|
||||||
@ -342,14 +366,14 @@ def add_vouchers():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
|
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_no = "Herr G Oct 18"
|
||||||
pe.reference_date = "2018-10-24"
|
pe.reference_date = "2018-10-24"
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
|
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_no = "Herr G Nov 18"
|
||||||
pe.reference_date = "2018-11-01"
|
pe.reference_date = "2018-11-01"
|
||||||
pe.insert()
|
pe.insert()
|
||||||
@ -380,10 +404,10 @@ def add_vouchers():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
|
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.insert()
|
||||||
pi.submit()
|
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_no = "Poore Simon's Oct 18"
|
||||||
pe.reference_date = "2018-10-28"
|
pe.reference_date = "2018-10-28"
|
||||||
pe.paid_amount = 690
|
pe.paid_amount = 690
|
||||||
@ -392,7 +416,7 @@ def add_vouchers():
|
|||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
|
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_no = "Poore Simon's Oct 18"
|
||||||
pe.reference_date = "2018-10-28"
|
pe.reference_date = "2018-10-28"
|
||||||
pe.insert()
|
pe.insert()
|
||||||
@ -415,16 +439,12 @@ def add_vouchers():
|
|||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||||
):
|
):
|
||||||
mode_of_payment.append(
|
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||||
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
|
|
||||||
)
|
|
||||||
mode_of_payment.save()
|
mode_of_payment.save()
|
||||||
|
|
||||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||||
si.is_pos = 1
|
si.is_pos = 1
|
||||||
si.append(
|
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
|
|
||||||
)
|
|
||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
@ -13,16 +13,9 @@ import erpnext
|
|||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_checks_for_pl_and_bs_accounts,
|
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.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||||
from erpnext.exceptions import (
|
from erpnext.exceptions import InvalidAccountCurrency
|
||||||
InvalidAccountCurrency,
|
|
||||||
InvalidAccountDimensionError,
|
|
||||||
MandatoryAccountDimensionError,
|
|
||||||
)
|
|
||||||
|
|
||||||
exclude_from_linked_with = True
|
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":
|
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||||
self.validate_account_details(adv_adj)
|
self.validate_account_details(adv_adj)
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
self.validate_allowed_dimensions()
|
|
||||||
validate_balance_type(self.account, adv_adj)
|
validate_balance_type(self.account, adv_adj)
|
||||||
validate_frozen_account(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):
|
def check_pl_account(self):
|
||||||
if (
|
if (
|
||||||
self.is_opening == "Yes"
|
self.is_opening == "Yes"
|
||||||
|
@ -1167,7 +1167,9 @@ class JournalEntry(AccountsController):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
def get_default_bank_cash_account(
|
||||||
|
company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
|
||||||
|
):
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||||
|
|
||||||
if mode_of_payment:
|
if mode_of_payment:
|
||||||
@ -1205,7 +1207,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
|
|||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
{
|
{
|
||||||
"account": account,
|
"account": account,
|
||||||
"balance": get_balance_on(account),
|
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
|
||||||
"account_currency": account_details.account_currency,
|
"account_currency": account_details.account_currency,
|
||||||
"account_type": account_details.account_type,
|
"account_type": account_details.account_type,
|
||||||
}
|
}
|
||||||
|
@ -2220,6 +2220,7 @@ def get_payment_entry(
|
|||||||
party_type=None,
|
party_type=None,
|
||||||
payment_type=None,
|
payment_type=None,
|
||||||
reference_date=None,
|
reference_date=None,
|
||||||
|
ignore_permissions=False,
|
||||||
):
|
):
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||||
@ -2242,14 +2243,14 @@ def get_payment_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# bank or cash
|
# bank or cash
|
||||||
bank = get_bank_cash_account(doc, bank_account)
|
bank = get_bank_cash_account(doc, bank_account, ignore_permissions=ignore_permissions)
|
||||||
|
|
||||||
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
|
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
|
||||||
if party_type in ["Customer", "Supplier"] and not bank:
|
if party_type in ["Customer", "Supplier"] and not bank:
|
||||||
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
|
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
|
||||||
if party_bank_account:
|
if party_bank_account:
|
||||||
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
|
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
|
||||||
bank = get_bank_cash_account(doc, account)
|
bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
|
||||||
|
|
||||||
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
||||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||||
@ -2389,9 +2390,13 @@ def update_accounting_dimensions(pe, doc):
|
|||||||
pe.set(dimension, doc.get(dimension))
|
pe.set(dimension, doc.get(dimension))
|
||||||
|
|
||||||
|
|
||||||
def get_bank_cash_account(doc, bank_account):
|
def get_bank_cash_account(doc, bank_account, ignore_permissions=False):
|
||||||
bank = get_default_bank_cash_account(
|
bank = get_default_bank_cash_account(
|
||||||
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
|
doc.company,
|
||||||
|
"Bank",
|
||||||
|
mode_of_payment=doc.get("mode_of_payment"),
|
||||||
|
account=bank_account,
|
||||||
|
ignore_permissions=ignore_permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not bank:
|
if not bank:
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import getdate
|
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 (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
make_payment_order,
|
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
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentOrder(unittest.TestCase):
|
class TestPaymentOrder(FrappeTestCase):
|
||||||
def setUp(self):
|
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):
|
def tearDown(self):
|
||||||
for bt in frappe.get_all("Payment Order"):
|
frappe.db.rollback()
|
||||||
doc = frappe.get_doc("Payment Order", bt.name)
|
|
||||||
doc.cancel()
|
|
||||||
doc.delete()
|
|
||||||
|
|
||||||
def test_payment_order_creation_against_payment_entry(self):
|
def test_payment_order_creation_against_payment_entry(self):
|
||||||
purchase_invoice = make_purchase_invoice()
|
purchase_invoice = make_purchase_invoice()
|
||||||
payment_entry = get_payment_entry(
|
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_no = "_Test_Payment_Order"
|
||||||
payment_entry.reference_date = getdate()
|
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.insert()
|
||||||
payment_entry.submit()
|
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]
|
reference_doc = doc.get("references")[0]
|
||||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||||
@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
|
|||||||
self.assertEqual(reference_doc.amount, 250)
|
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(
|
payment_order = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Payment Order",
|
doctype="Payment Order",
|
||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
payment_order_type=order_type,
|
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)
|
doc = make_payment_order(ref_doc.name, payment_order)
|
||||||
|
@ -591,6 +591,70 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(si.status, "Paid")
|
self.assertEqual(si.status, "Paid")
|
||||||
self.assertEqual(si.outstanding_amount, 0)
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
|
||||||
|
def test_invoice_status_after_cr_note_cancellation(self):
|
||||||
|
# This test case is made after the 'always standalone Credit/Debit notes' feature is introduced
|
||||||
|
transaction_date = nowdate()
|
||||||
|
amount = 100
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||||
|
|
||||||
|
cr_note = self.create_sales_invoice(
|
||||||
|
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
cr_note.is_return = 1
|
||||||
|
cr_note.return_against = si.name
|
||||||
|
cr_note = cr_note.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(pr.get("invoices"), [])
|
||||||
|
self.assertEqual(pr.get("payments"), [])
|
||||||
|
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"is_system_generated": 1,
|
||||||
|
"docstatus": 1,
|
||||||
|
"voucher_type": "Credit Note",
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(journals), 1)
|
||||||
|
|
||||||
|
# assert status and outstanding
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Credit Note Issued")
|
||||||
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
|
||||||
|
cr_note.reload()
|
||||||
|
cr_note.cancel()
|
||||||
|
# 'Credit Note' Journal should be auto cancelled
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"is_system_generated": 1,
|
||||||
|
"docstatus": 1,
|
||||||
|
"voucher_type": "Credit Note",
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(journals), 0)
|
||||||
|
# assert status and outstanding
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Unpaid")
|
||||||
|
self.assertEqual(si.outstanding_amount, 100)
|
||||||
|
|
||||||
def test_cr_note_partial_against_invoice(self):
|
def test_cr_note_partial_against_invoice(self):
|
||||||
transaction_date = nowdate()
|
transaction_date = nowdate()
|
||||||
amount = 100
|
amount = 100
|
||||||
|
@ -80,13 +80,16 @@
|
|||||||
"target_warehouse",
|
"target_warehouse",
|
||||||
"quality_inspection",
|
"quality_inspection",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"batch_no",
|
"use_serial_batch_fields",
|
||||||
"col_break5",
|
"col_break5",
|
||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"serial_no",
|
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
|
"section_break_tlhi",
|
||||||
|
"serial_no",
|
||||||
|
"column_break_ciit",
|
||||||
|
"batch_no",
|
||||||
"edit_references",
|
"edit_references",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"so_detail",
|
"so_detail",
|
||||||
@ -628,13 +631,13 @@
|
|||||||
"options": "Quality Inspection"
|
"options": "Quality Inspection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "col_break5",
|
"fieldname": "col_break5",
|
||||||
@ -649,14 +652,14 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
"oldfieldname": "serial_no",
|
"oldfieldname": "serial_no",
|
||||||
"oldfieldtype": "Small Text",
|
"oldfieldtype": "Small Text"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item_tax_rate",
|
"fieldname": "item_tax_rate",
|
||||||
@ -824,17 +827,33 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"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,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-14 18:33:22.585715",
|
"modified": "2024-02-04 16:36:25.665743",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice Item",
|
"name": "POS Invoice Item",
|
||||||
|
@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
|
|||||||
target_warehouse: DF.Link | None
|
target_warehouse: DF.Link | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
weight_per_unit: DF.Float
|
weight_per_unit: DF.Float
|
||||||
weight_uom: DF.Link | None
|
weight_uom: DF.Link | None
|
||||||
|
@ -120,18 +120,6 @@ def get_statement_dict(doc, get_statement_dict=False):
|
|||||||
statement_dict = {}
|
statement_dict = {}
|
||||||
ageing = ""
|
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:
|
for entry in doc.customers:
|
||||||
if doc.include_ageing:
|
if doc.include_ageing:
|
||||||
ageing = set_ageing(doc, entry)
|
ageing = set_ageing(doc, entry)
|
||||||
@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
filters = get_common_filters(doc)
|
filters = get_common_filters(doc)
|
||||||
if err_journals:
|
if doc.ignore_exchange_rate_revaluation_journals:
|
||||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
filters.update({"ignore_err": True})
|
||||||
|
|
||||||
if doc.report == "General Ledger":
|
if doc.report == "General Ledger":
|
||||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
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,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
if self.is_old_subcontracting_flow:
|
if self.is_old_subcontracting_flow:
|
||||||
|
@ -62,16 +62,19 @@
|
|||||||
"rm_supp_cost",
|
"rm_supp_cost",
|
||||||
"warehouse_section",
|
"warehouse_section",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"from_warehouse",
|
|
||||||
"quality_inspection",
|
|
||||||
"add_serial_batch_bundle",
|
"add_serial_batch_bundle",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"serial_no",
|
"use_serial_batch_fields",
|
||||||
"col_br_wh",
|
"col_br_wh",
|
||||||
|
"from_warehouse",
|
||||||
|
"quality_inspection",
|
||||||
"rejected_warehouse",
|
"rejected_warehouse",
|
||||||
"rejected_serial_and_batch_bundle",
|
"rejected_serial_and_batch_bundle",
|
||||||
"batch_no",
|
"section_break_rqbe",
|
||||||
|
"serial_no",
|
||||||
"rejected_serial_no",
|
"rejected_serial_no",
|
||||||
|
"column_break_vbbb",
|
||||||
|
"batch_no",
|
||||||
"manufacture_details",
|
"manufacture_details",
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
@ -440,13 +443,11 @@
|
|||||||
"print_hide": 1
|
"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",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -454,21 +455,18 @@
|
|||||||
"fieldtype": "Column Break"
|
"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",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"hidden": 1,
|
"label": "Serial No"
|
||||||
"label": "Serial No",
|
|
||||||
"read_only": 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": "rejected_serial_no",
|
"fieldname": "rejected_serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Rejected Serial No",
|
"label": "Rejected Serial No",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "accounting",
|
"fieldname": "accounting",
|
||||||
@ -891,7 +889,7 @@
|
|||||||
"label": "Apply TDS"
|
"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",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -901,7 +899,7 @@
|
|||||||
"search_index": 1
|
"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",
|
"fieldname": "rejected_serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Rejected Serial and Batch Bundle",
|
"label": "Rejected Serial and Batch Bundle",
|
||||||
@ -916,16 +914,31 @@
|
|||||||
"options": "Asset"
|
"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",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-21 19:46:25.537861",
|
"modified": "2024-02-04 14:11:52.742228",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
|
|||||||
stock_uom_rate: DF.Currency
|
stock_uom_rate: DF.Currency
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
weight_per_unit: DF.Float
|
weight_per_unit: DF.Float
|
||||||
|
@ -447,6 +447,7 @@ class SalesInvoice(SellingController):
|
|||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
# this sequence because outstanding may get -ve
|
# this sequence because outstanding may get -ve
|
||||||
|
@ -83,14 +83,17 @@
|
|||||||
"quality_inspection",
|
"quality_inspection",
|
||||||
"pick_serial_and_batch",
|
"pick_serial_and_batch",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"batch_no",
|
"use_serial_batch_fields",
|
||||||
"incoming_rate",
|
|
||||||
"col_break5",
|
"col_break5",
|
||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"serial_no",
|
"incoming_rate",
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
|
"section_break_eoec",
|
||||||
|
"serial_no",
|
||||||
|
"column_break_ytgd",
|
||||||
|
"batch_no",
|
||||||
"edit_references",
|
"edit_references",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"so_detail",
|
"so_detail",
|
||||||
@ -600,12 +603,11 @@
|
|||||||
"options": "Quality Inspection"
|
"options": "Quality Inspection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -621,13 +623,12 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
"oldfieldname": "serial_no",
|
"oldfieldname": "serial_no",
|
||||||
"oldfieldtype": "Small Text",
|
"oldfieldtype": "Small Text"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item_group",
|
"fieldname": "item_group",
|
||||||
@ -891,6 +892,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -904,12 +906,27 @@
|
|||||||
"fieldname": "pick_serial_and_batch",
|
"fieldname": "pick_serial_and_batch",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Pick Serial / Batch No"
|
"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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-29 13:03:14.121298",
|
"modified": "2024-02-04 11:52:16.106541",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
|
|||||||
target_warehouse: DF.Link | None
|
target_warehouse: DF.Link | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
weight_per_unit: DF.Float
|
weight_per_unit: DF.Float
|
||||||
weight_uom: DF.Link | None
|
weight_uom: DF.Link | None
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"default",
|
"default",
|
||||||
"mode_of_payment",
|
"mode_of_payment",
|
||||||
"amount",
|
"amount",
|
||||||
|
"reference_no",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"account",
|
"account",
|
||||||
"type",
|
"type",
|
||||||
@ -75,11 +76,16 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Default",
|
"label": "Default",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Reference No"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-03 12:45:39.986598",
|
"modified": "2024-01-23 16:20:06.436979",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Payment",
|
"name": "Sales Invoice Payment",
|
||||||
@ -87,5 +93,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
reference_no: DF.Data | None
|
||||||
type: DF.ReadOnly | None
|
type: DF.ReadOnly | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
@ -13,9 +13,13 @@ import erpnext
|
|||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
|
from erpnext.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.accounting_period.accounting_period import ClosedAccountingPeriod
|
||||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||||
|
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||||
|
|
||||||
|
|
||||||
def make_gl_entries(
|
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)
|
process_debit_credit_difference(gl_map)
|
||||||
|
|
||||||
|
dimension_filter_map = get_dimension_filter_map()
|
||||||
if gl_map:
|
if gl_map:
|
||||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
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"])
|
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
|
validate_allowed_dimensions(entry, dimension_filter_map)
|
||||||
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
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""",
|
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
|
||||||
(now(), frappe.session.user, voucher_type, voucher_no),
|
(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",
|
"fieldname": "show_remarks",
|
||||||
"label": __("Show Remarks"),
|
"label": __("Show Remarks"),
|
||||||
"fieldtype": "Check"
|
"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"):
|
if filters.get("against_voucher_no"):
|
||||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
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"):
|
if filters.get("voucher_no_not_in"):
|
||||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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
|
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[2]["credit"], 900)
|
||||||
self.assertEqual(data[3]["debit"], 100)
|
self.assertEqual(data[3]["debit"], 100)
|
||||||
self.assertEqual(data[3]["credit"], 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]))
|
||||||
|
@ -63,16 +63,14 @@ def get_result(
|
|||||||
tax_amount += entry.credit - entry.debit
|
tax_amount += entry.credit - entry.debit
|
||||||
# infer tax withholding category from the account if it's the single account for this category
|
# infer tax withholding category from the account if it's the single account for this category
|
||||||
tax_withholding_category = tds_accounts.get(entry.account)
|
tax_withholding_category = tds_accounts.get(entry.account)
|
||||||
rate = tax_rate_map.get(tax_withholding_category)
|
|
||||||
# or else the consolidated value from the voucher document
|
# or else the consolidated value from the voucher document
|
||||||
if not tax_withholding_category:
|
if not tax_withholding_category:
|
||||||
# or else from the party default
|
|
||||||
tax_withholding_category = tax_category_map.get(name)
|
tax_withholding_category = tax_category_map.get(name)
|
||||||
rate = tax_rate_map.get(tax_withholding_category)
|
# or else from the party default
|
||||||
if not tax_withholding_category:
|
if not tax_withholding_category:
|
||||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||||
rate = tax_rate_map.get(tax_withholding_category)
|
|
||||||
|
|
||||||
|
rate = tax_rate_map.get(tax_withholding_category)
|
||||||
if net_total_map.get(name):
|
if net_total_map.get(name):
|
||||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||||
# back calcalute total amount from rate and tax_amount
|
# back calcalute total amount from rate and tax_amount
|
||||||
@ -295,7 +293,7 @@ def get_tds_docs(filters):
|
|||||||
tds_accounts = {}
|
tds_accounts = {}
|
||||||
for tds_acc in _tds_accounts:
|
for tds_acc in _tds_accounts:
|
||||||
# if it turns out not to be the only tax withholding category, then don't include in the map
|
# if it turns out not to be the only tax withholding category, then don't include in the map
|
||||||
if tds_accounts.get(tds_acc["account"]):
|
if tds_acc["account"] in tds_accounts:
|
||||||
tds_accounts[tds_acc["account"]] = None
|
tds_accounts[tds_acc["account"]] = None
|
||||||
else:
|
else:
|
||||||
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
|
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
|
||||||
@ -354,9 +352,6 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
|||||||
if filters.get("to_date"):
|
if filters.get("to_date"):
|
||||||
query = query.where(gle.posting_date <= filters.get("to_date"))
|
query = query.where(gle.posting_date <= filters.get("to_date"))
|
||||||
|
|
||||||
if bank_accounts:
|
|
||||||
query = query.where(gle.against.notin(bank_accounts))
|
|
||||||
|
|
||||||
if filters.get("party"):
|
if filters.get("party"):
|
||||||
party = [filters.get("party")]
|
party = [filters.get("party")]
|
||||||
jv_condition = gle.against.isin(party) | (
|
jv_condition = gle.against.isin(party) | (
|
||||||
@ -368,7 +363,14 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
|||||||
(gle.voucher_type == "Journal Entry")
|
(gle.voucher_type == "Journal Entry")
|
||||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||||
)
|
)
|
||||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
|
||||||
|
query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||||
|
if bank_accounts:
|
||||||
|
query = query.where(
|
||||||
|
gle.against.notin(bank_accounts) & (gle.account.isin(tds_accounts) & jv_condition)
|
||||||
|
| gle.party.isin(party)
|
||||||
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@ -408,7 +410,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
|||||||
"paid_amount_after_tax",
|
"paid_amount_after_tax",
|
||||||
"base_paid_amount",
|
"base_paid_amount",
|
||||||
],
|
],
|
||||||
"Journal Entry": ["tax_withholding_category", "total_amount"],
|
"Journal Entry": ["total_amount"],
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = frappe.get_all(
|
entries = frappe.get_all(
|
||||||
|
@ -5,7 +5,6 @@ import frappe
|
|||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import today
|
from frappe.utils import today
|
||||||
|
|
||||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@ -17,36 +16,63 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
|
|
||||||
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.clear_old_entries()
|
self.clear_old_entries()
|
||||||
create_tax_accounts()
|
create_tax_accounts()
|
||||||
create_tcs_category()
|
|
||||||
|
|
||||||
def test_tax_withholding_for_customers(self):
|
def test_tax_withholding_for_customers(self):
|
||||||
|
create_tax_category(cumulative_threshold=300)
|
||||||
|
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
|
||||||
si = create_sales_invoice(rate=1000)
|
si = create_sales_invoice(rate=1000)
|
||||||
pe = create_tcs_payment_entry()
|
pe = create_tcs_payment_entry()
|
||||||
|
jv = create_tcs_journal_entry()
|
||||||
|
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
|
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
|
||||||
)
|
)
|
||||||
result = execute(filters)[1]
|
result = execute(filters)[1]
|
||||||
expected_values = [
|
expected_values = [
|
||||||
|
# Check for JV totals using back calculation logic
|
||||||
|
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||||
]
|
]
|
||||||
self.check_expected_values(result, expected_values)
|
self.check_expected_values(result, expected_values)
|
||||||
|
|
||||||
|
def test_single_account_for_multiple_categories(self):
|
||||||
|
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
|
||||||
|
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||||
|
inv_1.tax_withholding_category = "TDS - 1"
|
||||||
|
inv_1.submit()
|
||||||
|
|
||||||
|
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
|
||||||
|
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||||
|
inv_2.tax_withholding_category = "TDS - 2"
|
||||||
|
inv_2.submit()
|
||||||
|
result = execute(
|
||||||
|
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
|
||||||
|
)[1]
|
||||||
|
expected_values = [
|
||||||
|
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
|
||||||
|
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
|
||||||
|
]
|
||||||
|
self.check_expected_values(result, expected_values)
|
||||||
|
|
||||||
def check_expected_values(self, result, expected_values):
|
def check_expected_values(self, result, expected_values):
|
||||||
for i in range(len(result)):
|
for i in range(len(result)):
|
||||||
voucher = frappe._dict(result[i])
|
voucher = frappe._dict(result[i])
|
||||||
voucher_expected_values = expected_values[i]
|
voucher_expected_values = expected_values[i]
|
||||||
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
|
voucher_actual_values = (
|
||||||
self.assertEqual(voucher.section_code, voucher_expected_values[1])
|
voucher.ref_no,
|
||||||
self.assertEqual(voucher.rate, voucher_expected_values[2])
|
voucher.section_code,
|
||||||
self.assertEqual(voucher.base_total, voucher_expected_values[3])
|
voucher.rate,
|
||||||
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
|
voucher.base_total,
|
||||||
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
|
voucher.tax_amount,
|
||||||
|
voucher.grand_total,
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.clear_old_entries()
|
self.clear_old_entries()
|
||||||
@ -67,24 +93,20 @@ def create_tax_accounts():
|
|||||||
).insert(ignore_if_duplicate=True)
|
).insert(ignore_if_duplicate=True)
|
||||||
|
|
||||||
|
|
||||||
def create_tcs_category():
|
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
|
||||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||||
from_date = fiscal_year[1]
|
from_date = fiscal_year[1]
|
||||||
to_date = fiscal_year[2]
|
to_date = fiscal_year[2]
|
||||||
|
|
||||||
tax_category = create_tax_withholding_category(
|
create_tax_withholding_category(
|
||||||
category_name="TCS",
|
category_name=category,
|
||||||
rate=0.075,
|
rate=rate,
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
to_date=to_date,
|
to_date=to_date,
|
||||||
account="TCS - _TC",
|
account=account,
|
||||||
cumulative_threshold=300,
|
cumulative_threshold=cumulative_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
|
||||||
customer.tax_withholding_category = "TCS"
|
|
||||||
customer.save()
|
|
||||||
|
|
||||||
|
|
||||||
def create_tcs_payment_entry():
|
def create_tcs_payment_entry():
|
||||||
payment_entry = create_payment_entry(
|
payment_entry = create_payment_entry(
|
||||||
@ -109,3 +131,32 @@ def create_tcs_payment_entry():
|
|||||||
)
|
)
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
return payment_entry
|
return payment_entry
|
||||||
|
|
||||||
|
|
||||||
|
def create_tcs_journal_entry():
|
||||||
|
jv = frappe.new_doc("Journal Entry")
|
||||||
|
jv.posting_date = today()
|
||||||
|
jv.company = "_Test Company"
|
||||||
|
jv.set(
|
||||||
|
"accounts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"account": "Debtors - _TC",
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": "_Test Customer",
|
||||||
|
"credit_in_account_currency": 10000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": "Debtors - _TC",
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": "_Test Customer",
|
||||||
|
"debit_in_account_currency": 9992.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": "TCS - _TC",
|
||||||
|
"debit_in_account_currency": 7.5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
jv.insert()
|
||||||
|
return jv.submit()
|
||||||
|
@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
|
|||||||
"options": erpnext.get_presentation_currency_list()
|
"options": erpnext.get_presentation_currency_list()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "with_period_closing_entry",
|
"fieldname": "with_period_closing_entry_for_opening",
|
||||||
"label": __("Period Closing Entry"),
|
"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",
|
"fieldtype": "Check",
|
||||||
"default": 1
|
"default": 1
|
||||||
},
|
},
|
||||||
|
@ -116,7 +116,7 @@ def get_data(filters):
|
|||||||
max_rgt,
|
max_rgt,
|
||||||
filters,
|
filters,
|
||||||
gl_entries_by_account,
|
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,
|
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)
|
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":
|
if doctype == "Account Closing Balance":
|
||||||
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
||||||
else:
|
else:
|
||||||
|
@ -237,7 +237,7 @@ def get_balance_on(
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
|
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center),))
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
||||||
@ -258,7 +258,7 @@ def get_balance_on(
|
|||||||
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
|
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
|
||||||
in_account_currency = False
|
in_account_currency = False
|
||||||
else:
|
else:
|
||||||
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
|
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
|
||||||
|
|
||||||
if account_type:
|
if account_type:
|
||||||
accounts = frappe.db.get_all(
|
accounts = frappe.db.get_all(
|
||||||
@ -278,11 +278,11 @@ def get_balance_on(
|
|||||||
if party_type and party:
|
if party_type and party:
|
||||||
cond.append(
|
cond.append(
|
||||||
"""gle.party_type = %s and gle.party = %s """
|
"""gle.party_type = %s and gle.party = %s """
|
||||||
% (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
|
% (frappe.db.escape(party_type), frappe.db.escape(party))
|
||||||
)
|
)
|
||||||
|
|
||||||
if company:
|
if company:
|
||||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
|
||||||
|
|
||||||
if account or (party_type and party) or account_type:
|
if account or (party_type and party) or account_type:
|
||||||
precision = get_currency_precision()
|
precision = get_currency_precision()
|
||||||
@ -348,7 +348,7 @@ def get_count_on(account, fieldname, date):
|
|||||||
% (acc.lft, acc.rgt)
|
% (acc.lft, acc.rgt)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
|
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
|
||||||
|
|
||||||
entries = frappe.db.sql(
|
entries = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
|
@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
|
|||||||
self.create_target_asset()
|
self.create_target_asset()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.update_target_asset()
|
self.update_target_asset()
|
||||||
|
@ -18,9 +18,12 @@
|
|||||||
"amount",
|
"amount",
|
||||||
"batch_and_serial_no_section",
|
"batch_and_serial_no_section",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
|
"use_serial_batch_fields",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"batch_no",
|
"section_break_bfqc",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
|
"column_break_mbuv",
|
||||||
|
"batch_no",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break"
|
"dimension_col_break"
|
||||||
@ -39,13 +42,13 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
@ -102,12 +105,12 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item_code",
|
"fieldname": "item_code",
|
||||||
@ -148,18 +151,34 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-06 01:10:17.947952",
|
"modified": "2024-02-04 16:41:09.239762",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Capitalization Stock Item",
|
"name": "Asset Capitalization Stock Item",
|
||||||
|
@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document):
|
|||||||
serial_no: DF.SmallText | None
|
serial_no: DF.SmallText | None
|
||||||
stock_qty: DF.Float
|
stock_qty: DF.Float
|
||||||
stock_uom: DF.Link
|
stock_uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
warehouse: DF.Link
|
warehouse: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
@ -457,6 +457,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
self.update_ordered_qty()
|
self.update_ordered_qty()
|
||||||
self.update_reserved_qty_for_subcontract()
|
self.update_reserved_qty_for_subcontract()
|
||||||
self.update_subcontracting_order_status()
|
self.update_subcontracting_order_status()
|
||||||
|
self.update_blanket_order()
|
||||||
self.notify_update()
|
self.notify_update()
|
||||||
clear_doctype_notifications(self)
|
clear_doctype_notifications(self)
|
||||||
|
|
||||||
@ -644,6 +645,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
|
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):
|
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||||
"""get last purchase rate for an item"""
|
"""get last purchase rate for an item"""
|
||||||
|
|
||||||
|
@ -822,6 +822,30 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
# To test if the PO does NOT have a Blanket Order
|
# To test if the PO does NOT have a Blanket Order
|
||||||
self.assertEqual(po_doc.items[0].blanket_order, None)
|
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):
|
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||||
create_payment_terms_template,
|
create_payment_terms_template,
|
||||||
@ -1148,6 +1172,7 @@ def create_purchase_order(**args):
|
|||||||
"schedule_date": add_days(nowdate(), 1),
|
"schedule_date": add_days(nowdate(), 1),
|
||||||
"include_exploded_items": args.get("include_exploded_items", 1),
|
"include_exploded_items": args.get("include_exploded_items", 1),
|
||||||
"against_blanket_order": args.against_blanket_order,
|
"against_blanket_order": args.against_blanket_order,
|
||||||
|
"against_blanket": args.against_blanket,
|
||||||
"material_request": args.material_request,
|
"material_request": args.material_request,
|
||||||
"material_request_item": args.material_request_item,
|
"material_request_item": args.material_request_item,
|
||||||
},
|
},
|
||||||
|
@ -545,7 +545,6 @@
|
|||||||
"fieldname": "blanket_order",
|
"fieldname": "blanket_order",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Blanket Order",
|
"label": "Blanket Order",
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Blanket Order"
|
"options": "Blanket Order"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -553,7 +552,6 @@
|
|||||||
"fieldname": "blanket_order_rate",
|
"fieldname": "blanket_order_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Blanket Order Rate",
|
"label": "Blanket Order Rate",
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -917,7 +915,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-24 13:24:41.298416",
|
"modified": "2024-02-05 11:23:24.859435",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
@ -693,7 +693,7 @@ class AccountsController(TransactionBase):
|
|||||||
if self.get("is_subcontracted"):
|
if self.get("is_subcontracted"):
|
||||||
args["is_subcontracted"] = self.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():
|
for fieldname, value in ret.items():
|
||||||
if item.meta.get_field(fieldname) and value is not None:
|
if item.meta.get_field(fieldname) and value is not None:
|
||||||
@ -1476,6 +1476,24 @@ class AccountsController(TransactionBase):
|
|||||||
x.update({dim.fieldname: self.get(dim.fieldname)})
|
x.update({dim.fieldname: self.get(dim.fieldname)})
|
||||||
reconcile_against_document(lst, active_dimensions=active_dimensions)
|
reconcile_against_document(lst, active_dimensions=active_dimensions)
|
||||||
|
|
||||||
|
def cancel_system_generated_credit_debit_notes(self):
|
||||||
|
# Cancel 'Credit/Debit' Note Journal Entries, if found.
|
||||||
|
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"is_system_generated": 1,
|
||||||
|
"reference_type": self.doctype,
|
||||||
|
"reference_name": self.name,
|
||||||
|
"voucher_type": voucher_type,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
for x in journals:
|
||||||
|
frappe.get_doc("Journal Entry", x).cancel()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
||||||
remove_from_bank_transaction,
|
remove_from_bank_transaction,
|
||||||
@ -1488,6 +1506,8 @@ class AccountsController(TransactionBase):
|
|||||||
remove_from_bank_transaction(self.doctype, self.name)
|
remove_from_bank_transaction(self.doctype, self.name)
|
||||||
|
|
||||||
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||||
|
self.cancel_system_generated_credit_debit_notes()
|
||||||
|
|
||||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||||
cancel_exchange_gain_loss_journal(self)
|
cancel_exchange_gain_loss_journal(self)
|
||||||
|
|
||||||
|
@ -729,17 +729,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
conditions, bin_conditions = [], []
|
conditions, bin_conditions = [], []
|
||||||
filter_dict = get_doctype_wise_filters(filters)
|
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
|
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||||
from `tabWarehouse` left join `tabBin`
|
from `tabWarehouse` left join `tabBin`
|
||||||
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
|
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
|
||||||
where
|
where
|
||||||
`tabWarehouse`.`{key}` like {txt}
|
`tabWarehouse`.`{key}` like {txt}
|
||||||
{fcond} {mcond}
|
{fcond} {mcond}
|
||||||
order by ifnull(`tabBin`.actual_qty, 0) desc
|
order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
|
||||||
limit
|
limit
|
||||||
{page_len} offset {start}
|
{page_len} offset {start}
|
||||||
""".format(
|
""".format(
|
||||||
|
warehouse_field=warehouse_field,
|
||||||
bin_conditions=get_filters_cond(
|
bin_conditions=get_filters_cond(
|
||||||
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
||||||
),
|
),
|
||||||
|
@ -599,7 +599,7 @@ class SellingController(StockController):
|
|||||||
if self.doctype in ["Sales Order", "Quotation"]:
|
if self.doctype in ["Sales Order", "Quotation"]:
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
item.gross_profit = flt(
|
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):
|
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 (
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||||
get_evaluated_inventory_dimension,
|
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
|
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
|
# remove extra whitespace and store one serial no on each line
|
||||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
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(
|
def get_gl_entries(
|
||||||
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
|
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
|
||||||
):
|
):
|
||||||
|
@ -98,6 +98,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||||
args = {
|
args = {
|
||||||
"net_rate": item.net_rate or item.rate,
|
"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"),
|
"tax_category": self.doc.get("tax_category"),
|
||||||
"posting_date": self.doc.get("posting_date"),
|
"posting_date": self.doc.get("posting_date"),
|
||||||
"bill_date": self.doc.get("bill_date"),
|
"bill_date": self.doc.get("bill_date"),
|
||||||
|
@ -1108,18 +1108,18 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
cr_note.reload()
|
cr_note.reload()
|
||||||
cr_note.cancel()
|
cr_note.cancel()
|
||||||
|
|
||||||
# Exchange Gain/Loss Journal should've been created.
|
# with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller
|
||||||
|
# JE(Credit Note) will be cancelled once the parent is cancelled
|
||||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
|
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||||
self.assertNotEqual(exc_je_for_si, [])
|
self.assertEqual(exc_je_for_si, [])
|
||||||
self.assertEqual(len(exc_je_for_si), 1)
|
self.assertEqual(len(exc_je_for_si), 0)
|
||||||
self.assertEqual(len(exc_je_for_cr), 0)
|
self.assertEqual(len(exc_je_for_cr), 0)
|
||||||
|
|
||||||
# The Credit Note JE is still active and is referencing the sales invoice
|
# No references, full outstanding
|
||||||
# So, outstanding stays the same
|
|
||||||
si.reload()
|
si.reload()
|
||||||
self.assertEqual(si.outstanding_amount, 1)
|
self.assertEqual(si.outstanding_amount, 2)
|
||||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||||
|
|
||||||
def test_40_cost_center_from_payment_entry(self):
|
def test_40_cost_center_from_payment_entry(self):
|
||||||
"""
|
"""
|
||||||
|
@ -42,7 +42,6 @@ setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wiza
|
|||||||
|
|
||||||
before_install = [
|
before_install = [
|
||||||
"erpnext.setup.install.check_setup_wizard_not_completed",
|
"erpnext.setup.install.check_setup_wizard_not_completed",
|
||||||
"erpnext.setup.install.check_frappe_version",
|
|
||||||
]
|
]
|
||||||
after_install = "erpnext.setup.install.after_install"
|
after_install = "erpnext.setup.install.after_install"
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ def make_order(source_name):
|
|||||||
def update_item(source, target, source_parent):
|
def update_item(source, target, source_parent):
|
||||||
target_qty = source.get("qty") - source.get("ordered_qty")
|
target_qty = source.get("qty") - source.get("ordered_qty")
|
||||||
target.qty = target_qty if flt(target_qty) >= 0 else 0
|
target.qty = target_qty if flt(target_qty) >= 0 else 0
|
||||||
|
target.rate = source.get("rate")
|
||||||
item = get_item_defaults(target.item_code, source_parent.company)
|
item = get_item_defaults(target.item_code, source_parent.company)
|
||||||
if item:
|
if item:
|
||||||
target.item_name = item.get("item_name")
|
target.item_name = item.get("item_name")
|
||||||
@ -111,6 +112,10 @@ def make_order(source_name):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if target_doc.doctype == "Purchase Order":
|
||||||
|
target_doc.set_missing_values()
|
||||||
|
|
||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
|
@ -312,9 +312,10 @@ class ProductionPlan(Document):
|
|||||||
so_item.parent,
|
so_item.parent,
|
||||||
so_item.item_code,
|
so_item.item_code,
|
||||||
so_item.warehouse,
|
so_item.warehouse,
|
||||||
(
|
so_item.qty,
|
||||||
(so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
|
so_item.work_order_qty,
|
||||||
).as_("pending_qty"),
|
so_item.delivered_qty,
|
||||||
|
so_item.conversion_factor,
|
||||||
so_item.description,
|
so_item.description,
|
||||||
so_item.name,
|
so_item.name,
|
||||||
so_item.bom_no,
|
so_item.bom_no,
|
||||||
@ -337,6 +338,11 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
items = items_query.run(as_dict=True)
|
items = items_query.run(as_dict=True)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item.pending_qty = (
|
||||||
|
flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor
|
||||||
|
)
|
||||||
|
|
||||||
pi = frappe.qb.DocType("Packed Item")
|
pi = frappe.qb.DocType("Packed Item")
|
||||||
|
|
||||||
packed_items_query = (
|
packed_items_query = (
|
||||||
@ -1334,10 +1340,10 @@ def get_sales_orders(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
date_field_mapper = {
|
date_field_mapper = {
|
||||||
"from_date": self.from_date >= so.transaction_date,
|
"from_date": so.transaction_date >= self.from_date,
|
||||||
"to_date": self.to_date <= so.transaction_date,
|
"to_date": so.transaction_date <= self.to_date,
|
||||||
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
|
"from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
|
||||||
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
|
"to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
for field, value in date_field_mapper.items():
|
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):
|
def validate_operation_data(row):
|
||||||
if row.get("qty") <= 0:
|
if flt(row.get("qty")) <= 0:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Quantity to Manufacture can not be zero for the operation {0}").format(
|
_("Quantity to Manufacture can not be zero for the operation {0}").format(
|
||||||
frappe.bold(row.get("operation"))
|
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(
|
frappe.throw(
|
||||||
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
|
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
|
||||||
frappe.bold(row.get("operation")),
|
frappe.bold(row.get("operation")),
|
||||||
|
@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
super.setup();
|
super.setup();
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
|
this.set_fields_onload_for_line_item();
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||||
|
|
||||||
frappe.flags.hide_serial_batch_dialog = true;
|
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", {
|
frappe.ui.form.on(this.frm.doctype + " Item", {
|
||||||
items_add: function(frm, cdt, cdn) {
|
items_add: function(frm, cdt, cdn) {
|
||||||
|
debugger
|
||||||
var item = frappe.get_doc(cdt, cdn);
|
var item = frappe.get_doc(cdt, cdn);
|
||||||
if (!item.warehouse && frm.doc.set_warehouse) {
|
if (!item.warehouse && frm.doc.set_warehouse) {
|
||||||
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;
|
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');
|
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) {
|
toggle_enable_for_stock_uom(field) {
|
||||||
@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
this.frm.doc.doctype === 'Delivery Note') {
|
this.frm.doc.doctype === 'Delivery Note') {
|
||||||
show_batch_dialog = 1;
|
show_batch_dialog = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_batch_dialog && item.use_serial_batch_fields === 1) {
|
||||||
|
show_batch_dialog = 0;
|
||||||
|
}
|
||||||
|
|
||||||
item.barcode = null;
|
item.barcode = null;
|
||||||
|
|
||||||
|
|
||||||
@ -502,6 +528,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
project: item.project || me.frm.doc.project,
|
project: item.project || me.frm.doc.project,
|
||||||
qty: item.qty || 1,
|
qty: item.qty || 1,
|
||||||
net_rate: item.rate,
|
net_rate: item.rate,
|
||||||
|
base_net_rate: item.base_net_rate,
|
||||||
stock_qty: item.stock_qty,
|
stock_qty: item.stock_qty,
|
||||||
conversion_factor: item.conversion_factor,
|
conversion_factor: item.conversion_factor,
|
||||||
weight_per_unit: item.weight_per_unit,
|
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.serial_no = item.serial_no.replace(/,/g, '\n');
|
||||||
item.conversion_factor = item.conversion_factor || 1;
|
item.conversion_factor = item.conversion_factor || 1;
|
||||||
refresh_field("serial_no", item.name, item.parentfield);
|
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(() => {
|
setTimeout(() => {
|
||||||
me.update_qty(cdt, cdn);
|
me.update_qty(cdt, cdn);
|
||||||
}, 10000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1241,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) {
|
service_start_date(frm, cdt, cdn) {
|
||||||
var child = locals[cdt][cdn];
|
var child = locals[cdt][cdn];
|
||||||
|
|
||||||
@ -1902,7 +1915,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
if (item.item_code) {
|
if (item.item_code) {
|
||||||
// Use combination of name and item code in case same item is added multiple times
|
// Use combination of name and item code in case same item is added multiple times
|
||||||
item_codes.push([item.item_code, item.name]);
|
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;
|
item_tax_templates[item.name] = item.item_tax_template;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
this.frm = opts.frm;
|
this.frm = opts.frm;
|
||||||
|
// frappe.flags.trigger_from_barcode_scanner is used for custom scripts
|
||||||
|
|
||||||
// field from which to capture input of scanned data
|
// field from which to capture input of scanned data
|
||||||
this.scan_field_name = opts.scan_field_name || "scan_barcode";
|
this.scan_field_name = opts.scan_field_name || "scan_barcode";
|
||||||
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
|
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
|
||||||
|
|
||||||
this.barcode_field = opts.barcode_field || "barcode";
|
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.uom_field = opts.uom_field || "uom";
|
||||||
this.qty_field = opts.qty_field || "qty";
|
this.qty_field = opts.qty_field || "qty";
|
||||||
// field name on row which defines max quantity to be scanned e.g. picklist
|
// 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;
|
this.frm.has_items = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serial_no) {
|
if (this.is_duplicate_serial_no(row, serial_no)) {
|
||||||
this.is_duplicate_serial_no(row, item_code, serial_no)
|
this.clean_up();
|
||||||
.then((is_duplicate) => {
|
reject();
|
||||||
if (!is_duplicate) {
|
return;
|
||||||
this.run_serially_tasks(row, data, resolve);
|
|
||||||
} else {
|
|
||||||
this.clean_up();
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.run_serially_tasks(row, data, resolve);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
// batch and serial selector is reduandant when all info can be added by scan
|
||||||
const {item_code, barcode, batch_no, serial_no, uom} = data;
|
// 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([
|
const require_selecting_batch = has_batch_no && !batch_no;
|
||||||
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
|
const require_selecting_serial = has_serial_no && !serial_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();
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.flags.trigger_from_barcode_scanner = false;
|
if (!(require_selecting_batch || require_selecting_serial)) {
|
||||||
},
|
frappe.flags.hide_serial_batch_dialog = true;
|
||||||
() => resolve(row),
|
}
|
||||||
]);
|
}
|
||||||
|
|
||||||
|
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) {
|
set_item(row, item_code, barcode, batch_no, serial_no) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const increment = async (value = 1) => {
|
const increment = async (value = 1) => {
|
||||||
const item_data = {item_code: item_code};
|
const item_data = {item_code: item_code, use_serial_batch_fields: 1.0};
|
||||||
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
|
||||||
frappe.flags.trigger_from_barcode_scanner = true;
|
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);
|
await frappe.model.set_value(row.doctype, row.name, item_data);
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
||||||
increment(value).then((value) => resolve(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 {
|
} else {
|
||||||
increment().then((value) => resolve(value));
|
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.model.set_value(row.doctype, row.name, item_data);
|
||||||
|
|
||||||
frappe.run_serially([
|
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_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.add_child_for_remaining_qty(row),
|
||||||
() => this.clean_up()
|
() => this.clean_up()
|
||||||
]);
|
]);
|
||||||
@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_serial_and_batch(row, item_code, serial_no, batch_no) {
|
async set_serial_no(row, serial_no) {
|
||||||
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
|
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
|
||||||
this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
|
const existing_serial_nos = row[this.serial_no_field];
|
||||||
} else if(row.serial_and_batch_bundle) {
|
let new_serial_nos = "";
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_key_for_localstorage() {
|
if (!!existing_serial_nos) {
|
||||||
let parts = this.frm.doc.name.split("-");
|
new_serial_nos = existing_serial_nos + "\n" + serial_no;
|
||||||
return parts[parts.length - 1] + this.frm.doc.doctype;
|
} else {
|
||||||
}
|
new_serial_nos = serial_no;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
||||||
for (let item_code in items) {
|
|
||||||
if (!existing_items.includes(item_code)) {
|
|
||||||
delete items[item_code];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage[docname] = JSON.stringify(items);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
async set_barcode_uom(row, uom) {
|
||||||
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
|
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
|
||||||
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
|
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) {
|
async set_barcode(row, barcode) {
|
||||||
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
||||||
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
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) {
|
is_duplicate_serial_no(row, serial_no) {
|
||||||
let is_duplicate = false;
|
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(is_duplicate);
|
if (is_duplicate) {
|
||||||
} else if (row.serial_and_batch_bundle) {
|
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return is_duplicate;
|
||||||
let existing_row = [];
|
|
||||||
if (entries[item_code]) {
|
|
||||||
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
|
|
||||||
}
|
|
||||||
|
|
||||||
return existing_row.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
|
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) {
|
show_alert(msg, indicator, duration=3) {
|
||||||
frappe.show_alert({message: msg, indicator: indicator}, duration);
|
frappe.show_alert({message: msg, indicator: indicator}, duration);
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -230,6 +230,7 @@ class Customer(TransactionBase):
|
|||||||
|
|
||||||
if self.flags.is_new_doc:
|
if self.flags.is_new_doc:
|
||||||
self.link_lead_address_and_contact()
|
self.link_lead_address_and_contact()
|
||||||
|
self.copy_communication()
|
||||||
|
|
||||||
self.update_customer_groups()
|
self.update_customer_groups()
|
||||||
|
|
||||||
@ -291,6 +292,17 @@ class Customer(TransactionBase):
|
|||||||
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
|
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
|
||||||
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
|
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):
|
def validate_name_with_customer_group(self):
|
||||||
if frappe.db.exists("Customer Group", self.name):
|
if frappe.db.exists("Customer Group", self.name):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@ -560,15 +572,14 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_emails(args):
|
def send_emails(customer, customer_outstanding, credit_limit, credit_controller_users_list):
|
||||||
args = json.loads(args)
|
if isinstance(credit_controller_users_list, str):
|
||||||
subject = _("Credit limit reached for customer {0}").format(args.get("customer"))
|
credit_controller_users_list = json.loads(credit_controller_users_list)
|
||||||
|
subject = _("Credit limit reached for customer {0}").format(customer)
|
||||||
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
|
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
|
||||||
args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit")
|
customer, customer_outstanding, credit_limit
|
||||||
)
|
|
||||||
frappe.sendmail(
|
|
||||||
recipients=args.get("credit_controller_users_list"), subject=subject, message=message
|
|
||||||
)
|
)
|
||||||
|
frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
|
||||||
|
|
||||||
|
|
||||||
def get_customer_outstanding(
|
def get_customer_outstanding(
|
||||||
|
@ -906,6 +906,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
|||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("set_po_nos")
|
target.run_method("set_po_nos")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
target.run_method("set_use_serial_batch_fields")
|
||||||
|
|
||||||
if source.company_address:
|
if source.company_address:
|
||||||
target.update({"company_address": source.company_address})
|
target.update({"company_address": source.company_address})
|
||||||
@ -1026,6 +1027,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("set_po_nos")
|
target.run_method("set_po_nos")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
target.run_method("set_use_serial_batch_fields")
|
||||||
|
|
||||||
if source.company_address:
|
if source.company_address:
|
||||||
target.update({"company_address": source.company_address})
|
target.update({"company_address": source.company_address})
|
||||||
@ -1608,7 +1610,11 @@ def create_pick_list(source_name, target_doc=None):
|
|||||||
"Sales Order",
|
"Sales Order",
|
||||||
source_name,
|
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": {
|
"Sales Order Item": {
|
||||||
"doctype": "Pick List Item",
|
"doctype": "Pick List Item",
|
||||||
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
|
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
|
||||||
|
@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_
|
|||||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
WarehouseRequired,
|
WarehouseRequired,
|
||||||
|
create_pick_list,
|
||||||
make_delivery_note,
|
make_delivery_note,
|
||||||
make_material_request,
|
make_material_request,
|
||||||
make_raw_material_request,
|
make_raw_material_request,
|
||||||
@ -2023,6 +2024,83 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_pick_list_without_rejected_materials(self):
|
||||||
|
serial_and_batch_item = make_item(
|
||||||
|
"_Test Serial and Batch Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BAT-TSBIFRM-.#####",
|
||||||
|
"serial_no_series": "SN-TSBIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Serial Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SN-TSIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Batch Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BAT-TBIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
normal_item = make_item("_Test Normal Item for Rejected Materials").name
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC"
|
||||||
|
|
||||||
|
if not frappe.db.exists("Warehouse", rejected_warehouse):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Warehouse",
|
||||||
|
"warehouse_name": rejected_warehouse,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"warehouse_group": "_Test Warehouse Group",
|
||||||
|
"is_rejected_warehouse": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True)
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse})
|
||||||
|
|
||||||
|
se.save()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True
|
||||||
|
)
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse})
|
||||||
|
|
||||||
|
se.save()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True)
|
||||||
|
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse})
|
||||||
|
|
||||||
|
so.save()
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
pick_list = create_pick_list(so.name)
|
||||||
|
|
||||||
|
pick_list.save()
|
||||||
|
for row in pick_list.locations:
|
||||||
|
self.assertEqual(row.qty, 1.0)
|
||||||
|
self.assertFalse(row.warehouse == rejected_warehouse)
|
||||||
|
self.assertTrue(row.warehouse == warehouse)
|
||||||
|
|
||||||
|
|
||||||
def automatically_fetch_payment_terms(enable=1):
|
def automatically_fetch_payment_terms(enable=1):
|
||||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||||
|
@ -2,14 +2,12 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import click
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
import erpnext
|
|
||||||
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
|
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
|
||||||
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
||||||
|
|
||||||
@ -42,25 +40,6 @@ You can reinstall this site (after saving your data) using: bench --site [sitena
|
|||||||
frappe.throw(message) # nosemgrep
|
frappe.throw(message) # nosemgrep
|
||||||
|
|
||||||
|
|
||||||
def check_frappe_version():
|
|
||||||
def major_version(v: str) -> str:
|
|
||||||
return v.split(".")[0]
|
|
||||||
|
|
||||||
frappe_version = major_version(frappe.__version__)
|
|
||||||
erpnext_version = major_version(erpnext.__version__)
|
|
||||||
|
|
||||||
if frappe_version == erpnext_version:
|
|
||||||
return
|
|
||||||
|
|
||||||
click.secho(
|
|
||||||
f"You're attempting to install ERPNext version {erpnext_version} with Frappe version {frappe_version}. "
|
|
||||||
"This is not supported and will result in broken install. Switch to correct branch before installing.",
|
|
||||||
fg="red",
|
|
||||||
)
|
|
||||||
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def set_single_defaults():
|
def set_single_defaults():
|
||||||
for dt in (
|
for dt in (
|
||||||
"Accounts Settings",
|
"Accounts Settings",
|
||||||
|
@ -398,6 +398,8 @@ class DeliveryNote(SellingController):
|
|||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
elif self.issue_credit_note:
|
elif self.issue_credit_note:
|
||||||
self.make_return_invoice()
|
self.make_return_invoice()
|
||||||
|
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
@ -200,7 +200,6 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
|
||||||
serial_nos = [
|
serial_nos = [
|
||||||
"OSN-1",
|
"OSN-1",
|
||||||
"OSN-2",
|
"OSN-2",
|
||||||
@ -239,6 +238,8 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
se_doc.items[0].serial_no = "\n".join(serial_nos)
|
se_doc.items[0].serial_no = "\n".join(serial_nos)
|
||||||
|
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
se_doc.submit()
|
se_doc.submit()
|
||||||
|
|
||||||
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
|
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
|
||||||
@ -294,6 +295,8 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
self.assertTrue(serial_no in serial_nos)
|
self.assertTrue(serial_no in serial_nos)
|
||||||
self.assertFalse(serial_no in returned_serial_nos1)
|
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):
|
def test_sales_return_for_non_bundled_items_partial(self):
|
||||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||||
|
|
||||||
@ -1563,7 +1566,7 @@ def create_delivery_note(**args):
|
|||||||
dn.return_against = args.return_against
|
dn.return_against = args.return_against
|
||||||
|
|
||||||
bundle_id = None
|
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"
|
type_of_transaction = args.type_of_transaction or "Outward"
|
||||||
|
|
||||||
if dn.is_return:
|
if dn.is_return:
|
||||||
@ -1605,6 +1608,9 @@ def create_delivery_note(**args):
|
|||||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||||
"target_warehouse": args.target_warehouse,
|
"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",
|
"section_break_40",
|
||||||
"pick_serial_and_batch",
|
"pick_serial_and_batch",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
|
"use_serial_batch_fields",
|
||||||
"column_break_eaoe",
|
"column_break_eaoe",
|
||||||
|
"section_break_qyjv",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
|
"column_break_rxvc",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"available_qty_section",
|
"available_qty_section",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
@ -850,6 +853,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -859,6 +863,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "pick_serial_and_batch",
|
"fieldname": "pick_serial_and_batch",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Pick Serial / Batch No"
|
"label": "Pick Serial / Batch No"
|
||||||
@ -874,27 +879,40 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"hidden": 1,
|
"label": "Serial No"
|
||||||
"label": "Serial No",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-14 18:37:38.638144",
|
"modified": "2024-02-04 14:10:31.750340",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
@ -82,6 +82,7 @@ class DeliveryNoteItem(Document):
|
|||||||
target_warehouse: DF.Link | None
|
target_warehouse: DF.Link | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
weight_per_unit: DF.Float
|
weight_per_unit: DF.Float
|
||||||
weight_uom: DF.Link | None
|
weight_uom: DF.Link | None
|
||||||
|
@ -149,6 +149,13 @@ class LandedCostVoucher(Document):
|
|||||||
self.get("items")[item_count - 1].applicable_charges += diff
|
self.get("items")[item_count - 1].applicable_charges += diff
|
||||||
|
|
||||||
def validate_applicable_charges_for_item(self):
|
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()
|
based_on = self.distribute_charges_based_on.lower()
|
||||||
|
|
||||||
if based_on != "distribute manually":
|
if based_on != "distribute manually":
|
||||||
|
@ -20,9 +20,12 @@
|
|||||||
"uom",
|
"uom",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"pick_serial_and_batch",
|
"pick_serial_and_batch",
|
||||||
"serial_and_batch_bundle",
|
"use_serial_batch_fields",
|
||||||
"serial_no",
|
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
|
"serial_and_batch_bundle",
|
||||||
|
"section_break_bgys",
|
||||||
|
"serial_no",
|
||||||
|
"column_break_qlha",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
"section_break_13",
|
"section_break_13",
|
||||||
@ -118,10 +121,10 @@
|
|||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_11",
|
"fieldname": "column_break_11",
|
||||||
@ -131,8 +134,7 @@
|
|||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "section_break_13",
|
||||||
@ -259,6 +261,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -267,16 +270,32 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "pick_serial_and_batch",
|
"fieldname": "pick_serial_and_batch",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Pick Serial / Batch No"
|
"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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-28 13:16:38.460806",
|
"modified": "2024-02-04 16:30:44.263964",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Packed Item",
|
"name": "Packed Item",
|
||||||
|
@ -47,6 +47,7 @@ class PackedItem(Document):
|
|||||||
serial_no: DF.Text | None
|
serial_no: DF.Text | None
|
||||||
target_warehouse: DF.Link | None
|
target_warehouse: DF.Link | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', {
|
|||||||
frm.set_query('parent_warehouse', () => {
|
frm.set_query('parent_warehouse', () => {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'is_group': 1,
|
|
||||||
'company': frm.doc.company
|
'company': frm.doc.company
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"for_qty",
|
"for_qty",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
|
"consider_rejected_warehouses",
|
||||||
"get_item_locations",
|
"get_item_locations",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
@ -51,7 +52,7 @@
|
|||||||
"description": "Items under this warehouse will be suggested",
|
"description": "Items under this warehouse will be suggested",
|
||||||
"fieldname": "parent_warehouse",
|
"fieldname": "parent_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Parent Warehouse",
|
"label": "Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -184,11 +185,18 @@
|
|||||||
"report_hide": 1,
|
"report_hide": 1,
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Enable it if users want to consider rejected materials to dispatch.",
|
||||||
|
"fieldname": "consider_rejected_warehouses",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consider Rejected Warehouses"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-24 10:33:43.244476",
|
"modified": "2024-02-02 16:17:44.877426",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
@ -260,4 +268,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
|
|||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case
|
||||||
from frappe.query_builder.custom import GROUP_CONCAT
|
from frappe.query_builder.custom import GROUP_CONCAT
|
||||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
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 frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
@ -122,11 +122,42 @@ class PickList(Document):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_serial_and_batch_bundle()
|
self.validate_serial_and_batch_bundle()
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.update_bundle_picked_qty()
|
self.update_bundle_picked_qty()
|
||||||
self.update_reference_qty()
|
self.update_reference_qty()
|
||||||
self.update_sales_order_picking_status()
|
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:
|
def on_update_after_submit(self) -> None:
|
||||||
if self.has_reserved_stock():
|
if self.has_reserved_stock():
|
||||||
msg = _(
|
msg = _(
|
||||||
@ -156,6 +187,7 @@ class PickList(Document):
|
|||||||
{"is_cancelled": 1, "voucher_no": ""},
|
{"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)
|
row.db_set("serial_and_batch_bundle", None)
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
@ -324,7 +356,6 @@ class PickList(Document):
|
|||||||
locations_replica = self.get("locations")
|
locations_replica = self.get("locations")
|
||||||
|
|
||||||
# reset
|
# reset
|
||||||
self.remove_serial_and_batch_bundle()
|
|
||||||
self.delete_key("locations")
|
self.delete_key("locations")
|
||||||
updated_locations = frappe._dict()
|
updated_locations = frappe._dict()
|
||||||
for item_doc in items:
|
for item_doc in items:
|
||||||
@ -338,6 +369,7 @@ class PickList(Document):
|
|||||||
self.item_count_map.get(item_code),
|
self.item_count_map.get(item_code),
|
||||||
self.company,
|
self.company,
|
||||||
picked_item_details=picked_items_details.get(item_code),
|
picked_item_details=picked_items_details.get(item_code),
|
||||||
|
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -639,13 +671,19 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
|||||||
if not stock_qty:
|
if not stock_qty:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
serial_nos = None
|
||||||
|
if item_location.serial_nos:
|
||||||
|
serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)])
|
||||||
|
|
||||||
locations.append(
|
locations.append(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
"stock_qty": stock_qty,
|
"stock_qty": stock_qty,
|
||||||
"warehouse": item_location.warehouse,
|
"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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -673,6 +711,7 @@ def get_available_item_locations(
|
|||||||
company,
|
company,
|
||||||
ignore_validation=False,
|
ignore_validation=False,
|
||||||
picked_item_details=None,
|
picked_item_details=None,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
total_picked_qty = (
|
total_picked_qty = (
|
||||||
@ -681,17 +720,41 @@ def get_available_item_locations(
|
|||||||
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
|
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")
|
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,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
|
)
|
||||||
|
elif has_serial_no:
|
||||||
locations = get_available_item_locations_for_serialized_item(
|
locations = get_available_item_locations_for_serialized_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
elif has_batch_no:
|
elif has_batch_no:
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
locations = get_available_item_locations_for_other_item(
|
locations = get_available_item_locations_for_other_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_qty_available = sum(location.get("qty") for location in locations)
|
total_qty_available = sum(location.get("qty") for location in locations)
|
||||||
@ -724,8 +787,56 @@ def get_available_item_locations(
|
|||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_item_locations_for_serial_and_batched_item(
|
||||||
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
|
):
|
||||||
|
# Get batch nos by FIFO
|
||||||
|
locations = get_available_item_locations_for_batched_item(
|
||||||
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
def get_available_item_locations_for_serialized_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
||||||
|
|
||||||
@ -742,6 +853,10 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
else:
|
else:
|
||||||
query = query.where(Coalesce(sn.warehouse, "") != "")
|
query = query.where(Coalesce(sn.warehouse, "") != "")
|
||||||
|
|
||||||
|
if not consider_rejected_warehouses:
|
||||||
|
if rejected_warehouses := get_rejected_warehouses():
|
||||||
|
query = query.where(sn.warehouse.notin(rejected_warehouses))
|
||||||
|
|
||||||
serial_nos = query.run(as_list=True)
|
serial_nos = query.run(as_list=True)
|
||||||
|
|
||||||
warehouse_serial_nos_map = frappe._dict()
|
warehouse_serial_nos_map = frappe._dict()
|
||||||
@ -757,28 +872,16 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
picked_qty -= 1
|
picked_qty -= 1
|
||||||
|
|
||||||
locations = []
|
locations = []
|
||||||
|
|
||||||
for warehouse, serial_nos in warehouse_serial_nos_map.items():
|
for warehouse, serial_nos in warehouse_serial_nos_map.items():
|
||||||
qty = len(serial_nos)
|
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(
|
locations.append(
|
||||||
{
|
{
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
"warehouse": warehouse,
|
"warehouse": warehouse,
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"serial_and_batch_bundle": bundle_doc.name,
|
"serial_nos": serial_nos,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -786,7 +889,12 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_batched_item(
|
def get_available_item_locations_for_batched_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
data = get_auto_batch_nos(
|
data = get_auto_batch_nos(
|
||||||
@ -801,42 +909,42 @@ def get_available_item_locations_for_batched_item(
|
|||||||
)
|
)
|
||||||
|
|
||||||
warehouse_wise_batches = frappe._dict()
|
warehouse_wise_batches = frappe._dict()
|
||||||
|
rejected_warehouses = get_rejected_warehouses()
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
|
if (
|
||||||
|
not consider_rejected_warehouses and rejected_warehouses and d.warehouse in rejected_warehouses
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
if d.warehouse not in warehouse_wise_batches:
|
if d.warehouse not in warehouse_wise_batches:
|
||||||
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
|
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
|
||||||
|
|
||||||
warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
|
warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
|
||||||
|
|
||||||
for warehouse, batches in warehouse_wise_batches.items():
|
for warehouse, batches in warehouse_wise_batches.items():
|
||||||
qty = sum(batches.values())
|
for batch_no, qty in batches.items():
|
||||||
|
locations.append(
|
||||||
bundle_doc = SerialBatchCreation(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"item_code": item_code,
|
"qty": qty,
|
||||||
"warehouse": warehouse,
|
"warehouse": warehouse,
|
||||||
"voucher_type": "Pick List",
|
"item_code": item_code,
|
||||||
"total_qty": qty * -1,
|
"batch_no": batch_no,
|
||||||
"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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_other_item(
|
def get_available_item_locations_for_other_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
bin = frappe.qb.DocType("Bin")
|
bin = frappe.qb.DocType("Bin")
|
||||||
query = (
|
query = (
|
||||||
@ -853,6 +961,10 @@ def get_available_item_locations_for_other_item(
|
|||||||
wh = frappe.qb.DocType("Warehouse")
|
wh = frappe.qb.DocType("Warehouse")
|
||||||
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
|
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
|
||||||
|
|
||||||
|
if not consider_rejected_warehouses:
|
||||||
|
if rejected_warehouses := get_rejected_warehouses():
|
||||||
|
query = query.where(bin.warehouse.notin(rejected_warehouses))
|
||||||
|
|
||||||
item_locations = query.run(as_dict=True)
|
item_locations = query.run(as_dict=True)
|
||||||
|
|
||||||
return item_locations
|
return item_locations
|
||||||
@ -1174,3 +1286,15 @@ def update_common_item_properties(item, location):
|
|||||||
item.serial_no = location.serial_no
|
item.serial_no = location.serial_no
|
||||||
item.batch_no = location.batch_no
|
item.batch_no = location.batch_no
|
||||||
item.material_request_item = location.material_request_item
|
item.material_request_item = location.material_request_item
|
||||||
|
|
||||||
|
|
||||||
|
def get_rejected_warehouses():
|
||||||
|
if not hasattr(frappe.local, "rejected_warehouses"):
|
||||||
|
frappe.local.rejected_warehouses = []
|
||||||
|
|
||||||
|
if not frappe.local.rejected_warehouses:
|
||||||
|
frappe.local.rejected_warehouses = frappe.get_all(
|
||||||
|
"Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name"
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe.local.rejected_warehouses
|
||||||
|
@ -217,6 +217,8 @@ class TestPickList(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pick_list.save()
|
pick_list.save()
|
||||||
|
pick_list.submit()
|
||||||
|
|
||||||
self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
|
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].warehouse, "_Test Warehouse - _TC")
|
||||||
self.assertEqual(pick_list.locations[0].qty, 5)
|
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 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
|
||||||
|
|
||||||
pr1.load_from_db()
|
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)
|
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.set_item_locations()
|
||||||
|
pick_list.submit()
|
||||||
|
pick_list.reload()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
|
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
|
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pick_list.cancel()
|
||||||
pr1.cancel()
|
pr1.cancel()
|
||||||
pr2.cancel()
|
pr2.cancel()
|
||||||
|
|
||||||
@ -671,29 +676,22 @@ class TestPickList(FrappeTestCase):
|
|||||||
|
|
||||||
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||||
pl = create_pick_list(so.name)
|
pl = create_pick_list(so.name)
|
||||||
|
pl.submit()
|
||||||
# pick half the qty
|
# pick half the qty
|
||||||
for loc in pl.locations:
|
for loc in pl.locations:
|
||||||
self.assertEqual(loc.qty, 25.0)
|
self.assertEqual(loc.qty, 25.0)
|
||||||
self.assertTrue(loc.serial_and_batch_bundle)
|
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.save()
|
||||||
pl.submit()
|
pl.submit()
|
||||||
|
|
||||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
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
|
# pick half the qty
|
||||||
for loc in pl.locations:
|
for loc in pl1.locations:
|
||||||
self.assertEqual(loc.qty, 10.0)
|
self.assertEqual(loc.qty, 5.0)
|
||||||
self.assertTrue(loc.serial_and_batch_bundle)
|
self.assertTrue(loc.serial_and_batch_bundle)
|
||||||
|
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
@ -709,8 +707,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
elif d.batch_no == "PICKLT-000002":
|
elif d.batch_no == "PICKLT-000002":
|
||||||
self.assertEqual(d.qty, 5.0 * -1)
|
self.assertEqual(d.qty, 5.0 * -1)
|
||||||
|
|
||||||
pl.save()
|
pl1.cancel()
|
||||||
pl.submit()
|
|
||||||
pl.cancel()
|
pl.cancel()
|
||||||
|
|
||||||
def test_picklist_for_serial_item(self):
|
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)
|
so = make_sales_order(item_code=item, qty=25.0, rate=100)
|
||||||
pl = create_pick_list(so.name)
|
pl = create_pick_list(so.name)
|
||||||
|
pl.submit()
|
||||||
picked_serial_nos = []
|
picked_serial_nos = []
|
||||||
# pick half the qty
|
# pick half the qty
|
||||||
for loc in pl.locations:
|
for loc in pl.locations:
|
||||||
@ -736,13 +734,11 @@ class TestPickList(FrappeTestCase):
|
|||||||
picked_serial_nos = [d.serial_no for d in data]
|
picked_serial_nos = [d.serial_no for d in data]
|
||||||
self.assertEqual(len(picked_serial_nos), 25)
|
self.assertEqual(len(picked_serial_nos), 25)
|
||||||
|
|
||||||
pl.save()
|
|
||||||
pl.submit()
|
|
||||||
|
|
||||||
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
|
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
|
# pick half the qty
|
||||||
for loc in pl.locations:
|
for loc in pl1.locations:
|
||||||
self.assertEqual(loc.qty, 10.0)
|
self.assertEqual(loc.qty, 10.0)
|
||||||
self.assertTrue(loc.serial_and_batch_bundle)
|
self.assertTrue(loc.serial_and_batch_bundle)
|
||||||
|
|
||||||
@ -756,8 +752,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
for d in data:
|
for d in data:
|
||||||
self.assertTrue(d.serial_no not in picked_serial_nos)
|
self.assertTrue(d.serial_no not in picked_serial_nos)
|
||||||
|
|
||||||
pl.save()
|
pl1.cancel()
|
||||||
pl.submit()
|
|
||||||
pl.cancel()
|
pl.cancel()
|
||||||
|
|
||||||
def test_picklist_with_bundles(self):
|
def test_picklist_with_bundles(self):
|
||||||
|
@ -24,8 +24,11 @@
|
|||||||
"serial_no_and_batch_section",
|
"serial_no_and_batch_section",
|
||||||
"pick_serial_and_batch",
|
"pick_serial_and_batch",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"serial_no",
|
"use_serial_batch_fields",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
|
"section_break_ecxc",
|
||||||
|
"serial_no",
|
||||||
|
"column_break_belw",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
@ -72,19 +75,17 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "serial_no",
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "batch_no",
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -195,6 +196,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -204,6 +206,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "pick_serial_and_batch",
|
"fieldname": "pick_serial_and_batch",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Pick Serial / Batch No"
|
"label": "Pick Serial / Batch No"
|
||||||
@ -218,11 +221,26 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 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,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-26 12:54:15.785962",
|
"modified": "2024-02-04 16:12:16.257951",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
@ -37,6 +37,7 @@ class PickListItem(Document):
|
|||||||
stock_reserved_qty: DF.Float
|
stock_reserved_qty: DF.Float
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
@ -369,6 +369,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
else:
|
else:
|
||||||
self.db_set("status", "Completed")
|
self.db_set("status", "Completed")
|
||||||
|
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
||||||
# depends upon updated ordered qty in PO
|
# depends upon updated ordered qty in PO
|
||||||
@ -1360,16 +1361,16 @@ def get_item_account_wise_additional_cost(purchase_document):
|
|||||||
for lcv in landed_cost_vouchers:
|
for lcv in landed_cost_vouchers:
|
||||||
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
|
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
|
# Use amount field for total item cost for manually cost distributed LCVs
|
||||||
if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
|
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
|
||||||
based_on_field = "amount"
|
|
||||||
else:
|
|
||||||
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
|
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
|
||||||
|
|
||||||
total_item_cost = 0
|
total_item_cost = 0
|
||||||
|
|
||||||
for item in landed_cost_voucher_doc.items:
|
if based_on_field:
|
||||||
total_item_cost += item.get(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:
|
for item in landed_cost_voucher_doc.items:
|
||||||
if item.receipt_document == purchase_document:
|
if item.receipt_document == purchase_document:
|
||||||
|
@ -2230,6 +2230,93 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr_doc.reload()
|
pr_doc.reload()
|
||||||
self.assertFalse(pr_doc.items[0].from_warehouse)
|
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():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
@ -2399,7 +2486,7 @@ def make_purchase_receipt(**args):
|
|||||||
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
|
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
|
||||||
|
|
||||||
bundle_id = None
|
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 = {}
|
batches = {}
|
||||||
if args.get("batch_no"):
|
if args.get("batch_no"):
|
||||||
batches = frappe._dict({args.batch_no: qty})
|
batches = frappe._dict({args.batch_no: qty})
|
||||||
@ -2441,6 +2528,9 @@ def make_purchase_receipt(**args):
|
|||||||
"cost_center": args.cost_center
|
"cost_center": args.cost_center
|
||||||
or frappe.get_cached_value("Company", pr.company, "cost_center"),
|
or frappe.get_cached_value("Company", pr.company, "cost_center"),
|
||||||
"asset_location": args.location or "Test Location",
|
"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",
|
"section_break_45",
|
||||||
"add_serial_batch_bundle",
|
"add_serial_batch_bundle",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
|
"use_serial_batch_fields",
|
||||||
"col_break5",
|
"col_break5",
|
||||||
"add_serial_batch_for_rejected_qty",
|
"add_serial_batch_for_rejected_qty",
|
||||||
"rejected_serial_and_batch_bundle",
|
"rejected_serial_and_batch_bundle",
|
||||||
@ -1003,6 +1004,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@ -1020,24 +1022,22 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "rejected_serial_no",
|
"fieldname": "rejected_serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Rejected Serial No",
|
"label": "Rejected Serial No"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "rejected_serial_and_batch_bundle",
|
"fieldname": "rejected_serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Rejected Serial and Batch Bundle",
|
"label": "Rejected Serial and Batch Bundle",
|
||||||
@ -1045,11 +1045,13 @@
|
|||||||
"options": "Serial and Batch Bundle"
|
"options": "Serial and Batch Bundle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "section_break_3vxt",
|
"fieldname": "section_break_3vxt",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
@ -1058,6 +1060,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||||
"fieldname": "add_serial_batch_bundle",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"label": "Add Serial / Batch No"
|
||||||
@ -1098,12 +1101,18 @@
|
|||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1,
|
"report_hide": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "use_serial_batch_fields",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Serial No / Batch Fields"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-25 22:32:09.801965",
|
"modified": "2024-02-04 11:48:06.653771",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
|
@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document):
|
|||||||
supplier_part_no: DF.Data | None
|
supplier_part_no: DF.Data | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
weight_per_unit: DF.Float
|
weight_per_unit: DF.Float
|
||||||
|
@ -281,6 +281,7 @@ def repost(doc):
|
|||||||
repost_gl_entries(doc)
|
repost_gl_entries(doc)
|
||||||
|
|
||||||
doc.set_status("Completed")
|
doc.set_status("Completed")
|
||||||
|
remove_attached_file(doc.name)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if frappe.flags.in_test:
|
if frappe.flags.in_test:
|
||||||
@ -309,6 +310,13 @@ def repost(doc):
|
|||||||
frappe.db.commit()
|
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):
|
def repost_sl_entries(doc):
|
||||||
if doc.based_on == "Transaction":
|
if doc.based_on == "Transaction":
|
||||||
repost_future_sle(
|
repost_future_sle(
|
||||||
|
@ -420,3 +420,38 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, riv.save)
|
self.assertRaises(frappe.ValidationError, riv.save)
|
||||||
doc.cancel()
|
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):
|
if isinstance(data, list):
|
||||||
return data
|
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()
|
@frappe.whitelist()
|
||||||
@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers(
|
|||||||
|
|
||||||
|
|
||||||
def get_type_of_transaction(parent_doc, child_row):
|
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":
|
if parent_doc.get("doctype") == "Stock Entry":
|
||||||
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
|
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}
|
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"):
|
if not kwargs.get("ignore_warehouse"):
|
||||||
filters["warehouse"] = ("is", "set")
|
filters["warehouse"] = ("is", "set")
|
||||||
if kwargs.warehouse:
|
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)
|
query = query.where(sb_entry.batch_no == kwargs.batch_no)
|
||||||
|
|
||||||
if kwargs.warehouse:
|
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:
|
if kwargs.ignore_voucher_nos:
|
||||||
query = query.where(sre.name.notin(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):
|
def test_old_batch_valuation(self):
|
||||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
batch_item_code = "Old Batch Item Valuation 1"
|
batch_item_code = "Old Batch Item Valuation 1"
|
||||||
make_item(
|
make_item(
|
||||||
batch_item_code,
|
batch_item_code,
|
||||||
@ -240,6 +241,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
bundle_doc.submit()
|
bundle_doc.submit()
|
||||||
|
|
||||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
|
|
||||||
def test_old_serial_no_valuation(self):
|
def test_old_serial_no_valuation(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
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.ignore_serial_batch_bundle_validation = True
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
|
|
||||||
serial_no_id = "Old Serial No 1"
|
serial_no_id = "Old Serial No 1"
|
||||||
if not frappe.db.exists("Serial No", serial_no_id):
|
if not frappe.db.exists("Serial No", serial_no_id):
|
||||||
@ -320,6 +323,9 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
for row in bundle_doc.entries:
|
for row in bundle_doc.entries:
|
||||||
self.assertEqual(flt(row.stock_value_difference, 2), -100.00)
|
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):
|
def test_batch_not_belong_to_serial_no(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
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):
|
if isinstance(serial_no, list):
|
||||||
return serial_no
|
return serial_no
|
||||||
|
|
||||||
return [
|
return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
|
||||||
s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def clean_serial_no_string(serial_no: str) -> str:
|
def clean_serial_no_string(serial_no: str) -> str:
|
||||||
|
@ -274,6 +274,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.update_work_order()
|
self.update_work_order()
|
||||||
self.validate_subcontract_order()
|
self.validate_subcontract_order()
|
||||||
|
@ -92,6 +92,9 @@ def make_stock_entry(**args):
|
|||||||
else:
|
else:
|
||||||
args.qty = cint(args.qty)
|
args.qty = cint(args.qty)
|
||||||
|
|
||||||
|
if args.serial_no or args.batch_no:
|
||||||
|
args.use_serial_batch_fields = True
|
||||||
|
|
||||||
# purpose
|
# purpose
|
||||||
if not args.purpose:
|
if not args.purpose:
|
||||||
if args.source and args.target:
|
if args.source and args.target:
|
||||||
@ -162,6 +165,7 @@ def make_stock_entry(**args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
args.serial_no = serial_number
|
args.serial_no = serial_number
|
||||||
|
|
||||||
s.append(
|
s.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
@ -177,6 +181,7 @@ def make_stock_entry(**args):
|
|||||||
"batch_no": args.batch_no,
|
"batch_no": args.batch_no,
|
||||||
"cost_center": args.cost_center,
|
"cost_center": args.cost_center,
|
||||||
"expense_account": args.expense_account,
|
"expense_account": args.expense_account,
|
||||||
|
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -680,6 +680,7 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
def test_serial_move(self):
|
def test_serial_move(self):
|
||||||
se = make_serialized_item()
|
se = make_serialized_item()
|
||||||
serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
|
serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
|
|
||||||
se = frappe.copy_doc(test_records[0])
|
se = frappe.copy_doc(test_records[0])
|
||||||
se.purpose = "Material Transfer"
|
se.purpose = "Material Transfer"
|
||||||
@ -700,6 +701,7 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
|
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
|
||||||
)
|
)
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
|
|
||||||
def test_serial_cancel(self):
|
def test_serial_cancel(self):
|
||||||
se, serial_nos = self.test_serial_by_series()
|
se, serial_nos = self.test_serial_by_series()
|
||||||
@ -999,6 +1001,8 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
|
|
||||||
cls_obj = SerialBatchCreation(
|
cls_obj = SerialBatchCreation(
|
||||||
{
|
{
|
||||||
"type_of_transaction": "Inward",
|
"type_of_transaction": "Inward",
|
||||||
@ -1035,84 +1039,7 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
|
|
||||||
s2.submit()
|
s2.submit()
|
||||||
s2.cancel()
|
s2.cancel()
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
# def test_retain_sample(self):
|
|
||||||
# from erpnext.stock.doctype.batch.batch import get_batch_qty
|
|
||||||
# from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
|
||||||
|
|
||||||
# create_warehouse("Test Warehouse for Sample Retention")
|
|
||||||
# frappe.db.set_value(
|
|
||||||
# "Stock Settings",
|
|
||||||
# None,
|
|
||||||
# "sample_retention_warehouse",
|
|
||||||
# "Test Warehouse for Sample Retention - _TC",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# test_item_code = "Retain Sample Item"
|
|
||||||
# if not frappe.db.exists("Item", test_item_code):
|
|
||||||
# item = frappe.new_doc("Item")
|
|
||||||
# item.item_code = test_item_code
|
|
||||||
# item.item_name = "Retain Sample Item"
|
|
||||||
# item.description = "Retain Sample Item"
|
|
||||||
# item.item_group = "All Item Groups"
|
|
||||||
# item.is_stock_item = 1
|
|
||||||
# item.has_batch_no = 1
|
|
||||||
# item.create_new_batch = 1
|
|
||||||
# item.retain_sample = 1
|
|
||||||
# item.sample_quantity = 4
|
|
||||||
# item.save()
|
|
||||||
|
|
||||||
# receipt_entry = frappe.new_doc("Stock Entry")
|
|
||||||
# receipt_entry.company = "_Test Company"
|
|
||||||
# receipt_entry.purpose = "Material Receipt"
|
|
||||||
# receipt_entry.append(
|
|
||||||
# "items",
|
|
||||||
# {
|
|
||||||
# "item_code": test_item_code,
|
|
||||||
# "t_warehouse": "_Test Warehouse - _TC",
|
|
||||||
# "qty": 40,
|
|
||||||
# "basic_rate": 12,
|
|
||||||
# "cost_center": "_Test Cost Center - _TC",
|
|
||||||
# "sample_quantity": 4,
|
|
||||||
# },
|
|
||||||
# )
|
|
||||||
# receipt_entry.set_stock_entry_type()
|
|
||||||
# receipt_entry.insert()
|
|
||||||
# receipt_entry.submit()
|
|
||||||
|
|
||||||
# retention_data = move_sample_to_retention_warehouse(
|
|
||||||
# receipt_entry.company, receipt_entry.get("items")
|
|
||||||
# )
|
|
||||||
# retention_entry = frappe.new_doc("Stock Entry")
|
|
||||||
# retention_entry.company = retention_data.company
|
|
||||||
# retention_entry.purpose = retention_data.purpose
|
|
||||||
# retention_entry.append(
|
|
||||||
# "items",
|
|
||||||
# {
|
|
||||||
# "item_code": test_item_code,
|
|
||||||
# "t_warehouse": "Test Warehouse for Sample Retention - _TC",
|
|
||||||
# "s_warehouse": "_Test Warehouse - _TC",
|
|
||||||
# "qty": 4,
|
|
||||||
# "basic_rate": 12,
|
|
||||||
# "cost_center": "_Test Cost Center - _TC",
|
|
||||||
# "batch_no": get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
|
|
||||||
# },
|
|
||||||
# )
|
|
||||||
# retention_entry.set_stock_entry_type()
|
|
||||||
# retention_entry.insert()
|
|
||||||
# retention_entry.submit()
|
|
||||||
|
|
||||||
# qty_in_usable_warehouse = get_batch_qty(
|
|
||||||
# get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), "_Test Warehouse - _TC", "_Test Item"
|
|
||||||
# )
|
|
||||||
# qty_in_retention_warehouse = get_batch_qty(
|
|
||||||
# get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
|
|
||||||
# "Test Warehouse for Sample Retention - _TC",
|
|
||||||
# "_Test Item",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# self.assertEqual(qty_in_usable_warehouse, 36)
|
|
||||||
# self.assertEqual(qty_in_retention_warehouse, 4)
|
|
||||||
|
|
||||||
def test_quality_check(self):
|
def test_quality_check(self):
|
||||||
item_code = "_Test Item For QC"
|
item_code = "_Test Item For QC"
|
||||||
@ -1785,6 +1712,48 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, se1.cancel)
|
self.assertRaises(frappe.ValidationError, se1.cancel)
|
||||||
|
|
||||||
|
def test_auto_reorder_level(self):
|
||||||
|
from erpnext.stock.reorder_item import reorder_item
|
||||||
|
|
||||||
|
item_doc = make_item(
|
||||||
|
"Test Auto Reorder Item - 001",
|
||||||
|
properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1},
|
||||||
|
uoms=[{"uom": "Nos", "conversion_factor": 5}],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
|
||||||
|
item_doc.append(
|
||||||
|
"reorder_levels",
|
||||||
|
{
|
||||||
|
"warehouse_reorder_level": 0,
|
||||||
|
"warehouse_reorder_qty": 10,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"material_request_type": "Purchase",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
item_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_indent", 1)
|
||||||
|
|
||||||
|
mr_list = reorder_item()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
|
||||||
|
mrs = frappe.get_all(
|
||||||
|
"Material Request Item",
|
||||||
|
fields=["qty", "stock_uom", "stock_qty"],
|
||||||
|
filters={"item_code": item_doc.name, "uom": "Nos"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for mri in mrs:
|
||||||
|
self.assertEqual(mri.stock_uom, "Kg")
|
||||||
|
self.assertEqual(mri.stock_qty, 10)
|
||||||
|
self.assertEqual(mri.qty, 2)
|
||||||
|
|
||||||
|
for mr in mr_list:
|
||||||
|
mr.cancel()
|
||||||
|
mr.delete()
|
||||||
|
|
||||||
|
|
||||||
def make_serialized_item(**args):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -47,9 +47,12 @@
|
|||||||
"amount",
|
"amount",
|
||||||
"serial_no_batch",
|
"serial_no_batch",
|
||||||
"add_serial_batch_bundle",
|
"add_serial_batch_bundle",
|
||||||
"serial_and_batch_bundle",
|
"use_serial_batch_fields",
|
||||||
"col_break4",
|
"col_break4",
|
||||||
|
"serial_and_batch_bundle",
|
||||||
|
"section_break_rdtg",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
|
"column_break_prps",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"accounting",
|
"accounting",
|
||||||
"expense_account",
|
"expense_account",
|
||||||
@ -289,27 +292,27 @@
|
|||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "serial_no",
|
"oldfieldname": "serial_no",
|
||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "col_break4",
|
"fieldname": "col_break4",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "batch_no",
|
"oldfieldname": "batch_no",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Batch",
|
"options": "Batch"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.inspection_required && doc.t_warehouse",
|
"depends_on": "eval:parent.inspection_required && doc.t_warehouse",
|
||||||
@ -573,24 +576,41 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||||
"fieldname": "add_serial_batch_bundle",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"label": "Add Serial / Batch No"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"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_rdtg",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_prps",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-12 11:56:04.626103",
|
"modified": "2024-02-04 16:16:47.606270",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
@ -63,6 +63,7 @@ class StockEntryDetail(Document):
|
|||||||
transfer_qty: DF.Float
|
transfer_qty: DF.Float
|
||||||
transferred_qty: DF.Float
|
transferred_qty: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
@ -93,6 +93,9 @@ class StockLedgerEntry(Document):
|
|||||||
self.validate_inventory_dimension_negative_stock()
|
self.validate_inventory_dimension_negative_stock()
|
||||||
|
|
||||||
def validate_inventory_dimension_negative_stock(self):
|
def validate_inventory_dimension_negative_stock(self):
|
||||||
|
if self.is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
extra_cond = ""
|
extra_cond = ""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
@ -482,6 +482,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
|||||||
(item, warehouses[0], batches[1], 1, 200),
|
(item, warehouses[0], batches[1], 1, 200),
|
||||||
(item, warehouses[0], batches[0], 1, 200),
|
(item, warehouses[0], batches[0], 1, 200),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
frappe.flags.use_serial_and_batch_fields = True
|
||||||
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
|
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
|
||||||
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
|
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
|
||||||
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
|
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
|
||||||
@ -494,6 +496,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
|||||||
"Incorrect 'Incoming Rate' values fetched for DN items",
|
"Incorrect 'Incoming Rate' values fetched for DN items",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
frappe.flags.use_serial_and_batch_fields = False
|
||||||
|
|
||||||
def test_batchwise_item_valuation_stock_reco(self):
|
def test_batchwise_item_valuation_stock_reco(self):
|
||||||
item, warehouses, batches = setup_item_valuation_test()
|
item, warehouses, batches = setup_item_valuation_test()
|
||||||
state = {"stock_value": 0.0, "qty": 0.0}
|
state = {"stock_value": 0.0, "qty": 0.0}
|
||||||
|
@ -198,6 +198,7 @@ frappe.ui.form.on("Stock Reconciliation", {
|
|||||||
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
|
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
|
||||||
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
|
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
|
||||||
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
|
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
|
||||||
|
frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields);
|
||||||
|
|
||||||
if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
|
if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
|
||||||
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
|
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
|
||||||
|
@ -99,6 +99,8 @@ class StockReconciliation(StockController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.make_bundle_for_current_qty()
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
@ -116,9 +118,52 @@ class StockReconciliation(StockController):
|
|||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.delete_auto_created_batches()
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
|
def make_bundle_for_current_qty(self):
|
||||||
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
if not row.use_serial_batch_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if row.current_serial_and_batch_bundle:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if row.current_qty and (row.current_serial_no or row.batch_no):
|
||||||
|
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": row.qty,
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
"company": self.company,
|
||||||
|
"is_rejected": 0,
|
||||||
|
"serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None,
|
||||||
|
"batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None,
|
||||||
|
"batch_no": row.batch_no,
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
).make_serial_and_batch_bundle()
|
||||||
|
|
||||||
|
row.current_serial_and_batch_bundle = sn_doc.name
|
||||||
|
row.db_set(
|
||||||
|
{
|
||||||
|
"current_serial_and_batch_bundle": sn_doc.name,
|
||||||
|
"current_serial_no": "",
|
||||||
|
"batch_no": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
||||||
"""Set Serial and Batch Bundle for each item"""
|
"""Set Serial and Batch Bundle for each item"""
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
if not save and item.use_serial_batch_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
if voucher_detail_no and voucher_detail_no != item.name:
|
if voucher_detail_no and voucher_detail_no != item.name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -229,6 +274,9 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
def set_new_serial_and_batch_bundle(self):
|
def set_new_serial_and_batch_bundle(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
if item.use_serial_batch_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
if not item.qty:
|
if not item.qty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -291,8 +339,10 @@ class StockReconciliation(StockController):
|
|||||||
inventory_dimensions_dict=inventory_dimensions_dict,
|
inventory_dimensions_dict=inventory_dimensions_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (item.qty is None or item.qty == item_dict.get("qty")) and (
|
if (
|
||||||
item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
|
(item.qty is None or item.qty == item_dict.get("qty"))
|
||||||
|
and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
|
||||||
|
and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -303,6 +353,11 @@ class StockReconciliation(StockController):
|
|||||||
if item.valuation_rate is None:
|
if item.valuation_rate is None:
|
||||||
item.valuation_rate = item_dict.get("rate")
|
item.valuation_rate = item_dict.get("rate")
|
||||||
|
|
||||||
|
if item_dict.get("serial_nos"):
|
||||||
|
item.current_serial_no = item_dict.get("serial_nos")
|
||||||
|
if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
|
||||||
|
item.serial_no = item.current_serial_no
|
||||||
|
|
||||||
item.current_qty = item_dict.get("qty")
|
item.current_qty = item_dict.get("qty")
|
||||||
item.current_valuation_rate = item_dict.get("rate")
|
item.current_valuation_rate = item_dict.get("rate")
|
||||||
self.calculate_difference_amount(item, item_dict)
|
self.calculate_difference_amount(item, item_dict)
|
||||||
@ -1135,9 +1190,16 @@ def get_stock_balance_for(
|
|||||||
has_serial_no = bool(item_dict.get("has_serial_no"))
|
has_serial_no = bool(item_dict.get("has_serial_no"))
|
||||||
has_batch_no = bool(item_dict.get("has_batch_no"))
|
has_batch_no = bool(item_dict.get("has_batch_no"))
|
||||||
|
|
||||||
|
use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||||
|
|
||||||
if not batch_no and has_batch_no:
|
if not batch_no and has_batch_no:
|
||||||
# Not enough information to fetch data
|
# Not enough information to fetch data
|
||||||
return {"qty": 0, "rate": 0, "serial_nos": None}
|
return {
|
||||||
|
"qty": 0,
|
||||||
|
"rate": 0,
|
||||||
|
"serial_nos": None,
|
||||||
|
"use_serial_batch_fields": use_serial_batch_fields,
|
||||||
|
}
|
||||||
|
|
||||||
# TODO: fetch only selected batch's values
|
# TODO: fetch only selected batch's values
|
||||||
data = get_stock_balance(
|
data = get_stock_balance(
|
||||||
@ -1160,7 +1222,12 @@ def get_stock_balance_for(
|
|||||||
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
|
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
|
return {
|
||||||
|
"qty": qty,
|
||||||
|
"rate": rate,
|
||||||
|
"serial_nos": serial_nos,
|
||||||
|
"use_serial_batch_fields": use_serial_batch_fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -1094,7 +1094,7 @@ def create_stock_reconciliation(**args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
bundle_id = None
|
bundle_id = None
|
||||||
if args.batch_no or args.serial_no:
|
if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
|
||||||
batches = frappe._dict({})
|
batches = frappe._dict({})
|
||||||
if args.batch_no:
|
if args.batch_no:
|
||||||
batches[args.batch_no] = args.qty
|
batches[args.batch_no] = args.qty
|
||||||
@ -1125,7 +1125,10 @@ def create_stock_reconciliation(**args):
|
|||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"qty": args.qty,
|
"qty": args.qty,
|
||||||
"valuation_rate": args.rate,
|
"valuation_rate": args.rate,
|
||||||
|
"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,
|
||||||
"serial_and_batch_bundle": bundle_id,
|
"serial_and_batch_bundle": bundle_id,
|
||||||
|
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,11 +19,14 @@
|
|||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"serial_no_and_batch_section",
|
"serial_no_and_batch_section",
|
||||||
"add_serial_batch_bundle",
|
"add_serial_batch_bundle",
|
||||||
"serial_and_batch_bundle",
|
"use_serial_batch_fields",
|
||||||
"batch_no",
|
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
|
"serial_and_batch_bundle",
|
||||||
"current_serial_and_batch_bundle",
|
"current_serial_and_batch_bundle",
|
||||||
|
"section_break_lypk",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
|
"column_break_eefq",
|
||||||
|
"batch_no",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"current_qty",
|
"current_qty",
|
||||||
"current_amount",
|
"current_amount",
|
||||||
@ -103,10 +106,10 @@
|
|||||||
"label": "Serial No and Batch"
|
"label": "Serial No and Batch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Long Text",
|
"fieldtype": "Long Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_11",
|
"fieldname": "column_break_11",
|
||||||
@ -171,11 +174,11 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -195,6 +198,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial / Batch Bundle",
|
"label": "Serial / Batch Bundle",
|
||||||
@ -204,6 +208,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "current_serial_and_batch_bundle",
|
"fieldname": "current_serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Current Serial / Batch Bundle",
|
"label": "Current Serial / Batch Bundle",
|
||||||
@ -212,6 +217,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "add_serial_batch_bundle",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"label": "Add Serial / Batch No"
|
||||||
@ -222,11 +228,26 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item Group",
|
"label": "Item Group",
|
||||||
"options": "Item Group"
|
"options": "Item Group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_lypk",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_eefq",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-14 10:04:23.599951",
|
"modified": "2024-02-04 16:19:44.576022",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation Item",
|
"name": "Stock Reconciliation Item",
|
||||||
|
@ -26,6 +26,7 @@ class StockReconciliationItem(Document):
|
|||||||
current_valuation_rate: DF.Currency
|
current_valuation_rate: DF.Currency
|
||||||
has_item_scanned: DF.Data | None
|
has_item_scanned: DF.Data | None
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
|
item_group: DF.Link | None
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
@ -34,6 +35,7 @@ class StockReconciliationItem(Document):
|
|||||||
quantity_difference: DF.ReadOnly | None
|
quantity_difference: DF.ReadOnly | None
|
||||||
serial_and_batch_bundle: DF.Link | None
|
serial_and_batch_bundle: DF.Link | None
|
||||||
serial_no: DF.LongText | None
|
serial_no: DF.LongText | None
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_rate: DF.Currency
|
valuation_rate: DF.Currency
|
||||||
warehouse: DF.Link
|
warehouse: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
@ -315,7 +315,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-19 16:41:16.545416",
|
"modified": "2024-02-07 16:05:17.772098",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
@ -335,6 +335,90 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
"disable_serial_no_and_batch_selector",
|
"disable_serial_no_and_batch_selector",
|
||||||
"use_naming_series",
|
"use_naming_series",
|
||||||
"naming_series_prefix",
|
"naming_series_prefix",
|
||||||
|
"use_serial_batch_fields",
|
||||||
"stock_planning_tab",
|
"stock_planning_tab",
|
||||||
"auto_material_request",
|
"auto_material_request",
|
||||||
"auto_indent",
|
"auto_indent",
|
||||||
@ -420,6 +421,12 @@
|
|||||||
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "use_serial_batch_fields",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Serial / Batch Fields"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@ -427,7 +434,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-30 14:03:52.143457",
|
"modified": "2024-02-04 12:01:31.931864",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
@ -57,6 +57,7 @@ class StockSettings(Document):
|
|||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
update_existing_price_list_rate: DF.Check
|
update_existing_price_list_rate: DF.Check
|
||||||
use_naming_series: DF.Check
|
use_naming_series: DF.Check
|
||||||
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
|
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ class StockSettings(Document):
|
|||||||
"allow_negative_stock",
|
"allow_negative_stock",
|
||||||
"default_warehouse",
|
"default_warehouse",
|
||||||
"set_qty_in_transactions_based_on_serial_no_input",
|
"set_qty_in_transactions_based_on_serial_no_input",
|
||||||
|
"use_serial_batch_fields",
|
||||||
]:
|
]:
|
||||||
frappe.db.set_default(key, self.get(key, ""))
|
frappe.db.set_default(key, self.get(key, ""))
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"is_group",
|
"is_group",
|
||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
|
"is_rejected_warehouse",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"account",
|
"account",
|
||||||
"company",
|
"company",
|
||||||
@ -249,13 +250,20 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_qajx",
|
"fieldname": "column_break_qajx",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If yes, then this warehouse will be used to store rejected materials",
|
||||||
|
"fieldname": "is_rejected_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Rejected Warehouse"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-building",
|
"icon": "fa fa-building",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-29 13:10:43.333160",
|
"modified": "2024-01-24 16:27:28.299520",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Warehouse",
|
"name": "Warehouse",
|
||||||
|
@ -86,7 +86,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
|
|
||||||
get_party_item_code(args, item, out)
|
get_party_item_code(args, item, out)
|
||||||
|
|
||||||
set_valuation_rate(out, args)
|
if args.get("doctype") in ["Sales Order", "Quotation"]:
|
||||||
|
set_valuation_rate(out, args)
|
||||||
|
|
||||||
update_party_blanket_order(args, out)
|
update_party_blanket_order(args, out)
|
||||||
|
|
||||||
@ -269,7 +270,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
|||||||
if not item:
|
if not item:
|
||||||
item = frappe.get_doc("Item", args.get("item_code"))
|
item = frappe.get_doc("Item", args.get("item_code"))
|
||||||
|
|
||||||
if item.variant_of and not item.taxes:
|
if (
|
||||||
|
item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of})
|
||||||
|
):
|
||||||
item.update_template_tables()
|
item.update_template_tables()
|
||||||
|
|
||||||
item_defaults = get_item_defaults(item.name, args.company)
|
item_defaults = get_item_defaults(item.name, args.company)
|
||||||
@ -543,7 +546,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
|
|||||||
args = {
|
args = {
|
||||||
"company": company,
|
"company": company,
|
||||||
"tax_category": tax_category,
|
"tax_category": tax_category,
|
||||||
"net_rate": item_rates.get(item_code[1]),
|
"base_net_rate": item_rates.get(item_code[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if item_tax_templates:
|
if item_tax_templates:
|
||||||
@ -635,7 +638,7 @@ def is_within_valid_range(args, tax):
|
|||||||
if not flt(tax.maximum_net_rate):
|
if not flt(tax.maximum_net_rate):
|
||||||
# No range specified, just ignore
|
# No range specified, just ignore
|
||||||
return True
|
return True
|
||||||
elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate):
|
elif flt(tax.minimum_net_rate) <= flt(args.get("base_net_rate")) <= flt(tax.maximum_net_rate):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -34,73 +34,157 @@ def _reorder_item():
|
|||||||
erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
|
erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
|
||||||
)
|
)
|
||||||
|
|
||||||
items_to_consider = frappe.db.sql_list(
|
items_to_consider = get_items_for_reorder()
|
||||||
"""select name from `tabItem` item
|
|
||||||
where is_stock_item=1 and has_variants=0
|
|
||||||
and disabled=0
|
|
||||||
and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s)
|
|
||||||
and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name)
|
|
||||||
or (variant_of is not null and variant_of != ''
|
|
||||||
and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of))
|
|
||||||
)""",
|
|
||||||
{"today": nowdate()},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not items_to_consider:
|
if not items_to_consider:
|
||||||
return
|
return
|
||||||
|
|
||||||
item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
|
item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
|
||||||
|
|
||||||
def add_to_material_request(
|
def add_to_material_request(**kwargs):
|
||||||
item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None
|
if isinstance(kwargs, dict):
|
||||||
):
|
kwargs = frappe._dict(kwargs)
|
||||||
if warehouse not in warehouse_company:
|
|
||||||
|
if kwargs.warehouse not in warehouse_company:
|
||||||
# a disabled warehouse
|
# a disabled warehouse
|
||||||
return
|
return
|
||||||
|
|
||||||
reorder_level = flt(reorder_level)
|
reorder_level = flt(kwargs.reorder_level)
|
||||||
reorder_qty = flt(reorder_qty)
|
reorder_qty = flt(kwargs.reorder_qty)
|
||||||
|
|
||||||
# projected_qty will be 0 if Bin does not exist
|
# projected_qty will be 0 if Bin does not exist
|
||||||
if warehouse_group:
|
if kwargs.warehouse_group:
|
||||||
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group))
|
projected_qty = flt(
|
||||||
|
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
|
projected_qty = flt(
|
||||||
|
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
|
||||||
|
)
|
||||||
|
|
||||||
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
|
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
|
||||||
deficiency = reorder_level - projected_qty
|
deficiency = reorder_level - projected_qty
|
||||||
if deficiency > reorder_qty:
|
if deficiency > reorder_qty:
|
||||||
reorder_qty = deficiency
|
reorder_qty = deficiency
|
||||||
|
|
||||||
company = warehouse_company.get(warehouse) or default_company
|
company = warehouse_company.get(kwargs.warehouse) or default_company
|
||||||
|
|
||||||
material_requests[material_request_type].setdefault(company, []).append(
|
material_requests[kwargs.material_request_type].setdefault(company, []).append(
|
||||||
{"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty}
|
{
|
||||||
|
"item_code": kwargs.item_code,
|
||||||
|
"warehouse": kwargs.warehouse,
|
||||||
|
"reorder_qty": reorder_qty,
|
||||||
|
"item_details": kwargs.item_details,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
for item_code in items_to_consider:
|
for item_code, reorder_levels in items_to_consider.items():
|
||||||
item = frappe.get_doc("Item", item_code)
|
for d in reorder_levels:
|
||||||
|
if d.has_variants:
|
||||||
|
continue
|
||||||
|
|
||||||
if item.variant_of and not item.get("reorder_levels"):
|
add_to_material_request(
|
||||||
item.update_template_tables()
|
item_code=item_code,
|
||||||
|
warehouse=d.warehouse,
|
||||||
if item.get("reorder_levels"):
|
reorder_level=d.warehouse_reorder_level,
|
||||||
for d in item.get("reorder_levels"):
|
reorder_qty=d.warehouse_reorder_qty,
|
||||||
add_to_material_request(
|
material_request_type=d.material_request_type,
|
||||||
item_code,
|
warehouse_group=d.warehouse_group,
|
||||||
d.warehouse,
|
item_details=frappe._dict(
|
||||||
d.warehouse_reorder_level,
|
{
|
||||||
d.warehouse_reorder_qty,
|
"item_code": item_code,
|
||||||
d.material_request_type,
|
"name": item_code,
|
||||||
warehouse_group=d.warehouse_group,
|
"item_name": d.item_name,
|
||||||
)
|
"item_group": d.item_group,
|
||||||
|
"brand": d.brand,
|
||||||
|
"description": d.description,
|
||||||
|
"stock_uom": d.stock_uom,
|
||||||
|
"purchase_uom": d.purchase_uom,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if material_requests:
|
if material_requests:
|
||||||
return create_material_request(material_requests)
|
return create_material_request(material_requests)
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_for_reorder() -> dict[str, list]:
|
||||||
|
reorder_table = frappe.qb.DocType("Item Reorder")
|
||||||
|
item_table = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(reorder_table)
|
||||||
|
.inner_join(item_table)
|
||||||
|
.on(reorder_table.parent == item_table.name)
|
||||||
|
.select(
|
||||||
|
reorder_table.warehouse,
|
||||||
|
reorder_table.warehouse_group,
|
||||||
|
reorder_table.material_request_type,
|
||||||
|
reorder_table.warehouse_reorder_level,
|
||||||
|
reorder_table.warehouse_reorder_qty,
|
||||||
|
item_table.name,
|
||||||
|
item_table.stock_uom,
|
||||||
|
item_table.purchase_uom,
|
||||||
|
item_table.description,
|
||||||
|
item_table.item_name,
|
||||||
|
item_table.item_group,
|
||||||
|
item_table.brand,
|
||||||
|
item_table.variant_of,
|
||||||
|
item_table.has_variants,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(item_table.disabled == 0)
|
||||||
|
& (item_table.is_stock_item == 1)
|
||||||
|
& (
|
||||||
|
(item_table.end_of_life.isnull())
|
||||||
|
| (item_table.end_of_life > nowdate())
|
||||||
|
| (item_table.end_of_life == "0000-00-00")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
itemwise_reorder = frappe._dict({})
|
||||||
|
for d in data:
|
||||||
|
itemwise_reorder.setdefault(d.name, []).append(d)
|
||||||
|
|
||||||
|
itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
|
||||||
|
|
||||||
|
return itemwise_reorder
|
||||||
|
|
||||||
|
|
||||||
|
def get_reorder_levels_for_variants(itemwise_reorder):
|
||||||
|
item_table = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(item_table)
|
||||||
|
.select(
|
||||||
|
item_table.name,
|
||||||
|
item_table.variant_of,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(item_table.disabled == 0)
|
||||||
|
& (item_table.is_stock_item == 1)
|
||||||
|
& (
|
||||||
|
(item_table.end_of_life.isnull())
|
||||||
|
| (item_table.end_of_life > nowdate())
|
||||||
|
| (item_table.end_of_life == "0000-00-00")
|
||||||
|
)
|
||||||
|
& (item_table.variant_of.notnull())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
variants_item = query.run(as_dict=True)
|
||||||
|
for row in variants_item:
|
||||||
|
if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
|
||||||
|
itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
|
||||||
|
|
||||||
|
return itemwise_reorder
|
||||||
|
|
||||||
|
|
||||||
def get_item_warehouse_projected_qty(items_to_consider):
|
def get_item_warehouse_projected_qty(items_to_consider):
|
||||||
item_warehouse_projected_qty = {}
|
item_warehouse_projected_qty = {}
|
||||||
|
items_to_consider = list(items_to_consider.keys())
|
||||||
|
|
||||||
for item_code, warehouse, projected_qty in frappe.db.sql(
|
for item_code, warehouse, projected_qty in frappe.db.sql(
|
||||||
"""select item_code, warehouse, projected_qty
|
"""select item_code, warehouse, projected_qty
|
||||||
@ -164,7 +248,7 @@ def create_material_request(material_requests):
|
|||||||
|
|
||||||
for d in items:
|
for d in items:
|
||||||
d = frappe._dict(d)
|
d = frappe._dict(d)
|
||||||
item = frappe.get_doc("Item", d.item_code)
|
item = d.get("item_details")
|
||||||
uom = item.stock_uom
|
uom = item.stock_uom
|
||||||
conversion_factor = 1.0
|
conversion_factor = 1.0
|
||||||
|
|
||||||
@ -190,6 +274,7 @@ def create_material_request(material_requests):
|
|||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
"schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
|
"schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
|
"conversion_factor": conversion_factor,
|
||||||
"uom": uom,
|
"uom": uom,
|
||||||
"stock_uom": item.stock_uom,
|
"stock_uom": item.stock_uom,
|
||||||
"warehouse": d.warehouse,
|
"warehouse": d.warehouse,
|
||||||
|
@ -90,8 +90,7 @@ class StockBalanceReport(object):
|
|||||||
self.opening_data.setdefault(group_by_key, entry)
|
self.opening_data.setdefault(group_by_key, entry)
|
||||||
|
|
||||||
def prepare_new_data(self):
|
def prepare_new_data(self):
|
||||||
if not self.sle_entries:
|
self.item_warehouse_map = self.get_item_warehouse_map()
|
||||||
return
|
|
||||||
|
|
||||||
if self.filters.get("show_stock_ageing_data"):
|
if self.filters.get("show_stock_ageing_data"):
|
||||||
self.filters["show_warehouse_wise_stock"] = True
|
self.filters["show_warehouse_wise_stock"] = True
|
||||||
@ -99,7 +98,8 @@ class StockBalanceReport(object):
|
|||||||
|
|
||||||
_func = itemgetter(1)
|
_func = itemgetter(1)
|
||||||
|
|
||||||
self.item_warehouse_map = self.get_item_warehouse_map()
|
del self.sle_entries
|
||||||
|
|
||||||
sre_details = self.get_sre_reserved_qty_details()
|
sre_details = self.get_sre_reserved_qty_details()
|
||||||
|
|
||||||
variant_values = {}
|
variant_values = {}
|
||||||
@ -143,15 +143,22 @@ class StockBalanceReport(object):
|
|||||||
item_warehouse_map = {}
|
item_warehouse_map = {}
|
||||||
self.opening_vouchers = self.get_opening_vouchers()
|
self.opening_vouchers = self.get_opening_vouchers()
|
||||||
|
|
||||||
for entry in self.sle_entries:
|
if self.filters.get("show_stock_ageing_data"):
|
||||||
group_by_key = self.get_group_by_key(entry)
|
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||||
if group_by_key not in item_warehouse_map:
|
|
||||||
self.initialize_data(item_warehouse_map, group_by_key, entry)
|
|
||||||
|
|
||||||
self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
|
with frappe.db.unbuffered_cursor():
|
||||||
|
if not self.filters.get("show_stock_ageing_data"):
|
||||||
|
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
||||||
|
|
||||||
if self.opening_data.get(group_by_key):
|
for entry in self.sle_entries:
|
||||||
del self.opening_data[group_by_key]
|
group_by_key = self.get_group_by_key(entry)
|
||||||
|
if group_by_key not in item_warehouse_map:
|
||||||
|
self.initialize_data(item_warehouse_map, group_by_key, entry)
|
||||||
|
|
||||||
|
self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
|
||||||
|
|
||||||
|
if self.opening_data.get(group_by_key):
|
||||||
|
del self.opening_data[group_by_key]
|
||||||
|
|
||||||
for group_by_key, entry in self.opening_data.items():
|
for group_by_key, entry in self.opening_data.items():
|
||||||
if group_by_key not in item_warehouse_map:
|
if group_by_key not in item_warehouse_map:
|
||||||
@ -252,7 +259,8 @@ class StockBalanceReport(object):
|
|||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
& (table.company == self.filters.company)
|
& (table.company == self.filters.company)
|
||||||
& ((table.to_date <= self.from_date))
|
& (table.to_date <= self.from_date)
|
||||||
|
& (table.status == "Completed")
|
||||||
)
|
)
|
||||||
.orderby(table.to_date, order=Order.desc)
|
.orderby(table.to_date, order=Order.desc)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@ -305,7 +313,7 @@ class StockBalanceReport(object):
|
|||||||
if self.filters.get("company"):
|
if self.filters.get("company"):
|
||||||
query = query.where(sle.company == self.filters.get("company"))
|
query = query.where(sle.company == self.filters.get("company"))
|
||||||
|
|
||||||
self.sle_entries = query.run(as_dict=True)
|
self.sle_query = query
|
||||||
|
|
||||||
def apply_inventory_dimensions_filters(self, query, sle) -> str:
|
def apply_inventory_dimensions_filters(self, query, sle) -> str:
|
||||||
inventory_dimension_fields = self.get_inventory_dimension_fields()
|
inventory_dimension_fields = self.get_inventory_dimension_fields()
|
||||||
|
@ -283,6 +283,7 @@ class SerialBatchBundle:
|
|||||||
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
||||||
else "Inactive",
|
else "Inactive",
|
||||||
)
|
)
|
||||||
|
.set(sn_table.company, self.sle.company)
|
||||||
.where(sn_table.name.isin(serial_nos))
|
.where(sn_table.name.isin(serial_nos))
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
@ -793,6 +794,9 @@ class SerialBatchCreation:
|
|||||||
setattr(self, "actual_qty", qty)
|
setattr(self, "actual_qty", qty)
|
||||||
self.__dict__["actual_qty"] = self.actual_qty
|
self.__dict__["actual_qty"] = self.actual_qty
|
||||||
|
|
||||||
|
if not hasattr(self, "use_serial_batch_fields"):
|
||||||
|
setattr(self, "use_serial_batch_fields", 0)
|
||||||
|
|
||||||
def duplicate_package(self):
|
def duplicate_package(self):
|
||||||
if not self.serial_and_batch_bundle:
|
if not self.serial_and_batch_bundle:
|
||||||
return
|
return
|
||||||
@ -901,9 +905,14 @@ class SerialBatchCreation:
|
|||||||
self.batches = get_available_batches(kwargs)
|
self.batches = get_available_batches(kwargs)
|
||||||
|
|
||||||
def set_auto_serial_batch_entries_for_inward(self):
|
def set_auto_serial_batch_entries_for_inward(self):
|
||||||
|
print(self.get("serial_nos"))
|
||||||
|
|
||||||
if (self.get("batches") and self.has_batch_no) or (
|
if (self.get("batches") and self.has_batch_no) or (
|
||||||
self.get("serial_nos") and self.has_serial_no
|
self.get("serial_nos") and self.has_serial_no
|
||||||
):
|
):
|
||||||
|
if self.use_serial_batch_fields and self.get("serial_nos"):
|
||||||
|
self.make_serial_no_if_not_exists()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.batch_no = None
|
self.batch_no = None
|
||||||
@ -915,6 +924,59 @@ class SerialBatchCreation:
|
|||||||
else:
|
else:
|
||||||
self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
|
self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
|
||||||
|
|
||||||
|
def make_serial_no_if_not_exists(self):
|
||||||
|
non_exists_serial_nos = []
|
||||||
|
for row in self.serial_nos:
|
||||||
|
if not frappe.db.exists("Serial No", row):
|
||||||
|
non_exists_serial_nos.append(row)
|
||||||
|
|
||||||
|
if non_exists_serial_nos:
|
||||||
|
self.make_serial_nos(non_exists_serial_nos)
|
||||||
|
|
||||||
|
def make_serial_nos(self, serial_nos):
|
||||||
|
serial_nos_details = []
|
||||||
|
batch_no = None
|
||||||
|
if self.batches:
|
||||||
|
batch_no = list(self.batches.keys())[0]
|
||||||
|
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
serial_nos_details.append(
|
||||||
|
(
|
||||||
|
serial_no,
|
||||||
|
serial_no,
|
||||||
|
now(),
|
||||||
|
now(),
|
||||||
|
frappe.session.user,
|
||||||
|
frappe.session.user,
|
||||||
|
self.warehouse,
|
||||||
|
self.company,
|
||||||
|
self.item_code,
|
||||||
|
self.item_name,
|
||||||
|
self.description,
|
||||||
|
"Active",
|
||||||
|
batch_no,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if serial_nos_details:
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"serial_no",
|
||||||
|
"creation",
|
||||||
|
"modified",
|
||||||
|
"owner",
|
||||||
|
"modified_by",
|
||||||
|
"warehouse",
|
||||||
|
"company",
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"batch_no",
|
||||||
|
]
|
||||||
|
|
||||||
|
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||||
|
|
||||||
def set_serial_batch_entries(self, doc):
|
def set_serial_batch_entries(self, doc):
|
||||||
if self.get("serial_nos"):
|
if self.get("serial_nos"):
|
||||||
serial_no_wise_batch = frappe._dict({})
|
serial_no_wise_batch = frappe._dict({})
|
||||||
|
@ -897,9 +897,12 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
|
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
|
||||||
|
|
||||||
self.wh_data.qty_after_transaction += doc.total_qty
|
precision = doc.precision("total_qty")
|
||||||
|
self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
|
||||||
if self.wh_data.qty_after_transaction:
|
if self.wh_data.qty_after_transaction:
|
||||||
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
|
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
|
||||||
|
self.wh_data.qty_after_transaction, precision
|
||||||
|
)
|
||||||
|
|
||||||
def validate_negative_stock(self, sle):
|
def validate_negative_stock(self, sle):
|
||||||
"""
|
"""
|
||||||
|
@ -11,6 +11,9 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
|||||||
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
|
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
|
get_available_serial_nos,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
||||||
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
|
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
|
||||||
@ -125,7 +128,21 @@ def get_stock_balance(
|
|||||||
|
|
||||||
if with_valuation_rate:
|
if with_valuation_rate:
|
||||||
if with_serial_no:
|
if with_serial_no:
|
||||||
serial_nos = get_serial_nos_data_after_transactions(args)
|
serial_no_details = get_available_serial_nos(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"posting_date": posting_date,
|
||||||
|
"posting_time": posting_time,
|
||||||
|
"ignore_warehouse": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = ""
|
||||||
|
if serial_no_details:
|
||||||
|
serial_nos = "\n".join(d.serial_no for d in serial_no_details)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
|
(last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
|
||||||
@ -140,38 +157,6 @@ def get_stock_balance(
|
|||||||
return last_entry.qty_after_transaction if last_entry else 0.0
|
return last_entry.qty_after_transaction if last_entry else 0.0
|
||||||
|
|
||||||
|
|
||||||
def get_serial_nos_data_after_transactions(args):
|
|
||||||
|
|
||||||
serial_nos = set()
|
|
||||||
args = frappe._dict(args)
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
|
||||||
|
|
||||||
stock_ledger_entries = (
|
|
||||||
frappe.qb.from_(sle)
|
|
||||||
.select("serial_no", "actual_qty")
|
|
||||||
.where(
|
|
||||||
(sle.item_code == args.item_code)
|
|
||||||
& (sle.warehouse == args.warehouse)
|
|
||||||
& (
|
|
||||||
CombineDatetime(sle.posting_date, sle.posting_time)
|
|
||||||
< CombineDatetime(args.posting_date, args.posting_time)
|
|
||||||
)
|
|
||||||
& (sle.is_cancelled == 0)
|
|
||||||
)
|
|
||||||
.orderby(sle.posting_date, sle.posting_time, sle.creation)
|
|
||||||
.run(as_dict=1)
|
|
||||||
)
|
|
||||||
|
|
||||||
for stock_ledger_entry in stock_ledger_entries:
|
|
||||||
changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
|
|
||||||
if stock_ledger_entry.actual_qty > 0:
|
|
||||||
serial_nos.update(changed_serial_no)
|
|
||||||
else:
|
|
||||||
serial_nos.difference_update(changed_serial_no)
|
|
||||||
|
|
||||||
return "\n".join(serial_nos)
|
|
||||||
|
|
||||||
|
|
||||||
def get_serial_nos_data(serial_nos):
|
def get_serial_nos_data(serial_nos):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
@ -149,6 +149,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.set_subcontracting_order_status()
|
self.set_subcontracting_order_status()
|
||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
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