Merge remote-tracking branch 'origin/develop' into feat/add-login-user-to-driver

This commit is contained in:
David Arnold
2024-03-02 13:16:54 +01:00
212 changed files with 99229 additions and 15028 deletions

View File

@@ -31,6 +31,9 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
@@ -117,11 +120,11 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- 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:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
- name: Show bench output
if: ${{ always() }}
@@ -129,6 +132,7 @@ jobs:
- name: Upload coverage data
uses: actions/upload-artifact@v3
if: github.event_name != 'pull_request'
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
@@ -137,6 +141,7 @@ jobs:
name: Coverage Wrap Up
needs: test
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Clone
uses: actions/checkout@v2
@@ -148,5 +153,6 @@ jobs:
uses: codecov/codecov-action@v2
with:
name: MariaDB
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
*.py~
.DS_Store
conf.py
locale
latest_updates.json
.wnf-lang-status
*.egg-info

View File

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

View File

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

View File

@@ -56,7 +56,9 @@
"Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {},
"Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {},
"Im\u00f3veis Destinados \u00e0 Venda": {},
"Insumos (materiais diretos)": {},
"Insumos (materiais diretos)": {
"account_type": "Stock"
},
"Insumos Agropecu\u00e1rios": {},
"Mercadorias para Revenda": {},
"Outras 11": {},
@@ -146,6 +148,65 @@
"root_type": "Asset"
},
"CUSTOS DE PRODU\u00c7\u00c3O": {
"CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": {
"CUSTO DOS PRODUTOS VENDIDOS": {
"CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": {
"Custos dos Produtos Vendidos em Geral": {
"account_type": "Cost of Goods Sold"
},
"Outros Custos 4": {},
"account_type": "Cost of Goods Sold"
},
"CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": {
"Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {},
"Custos dos Produtos para Assist\u00eancia Social - Vendidos": {},
"Outras": {}
},
"CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": {
"Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {},
"Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {},
"Outros Custos 6": {}
},
"CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": {
"Custos dos Produtos para Sa\u00fade - Gratuidades": {},
"Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {},
"Outros Custos 5": {}
},
"account_type": "Cost of Goods Sold"
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS": {
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": {
"Custo dos Servi\u00e7os Prestados em Geral": {},
"Outros Custos": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": {
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade 1": {},
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {},
"Outros Custos 2": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": {
"Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {},
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade": {},
"Custo dos Servi\u00e7os Prestados ao PROUNI": {},
"Outros Custos 1": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": {
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {},
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade 2": {},
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {},
"Outros Custos 3": {}
}
}
},
"CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": {
"CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": {
"Alimenta\u00e7\u00e3o do Trabalhador": {},
@@ -621,7 +682,9 @@
"Receita das Unidades Imobili\u00e1rias Vendidas": {},
"Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {},
"Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {},
"Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {},
"Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {
"account_type": "Income Account"
},
"Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {}
}
}
@@ -645,65 +708,6 @@
}
},
"RESULTADO OPERACIONAL": {
"CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": {
"CUSTO DOS PRODUTOS VENDIDOS": {
"CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": {
"Custos dos Produtos Vendidos em Geral": {
"account_type": "Cost of Goods Sold"
},
"Outros Custos 4": {},
"account_type": "Cost of Goods Sold"
},
"CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": {
"Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {},
"Custos dos Produtos para Assist\u00eancia Social - Vendidos": {},
"Outras": {}
},
"CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": {
"Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {},
"Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {},
"Outros Custos 6": {}
},
"CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": {
"Custos dos Produtos para Sa\u00fade - Gratuidades": {},
"Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {},
"Outros Custos 5": {}
},
"account_type": "Cost of Goods Sold"
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS": {
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": {
"Custo dos Servi\u00e7os Prestados em Geral": {},
"Outros Custos": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": {
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade 1": {},
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {},
"Outros Custos 2": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": {
"Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {},
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade": {},
"Custo dos Servi\u00e7os Prestados ao PROUNI": {},
"Outros Custos 1": {}
},
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": {
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {},
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {},
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {},
"Custo dos Servi\u00e7os Prestados a Gratuidade 2": {},
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {},
"Outros Custos 3": {}
}
}
},
"DESPESAS OPERACIONAIS": {
"DESPESAS OPERACIONAIS 1": {
"DESPESAS OPERACIONAIS 2": {

View File

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

View File

@@ -11,6 +11,10 @@ from frappe.model import core_doctypes_list
from frappe.model.document import Document
from frappe.utils import cstr
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
get_allowed_types_from_settings,
)
class AccountingDimension(Document):
# begin: auto-generated types
@@ -106,6 +110,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
doc_count = len(get_accounting_dimensions())
count = 0
repostable_doctypes = get_allowed_types_from_settings()
for doctype in doclist:
@@ -121,6 +126,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"options": doc.document_type,
"insert_after": insert_after_field,
"owner": "Administrator",
"allow_on_submit": 1 if doctype in repostable_doctypes else 0,
}
meta = frappe.get_meta(doctype, cached=False)

View File

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

View File

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

View File

@@ -80,7 +80,8 @@ class BankStatementImport(DataImport):
from frappe.utils.background_jobs import is_job_enqueued
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"))
job_id = f"bank_statement_import::{self.name}"
@@ -97,7 +98,7 @@ class BankStatementImport(DataImport):
google_sheets_url=self.google_sheets_url,
bank=self.bank,
template_options=self.template_options,
now=frappe.conf.developer_mode or frappe.flags.in_test,
now=run_now,
)
return True

View File

@@ -94,10 +94,13 @@ class BankTransaction(Document):
pe.append(reference)
def update_allocated_amount(self):
self.allocated_amount = (
allocated_amount = (
sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
)
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - allocated_amount
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
def before_submit(self):
self.allocate_payment_entries()

View File

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

View File

@@ -85,7 +85,14 @@ class Dunning(AccountsController):
frappe.throw(
_(
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
).format(row.sales_invoice, invoice_currency, self.currency)
).format(
frappe.get_desk_link(
"Sales Invoice",
row.sales_invoice,
),
invoice_currency,
self.currency,
)
)
def validate_overdue_payments(self):

View File

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

View File

@@ -14,6 +14,25 @@ frappe.ui.form.on("Journal Entry", {
refresh: function(frm) {
erpnext.toggle_naming_series();
if (frm.doc.repost_required && frm.doc.docstatus===1) {
frm.set_intro(__("Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update."));
frm.add_custom_button(__('Repost Accounting Entries'),
() => {
frm.call({
doc: frm.doc,
method: 'repost_accounting_entries',
freeze: true,
freeze_message: __('Reposting...'),
callback: (r) => {
if (!r.exc) {
frappe.msgprint(__('Accounting Entries are reposted.'));
frm.refresh();
}
}
});
}).removeClass('btn-default').addClass('btn-warning');
}
if(frm.doc.docstatus > 0) {
frm.add_custom_button(__('Ledger'), function() {
frappe.route_options = {
@@ -184,7 +203,6 @@ var update_jv_details = function(doc, r) {
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
frappe.model.set_value(row.doctype, row.name, "account", d.account)
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
});
refresh_field("accounts");
}
@@ -193,7 +211,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
onload() {
this.load_defaults();
this.setup_queries();
this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
@@ -292,19 +309,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
}
setup_balance_formatter() {
const formatter = function(value, df, options, doc) {
var currency = frappe.meta.get_field_currency(df, doc);
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
};
this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
}
reference_name(doc, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
@@ -400,23 +404,22 @@ frappe.ui.form.on("Journal Entry Account", {
if(!d.account && d.party_type && d.party) {
if(!frm.doc.company) frappe.throw(__("Please select Company"));
return frm.call({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_balance",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_currency",
child: d,
args: {
company: frm.doc.company,
party_type: d.party_type,
party: d.party,
cost_center: d.cost_center
}
});
}
},
cost_center: function(frm, dt, dn) {
erpnext.journal_entry.set_account_balance(frm, dt, dn);
erpnext.journal_entry.set_account_details(frm, dt, dn);
},
account: function(frm, dt, dn) {
erpnext.journal_entry.set_account_balance(frm, dt, dn);
erpnext.journal_entry.set_account_details(frm, dt, dn);
},
debit_in_account_currency: function(frm, cdt, cdn) {
@@ -600,14 +603,14 @@ $.extend(erpnext.journal_entry, {
});
$.extend(erpnext.journal_entry, {
set_account_balance: function(frm, dt, dn) {
set_account_details: function(frm, dt, dn) {
var d = locals[dt][dn];
if(d.account) {
if(!frm.doc.company) frappe.throw(__("Please select Company first"));
if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first"));
return frappe.call({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_balance_and_party_type",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_details_and_party_type",
args: {
account: d.account,
date: frm.doc.posting_date,
@@ -615,7 +618,6 @@ $.extend(erpnext.journal_entry, {
debit: flt(d.debit_in_account_currency),
credit: flt(d.credit_in_account_currency),
exchange_rate: d.exchange_rate,
cost_center: d.cost_center
},
callback: function(r) {
if(r.message) {

View File

@@ -64,7 +64,8 @@
"stock_entry",
"subscription_section",
"auto_repeat",
"amended_from"
"amended_from",
"repost_required"
],
"fields": [
{
@@ -543,6 +544,15 @@
"label": "Is System Generated",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -558,6 +568,7 @@
}
],
"modified": "2023-11-23 12:11:04.128015",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -13,6 +13,10 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
get_party_account_based_on_invoice_discounting,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
@@ -140,7 +144,6 @@ class JournalEntry(AccountsController):
self.set_print_format_fields()
self.validate_credit_debit_note()
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
@@ -150,6 +153,10 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
def validate_for_repost(self):
validate_docs_for_voucher_types(["Journal Entry"])
validate_docs_for_deferred_accounting([self.name], [])
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
@@ -173,6 +180,15 @@ class JournalEntry(AccountsController):
self.update_inter_company_jv()
self.update_invoice_discounting()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
self.needs_repost = self.check_if_fields_updated(
fields_to_check=[], child_tables={"accounts": []}
)
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super(JournalEntry, self).on_cancel()
@@ -562,17 +578,28 @@ class JournalEntry(AccountsController):
elif d.party_type == "Supplier" and flt(d.credit) > 0:
frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx))
def system_generated_gain_loss(self):
return (
self.voucher_type == "Exchange Gain Or Loss"
and self.multi_currency
and self.is_system_generated
)
def validate_against_jv(self):
for d in self.get("accounts"):
if d.reference_type == "Journal Entry":
account_root_type = frappe.get_cached_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
if account_root_type == "Asset" and flt(d.debit) > 0 and not self.system_generated_gain_loss():
frappe.throw(
_(
"Row #{0}: For {1}, you can select reference document only if account gets credited"
).format(d.idx, d.account)
)
elif account_root_type == "Liability" and flt(d.credit) > 0:
elif (
account_root_type == "Liability"
and flt(d.credit) > 0
and not self.system_generated_gain_loss()
):
frappe.throw(
_(
"Row #{0}: For {1}, you can select reference document only if account gets debited"
@@ -604,7 +631,7 @@ class JournalEntry(AccountsController):
for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid:
if not valid and not self.system_generated_gain_loss():
frappe.throw(
_("Against Journal Entry {0} does not have any unmatched {1} entry").format(
d.reference_name, dr_or_cr
@@ -1152,24 +1179,11 @@ class JournalEntry(AccountsController):
if not self.get("accounts"):
frappe.throw(_("Accounts table cannot be blank."))
def set_account_and_party_balance(self):
account_balance = {}
party_balance = {}
for d in self.get("accounts"):
if d.account not in account_balance:
account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date)
if (d.party_type, d.party) not in party_balance:
party_balance[(d.party_type, d.party)] = get_balance_on(
party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company
)
d.account_balance = account_balance[d.account]
d.party_balance = party_balance[(d.party_type, d.party)]
@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
if mode_of_payment:
@@ -1207,7 +1221,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
return frappe._dict(
{
"account": account,
"balance": get_balance_on(account),
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
"account_currency": account_details.account_currency,
"account_type": account_details.account_type,
}
@@ -1332,8 +1346,6 @@ def get_payment_entry(ref_doc, args):
"account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"),
"account_currency": args.get("party_account_currency")
or get_account_currency(args.get("party_account")),
"balance": get_balance_on(args.get("party_account")),
"party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
"exchange_rate": exchange_rate,
args.get("amount_field_party"): args.get("amount"),
"is_advance": args.get("is_advance"),
@@ -1481,30 +1493,23 @@ def get_outstanding(args):
@frappe.whitelist()
def get_party_account_and_balance(company, party_type, party, cost_center=None):
def get_party_account_and_currency(company, party_type, party):
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
account = get_party_account(party_type, party, company)
account_balance = get_balance_on(account=account, cost_center=cost_center)
party_balance = get_balance_on(
party_type=party_type, party=party, company=company, cost_center=cost_center
)
return {
"account": account,
"balance": account_balance,
"party_balance": party_balance,
"account_currency": frappe.get_cached_value("Account", account, "account_currency"),
}
@frappe.whitelist()
def get_account_balance_and_party_type(
account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None
def get_account_details_and_party_type(
account, date, company, debit=None, credit=None, exchange_rate=None
):
"""Returns dict of account balance and party type to be set in Journal Entry on selection of account."""
"""Returns dict of account details and party type to be set in Journal Entry on selection of account."""
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1524,7 +1529,6 @@ def get_account_balance_and_party_type(
party_type = ""
grid_values = {
"balance": get_balance_on(account, date, cost_center=cost_center),
"party_type": party_type,
"account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency,

View File

@@ -166,43 +166,37 @@ class TestJournalEntry(unittest.TestCase):
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit()
gl_entries = frappe.db.sql(
"""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""",
jv.name,
as_dict=1,
)
self.voucher_no = jv.name
self.assertTrue(gl_entries)
self.fields = [
"account",
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
]
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
"credit_in_account_currency": 0,
},
"_Test Bank - _TC": {
self.expected_gle = [
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 5000,
},
}
{
"account": "_Test Bank USD - _TC",
"account_currency": "USD",
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
"credit_in_account_currency": 0,
},
]
for field in (
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
self.check_gl_entries()
# cancel
jv.cancel()
@@ -228,43 +222,37 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate()
rjv.submit()
gl_entries = frappe.db.sql(
"""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""",
rjv.name,
as_dict=1,
)
self.voucher_no = rjv.name
self.assertTrue(gl_entries)
self.fields = [
"account",
"account_currency",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
]
expected_values = {
"_Test Bank USD - _TC": {
self.expected_gle = [
{
"account": "_Test Bank USD - _TC",
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
"Sales - _TC": {
{
"account": "Sales - _TC",
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
},
}
]
for field in (
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
self.check_gl_entries()
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
@@ -344,23 +332,25 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
expected_values = {
"_Test Cash - _TC": {"cost_center": cost_center},
"_Test Bank - _TC": {"cost_center": cost_center},
}
self.voucher_no = jv.name
gl_entries = frappe.db.sql(
"""select account, cost_center, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""",
jv.name,
as_dict=1,
)
self.fields = [
"account",
"cost_center",
]
self.assertTrue(gl_entries)
self.expected_gle = [
{
"account": "_Test Bank - _TC",
"cost_center": cost_center,
},
{
"account": "_Test Cash - _TC",
"cost_center": cost_center,
},
]
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
self.check_gl_entries()
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
@@ -387,23 +377,22 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
expected_values = {
"_Test Cash - _TC": {"project": project_name},
"_Test Bank - _TC": {"project": project_name},
}
self.voucher_no = jv.name
gl_entries = frappe.db.sql(
"""select account, project, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""",
jv.name,
as_dict=1,
)
self.fields = ["account", "project"]
self.assertTrue(gl_entries)
self.expected_gle = [
{
"account": "_Test Bank - _TC",
"project": project_name,
},
{
"account": "_Test Cash - _TC",
"project": project_name,
},
]
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["project"], gle.project)
self.check_gl_entries()
def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -426,6 +415,79 @@ class TestJournalEntry(unittest.TestCase):
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance)
def test_repost_accounting_entries(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
# Configure Repost Accounting Ledger for JVs
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]:
settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True})
settings.save()
# Create JV with defaut cost center - _Test Cost Center
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.multi_currency = 0
jv.submit()
# Check GL entries before reposting
self.voucher_no = jv.name
self.fields = [
"account",
"debit_in_account_currency",
"credit_in_account_currency",
"cost_center",
]
self.expected_gle = [
{
"account": "_Test Bank - _TC",
"debit_in_account_currency": 0,
"credit_in_account_currency": 100,
"cost_center": "_Test Cost Center - _TC",
},
{
"account": "_Test Cash - _TC",
"debit_in_account_currency": 100,
"credit_in_account_currency": 0,
"cost_center": "_Test Cost Center - _TC",
},
]
self.check_gl_entries()
# Change cost center for bank account - _Test Cost Center for BS Account
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
jv.save()
# Check if repost flag gets set on update after submit
self.assertTrue(jv.repost_required)
jv.repost_accounting_entries()
# Check GL entries after reposting
jv.load_from_db()
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
self.check_gl_entries()
def check_gl_entries(self):
gl = frappe.qb.DocType("GL Entry")
query = frappe.qb.from_(gl)
for field in self.fields:
query = query.select(gl[field])
query = query.where(
(gl.voucher_type == "Journal Entry")
& (gl.voucher_no == self.voucher_no)
& (gl.is_cancelled == 0)
).orderby(gl.account)
gl_entries = query.run(as_dict=True)
for i in range(len(self.expected_gle)):
for field in self.fields:
self.assertEqual(self.expected_gle[i][field], gl_entries[i][field])
def make_journal_entry(
account1,

View File

@@ -9,12 +9,10 @@
"field_order": [
"account",
"account_type",
"balance",
"col_break1",
"bank_account",
"party_type",
"party",
"party_balance",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -64,17 +62,7 @@
"print_hide": 1
},
{
"fieldname": "balance",
"fieldtype": "Currency",
"label": "Account Balance",
"no_copy": 1,
"oldfieldname": "balance",
"oldfieldtype": "Data",
"options": "account_currency",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"default": ":Company",
"description": "If Income or Expense",
"fieldname": "cost_center",
@@ -107,14 +95,6 @@
"label": "Party",
"options": "party_type"
},
{
"fieldname": "party_balance",
"fieldtype": "Currency",
"label": "Party Balance",
"options": "account_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "currency_section",
"fieldtype": "Section Break",
@@ -223,6 +203,7 @@
"no_copy": 1
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -286,7 +267,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-12-03 23:21:22.205409",
"modified": "2024-02-05 01:10:50.224840",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -149,7 +149,7 @@ frappe.ui.form.on('Payment Entry', {
},
refresh: function(frm) {
erpnext.hide_company();
erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);

View File

@@ -471,7 +471,10 @@ class PaymentEntry(AccountsController):
)
def set_missing_ref_details(
self, force: bool = False, update_ref_details_only_for: list | None = None
self,
force: bool = False,
update_ref_details_only_for: list | None = None,
ref_exchange_rate: float | None = None,
) -> None:
for d in self.get("references"):
if d.allocated_amount:
@@ -484,6 +487,8 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
if ref_exchange_rate:
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
if d.exchange_gain_loss:
@@ -1032,19 +1037,19 @@ class PaymentEntry(AccountsController):
)
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount
elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount - base_party_amount
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
self.difference_amount = flt(
self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
self.difference_amount - total_deductions, self.precision("difference_amount")
)
def get_included_taxes(self):
@@ -2220,6 +2225,7 @@ def get_payment_entry(
party_type=None,
payment_type=None,
reference_date=None,
ignore_permissions=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2242,14 +2248,14 @@ def get_payment_entry(
)
# 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 party_type in ["Customer", "Supplier"] and not bank:
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
if party_bank_account:
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
bank = get_bank_cash_account(doc, account)
bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
@@ -2389,9 +2395,13 @@ def update_accounting_dimensions(pe, doc):
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(
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:

View File

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

View File

@@ -633,7 +633,12 @@ class PaymentReconciliation(Document):
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
filters={
"parent": ("in", journals),
"account": ("in", [self.receivable_payable_account]),
"party_type": self.party_type,
"party": self.party,
},
fields=[
"parent as `name`",
"exchange_rate",

View File

@@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
self.cash = "Cash - _PR"
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
@@ -486,6 +487,91 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_payment_against_foreign_currency_journal(self):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
amount = 100
exc_rate1 = 80
exc_rate2 = 83
je = frappe.new_doc("Journal Entry")
je.posting_date = transaction_date
je.company = self.company
je.user_remark = "test"
je.multi_currency = 1
je.set(
"accounts",
[
{
"account": self.creditors_usd,
"party_type": "Supplier",
"party": self.supplier,
"exchange_rate": exc_rate1,
"cost_center": self.cost_center,
"credit": amount * exc_rate1,
"credit_in_account_currency": amount,
},
{
"account": self.creditors_usd,
"party_type": "Supplier",
"party": self.supplier2,
"exchange_rate": exc_rate2,
"cost_center": self.cost_center,
"credit": amount * exc_rate2,
"credit_in_account_currency": amount,
},
{
"account": self.expense_account,
"cost_center": self.cost_center,
"debit": (amount * exc_rate1) + (amount * exc_rate2),
"debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2),
},
],
)
je.save().submit()
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
pe.payment_type = "Pay"
pe.party_type = "Supplier"
pe.party = self.supplier
pe.paid_to = self.creditors_usd
pe.paid_from = self.cash
pe.paid_amount = 8000
pe.received_amount = 100
pe.target_exchange_rate = exc_rate1
pe.paid_to_account_currency = "USD"
pe.save().submit()
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.receivable_payable_account = self.creditors_usd
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
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}))
# There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1'
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1},
fields=["parent"],
)
self.assertEqual([], journals)
def test_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
@@ -591,6 +677,70 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(si.status, "Paid")
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):
transaction_date = nowdate()
amount = 100
@@ -1184,3 +1334,17 @@ def make_customer(customer_name, currency=None):
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.type = "Individual"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name

View File

@@ -3,6 +3,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
@@ -106,6 +107,8 @@ class PaymentRequest(Document):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account)
if not ref_amount:
frappe.throw(_("Payment Entry is already created"))
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(
@@ -453,6 +456,8 @@ def make_payment_request(**args):
gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if not grand_total:
frappe.throw(_("Payment Entry is already created"))
if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
@@ -543,6 +548,7 @@ def get_amount(ref_doc, payment_account=None):
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
grand_total -= get_paid_amount_against_order(dt, ref_doc.name)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
@@ -562,10 +568,7 @@ def get_amount(ref_doc, payment_account=None):
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
else:
frappe.throw(_("Payment Entry is already created"))
return grand_total
def get_existing_payment_request_amount(ref_dt, ref_dn):
@@ -748,3 +751,27 @@ def validate_payment(doc, method=None):
doc.reference_docname
)
)
def get_paid_amount_against_order(dt, dn):
pe_ref = frappe.qb.DocType("Payment Entry Reference")
if dt == "Sales Order":
inv_dt, inv_field = "Sales Invoice Item", "sales_order"
else:
inv_dt, inv_field = "Purchase Invoice Item", "purchase_order"
inv_item = frappe.qb.DocType(inv_dt)
return (
frappe.qb.from_(pe_ref)
.select(
Sum(pe_ref.allocated_amount),
)
.where(
(pe_ref.docstatus == 1)
& (
(pe_ref.reference_name == dn)
| pe_ref.reference_name.isin(
frappe.qb.from_(inv_item).select(inv_item.parent).where(inv_item[inv_field] == dn).distinct()
)
)
)
).run()[0][0] or 0

View File

@@ -80,13 +80,16 @@
"target_warehouse",
"quality_inspection",
"serial_and_batch_bundle",
"batch_no",
"use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
"serial_no",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_tlhi",
"serial_no",
"column_break_ciit",
"batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -628,13 +631,13 @@
"options": "Quality Inspection"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "col_break5",
@@ -649,14 +652,14 @@
"print_hide": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"hidden": 1,
"in_list_view": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"read_only": 1
"oldfieldtype": "Small Text"
},
{
"fieldname": "item_tax_rate",
@@ -824,17 +827,33 @@
"read_only": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_tlhi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ciit",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2023-11-14 18:33:22.585715",
"modified": "2024-02-25 15:50:17.140269",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -72,7 +72,7 @@ class POSInvoiceItem(Document):
rate_with_margin: DF.Currency
sales_order: DF.Link | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
service_end_date: DF.Date | None
service_start_date: DF.Date | None
service_stop_date: DF.Date | None
@@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None

View File

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

View File

@@ -22,6 +22,8 @@
"is_paid",
"is_return",
"return_against",
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"tax_withholding_category",
"amended_from",
@@ -412,6 +414,20 @@
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_purchase_order",
"fieldtype": "Check",
"label": "Update Billed Amount in Purchase Order"
},
{
"default": "1",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_purchase_receipt",
"fieldtype": "Check",
"label": "Update Billed Amount in Purchase Receipt"
},
{
"fieldname": "section_addresses",
"fieldtype": "Section Break",
@@ -1613,7 +1629,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-01-26 10:46:00.469053",
"modified": "2024-02-25 11:20:28.366808",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -193,6 +193,7 @@ class PurchaseInvoice(BuyingController):
supplied_items: DF.Table[PurchaseReceiptItemSupplied]
supplier: DF.Link
supplier_address: DF.Link | None
supplier_group: DF.Link | None
supplier_name: DF.Data | None
supplier_warehouse: DF.Link | None
tax_category: DF.Link | None
@@ -214,6 +215,8 @@ class PurchaseInvoice(BuyingController):
total_qty: DF.Float
total_taxes_and_charges: DF.Currency
unrealized_profit_loss_account: DF.Link | None
update_billed_amount_in_purchase_order: DF.Check
update_billed_amount_in_purchase_receipt: DF.Check
update_stock: DF.Check
use_company_roundoff_cost_center: DF.Check
use_transaction_date_exchange_rate: DF.Check
@@ -679,6 +682,11 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).on_submit()
self.check_prev_docstatus()
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return
self.status_updater = []
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -696,6 +704,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
if self.is_old_subcontracting_flow:
@@ -723,6 +732,7 @@ class PurchaseInvoice(BuyingController):
"cash_bank_account",
"write_off_account",
"unrealized_profit_loss_account",
"is_opening",
]
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
@@ -1015,9 +1025,14 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
) or self.get_company_default("default_provisional_account")
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
"Purchase Receipt Item",
item.pr_detail,
["provisional_expense_account", "qty", "base_rate"],
)
provisional_account = provisional_account or self.get_company_default(
"default_provisional_account"
)
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
@@ -1034,13 +1049,18 @@ class PurchaseInvoice(BuyingController):
"voucher_detail_no": item.pr_detail,
"account": provisional_account,
},
["name"],
"name",
)
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
item, gl_entries, self.posting_date, provisional_account, reverse=1
item,
gl_entries,
self.posting_date,
provisional_account,
reverse=1,
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
)
if not self.is_internal_transfer():
@@ -1425,6 +1445,10 @@ class PurchaseInvoice(BuyingController):
self.check_on_hold_or_closed_status()
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return
self.status_updater = []
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -1519,6 +1543,9 @@ class PurchaseInvoice(BuyingController):
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
def update_billing_status_in_pr(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
return
updated_pr = []
po_details = []

View File

@@ -1539,18 +1539,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
company="_Test Company",
)
company = frappe.get_doc("Company", "_Test Company")
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
setup_provisional_accounting()
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)
@@ -1594,8 +1583,97 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
toggle_provisional_accounting_setting()
def test_provisional_accounting_entry_for_over_billing(self):
setup_provisional_accounting()
# Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
# Create PR: rate = 1000, qty = 5
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
)
# Overbill PR: rate = 2000, qty = 10
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].qty = 10
pi.items[0].rate = 2000
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
expected_gle = [
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 5000, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
["Provision Account - _TC", 0, 5000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
["Provision Account - _TC", 0, 5000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
]
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
)
toggle_provisional_accounting_setting()
def test_provisional_accounting_entry_for_partial_billing(self):
setup_provisional_accounting()
# Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
# Create PR: rate = 1000, qty = 5
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
)
# Partially bill PR: rate = 500, qty = 2
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].qty = 2
pi.items[0].rate = 500
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
expected_gle = [
["Cost of Goods Sold - _TC", 1000, 0, add_days(pr.posting_date, -1)],
["Creditors - _TC", 0, 1000, add_days(pr.posting_date, -1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 5000, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
["Provision Account - _TC", 0, 1000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
toggle_provisional_accounting_setting()
def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
@@ -2274,4 +2352,26 @@ def make_purchase_invoice_against_cost_center(**args):
return pi
def setup_provisional_accounting(**args):
args = frappe._dict(args)
create_item("_Test Non Stock Item", is_stock_item=0)
company = args.company or "_Test Company"
provisional_account = create_account(
account_name=args.account_name or "Provision Account",
parent_account=args.parent_account or "Current Liabilities - _TC",
company=company,
)
toggle_provisional_accounting_setting(
enable=1, company=company, provisional_account=provisional_account
)
def toggle_provisional_accounting_setting(**args):
args = frappe._dict(args)
company = frappe.get_doc("Company", args.company or "_Test Company")
company.enable_provisional_accounting_for_non_stock_items = args.enable or 0
company.default_provisional_account = args.provisional_account
company.save()
test_records = frappe.get_test_records("Purchase Invoice")

View File

@@ -62,16 +62,19 @@
"rm_supp_cost",
"warehouse_section",
"warehouse",
"from_warehouse",
"quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
"serial_no",
"use_serial_batch_fields",
"col_br_wh",
"from_warehouse",
"quality_inspection",
"rejected_warehouse",
"rejected_serial_and_batch_bundle",
"batch_no",
"section_break_rqbe",
"serial_no",
"rejected_serial_no",
"column_break_vbbb",
"batch_no",
"manufacture_details",
"manufacturer",
"column_break_13",
@@ -440,13 +443,11 @@
"print_hide": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1,
"search_index": 1
},
{
@@ -454,21 +455,18 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"read_only": 1
"label": "Serial No"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "accounting",
@@ -891,7 +889,7 @@
"label": "Apply TDS"
},
{
"depends_on": "eval:parent.update_stock == 1",
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -901,7 +899,7 @@
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock == 1",
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -916,16 +914,31 @@
"options": "Asset"
},
{
"depends_on": "eval:parent.update_stock === 1",
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "section_break_rqbe",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vbbb",
"fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-01-21 19:46:25.537861",
"modified": "2024-02-04 14:11:52.742228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
stock_uom_rate: DF.Currency
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float

View File

@@ -88,6 +88,7 @@ class RepostAccountingLedger(Document):
).append(gle.update({"old": True}))
def generate_preview_data(self):
frappe.flags.through_repost_accounting_ledger = True
self.gl_entries = []
self.get_existing_ledger_entries()
for x in self.vouchers:
@@ -141,6 +142,7 @@ class RepostAccountingLedger(Document):
@frappe.whitelist()
def start_repost(account_repost_doc=str) -> None:
frappe.flags.through_repost_accounting_ledger = True
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)

View File

@@ -14,23 +14,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.setup(doc);
}
company() {
super.company();
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
let me = this;
if (this.frm.doc.company) {
frappe.call({
method:
"erpnext.accounts.party.get_party_account",
args: {
party_type: 'Customer',
party: this.frm.doc.customer,
company: this.frm.doc.company
},
callback: (response) => {
if (response) me.frm.set_value("debit_to", response.message);
},
});
}
}
onload() {
var me = this;

View File

@@ -270,7 +270,7 @@ class SalesInvoice(SellingController):
super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time()
if not self.is_pos:
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
self.set_tax_withholding()
@@ -447,6 +447,11 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
for table_name in ["items", "packed_items"]:
if not self.get(table_name):
continue
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger()
# this sequence because outstanding may get -ve
@@ -722,6 +727,7 @@ class SalesInvoice(SellingController):
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
"is_opening",
]
child_tables = {
"items": ("income_account", "expense_account", "discount_account"),
@@ -1477,9 +1483,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher": self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
},

View File

@@ -1105,6 +1105,44 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, 10)
def test_ledger_entries_of_return_pos_invoice(self):
make_pos_profile()
pos = create_sales_invoice(do_not_save=True)
pos.is_pos = 1
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos.save().submit()
self.assertEqual(pos.outstanding_amount, 0.0)
self.assertEqual(pos.status, "Paid")
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
pos_return = make_sales_return(pos.name)
pos_return.save().submit()
pos_return.reload()
pos.reload()
self.assertEqual(pos_return.is_return, 1)
self.assertEqual(pos_return.return_against, pos.name)
self.assertEqual(pos_return.outstanding_amount, 0.0)
self.assertEqual(pos_return.status, "Return")
self.assertEqual(pos.outstanding_amount, 0.0)
self.assertEqual(pos.status, "Credit Note Issued")
expected = (
("Cash - _TC", 0.0, 100.0, pos_return.name, None),
("Debtors - _TC", 0.0, 100.0, pos_return.name, pos_return.name),
("Debtors - _TC", 100.0, 0.0, pos_return.name, pos_return.name),
("Sales - _TC", 100.0, 0.0, pos_return.name, None),
)
res = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pos_return.name, "is_cancelled": 0},
fields=["account", "debit", "credit", "voucher_no", "against_voucher"],
order_by="account, debit, credit",
as_list=1,
)
self.assertEqual(expected, res)
def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0)
@@ -3577,6 +3615,33 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_pulling_advance_based_on_debit_to(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
account_name="Debtors 2",
company="_Test Company",
account_type="Receivable",
)
si = create_sales_invoice(do_not_submit=True)
si.debit_to = debtors2
si.save()
pe = create_payment_entry(
company=si.company,
payment_type="Receive",
party_type="Customer",
party=si.customer,
paid_from=debtors2,
paid_to="Cash - _TC",
paid_amount=1000,
)
pe.submit()
advances = si.get_advance_entries()
self.assertEqual(1, len(advances))
self.assertEqual(advances[0].reference_name, pe.name)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -83,14 +83,17 @@
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"batch_no",
"incoming_rate",
"use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
"serial_no",
"incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_eoec",
"serial_no",
"column_break_ytgd",
"batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -600,12 +603,11 @@
"options": "Quality Inspection"
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1,
"search_index": 1
},
{
@@ -621,13 +623,12 @@
"print_hide": 1
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"fieldtype": "Text",
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"read_only": 1
"oldfieldtype": "Small Text"
},
{
"fieldname": "item_group",
@@ -891,6 +892,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -904,12 +906,27 @@
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "section_break_eoec",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ytgd",
"fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-12-29 13:03:14.121298",
"modified": "2024-02-25 15:56:44.828634",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -75,7 +75,7 @@ class SalesInvoiceItem(Document):
sales_invoice_item: DF.Data | None
sales_order: DF.Link | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
service_end_date: DF.Date | None
service_start_date: DF.Date | None
service_stop_date: DF.Date | None
@@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from frappe.utils import today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
@@ -49,6 +50,16 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
)
return pe
def create_sales_order(self):
so = make_sales_order(
company=self.company,
customer=self.customer,
item=self.item,
rate=100,
transaction_date=today(),
)
return so
def test_01_unreconcile_invoice(self):
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
@@ -314,3 +325,41 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
),
1,
)
def test_05_unreconcile_order(self):
so = self.create_sales_order()
pe = self.create_payment_entry()
# Allocation payment against Sales Order
pe.paid_amount = 100
pe.append(
"references",
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 100},
)
pe.save().submit()
# Assert 'Advance Paid'
so.reload()
self.assertEqual(so.advance_paid, 100)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 1)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([so.name], allocations)
# unreconcile so
unreconcile.save().submit()
# Assert 'Advance Paid'
so.reload()
pe.reload()
self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100)

View File

@@ -82,6 +82,11 @@ class UnreconcilePayment(Document):
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
if doc.doctype in frappe.get_hooks("advance_payment_payable_doctypes") + frappe.get_hooks(
"advance_payment_receivable_doctypes"
):
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)

View File

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

View File

@@ -9,7 +9,7 @@ from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Date, Sum
from frappe.query_builder.functions import Abs, Count, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -784,34 +784,37 @@ def get_timeline_data(doctype, name):
from frappe.desk.form.load import get_communication_data
out = {}
fields = "creation, count(*)"
after = add_years(None, -1).strftime("%Y-%m-%d")
group_by = "group by Date(creation)"
data = get_communication_data(
doctype,
name,
after=after,
group_by="group by creation",
fields="C.creation as creation, count(C.name)",
group_by="group by communication_date",
fields="C.communication_date as communication_date, count(C.name)",
as_dict=False,
)
# fetch and append data from Activity Log
data += frappe.db.sql(
"""select {fields}
from `tabActivity Log`
where (reference_doctype=%(doctype)s and reference_name=%(name)s)
or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s)
or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s)
and status!='Success' and creation > {after}
{group_by} order by creation desc
""".format(
fields=fields, group_by=group_by, after=after
),
{"doctype": doctype, "name": name},
as_dict=False,
)
activity_log = frappe.qb.DocType("Activity Log")
data += (
frappe.qb.from_(activity_log)
.select(activity_log.communication_date, Count(activity_log.name))
.where(
(
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
| (
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
& (activity_log.timeline_name == name)
)
)
& (activity_log.status != "Success")
& (activity_log.creation > after)
)
.groupby(activity_log.communication_date)
.orderby(activity_log.communication_date, order=frappe.qb.desc)
).run()
timeline_items = dict(data)

View File

@@ -5,7 +5,7 @@
<div class="row {% if df.bold %}important{% endif %} data-field">
<div class="col-xs-{{ "9" if df.fieldtype=="Check" else "5" }}
{%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label>
<label>{{ _(df.label, context=df.parent) }}</label>
</div>
<div class="col-xs-{{ "3" if df.fieldtype=="Check" else "7" }} value">
{% if doc.get(df.fieldname) != None -%}

View File

@@ -83,7 +83,10 @@ class ReceivablePayableReport(object):
self.skip_total_row = 1
if self.filters.get("in_party_currency"):
self.skip_total_row = 1
if self.filters.get("party") and len(self.filters.get("party")) == 1:
self.skip_total_row = 0
else:
self.skip_total_row = 1
def get_data(self):
self.get_ple_entries()

View File

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

View File

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

View File

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

View File

@@ -975,7 +975,7 @@ class GrossProfitGenerator(object):
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc)
.run(as_dict=True)
)

View File

@@ -357,7 +357,13 @@ def get_conditions(filters, additional_conditions=None):
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
if filters.get("warehouse"):
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
lft, rgt = frappe.db.get_all(
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
)[0]
conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
else:
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
if filters.get("brand"):
conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""

View File

@@ -163,7 +163,7 @@ def get_entries(filters):
"""select
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
from `tabGL Entry`
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0}
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {0}
""".format(
get_conditions(filters)
),

View File

@@ -63,16 +63,14 @@ def get_result(
tax_amount += entry.credit - entry.debit
# infer tax withholding category from the account if it's the single account for this category
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
if not tax_withholding_category:
# or else from the party default
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:
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 voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
@@ -244,7 +242,7 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Tax Amount"),
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
@@ -295,7 +293,7 @@ def get_tds_docs(filters):
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 tds_accounts.get(tds_acc["account"]):
if tds_acc["account"] in tds_accounts:
tds_accounts[tds_acc["account"]] = None
else:
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"):
query = query.where(gle.posting_date <= filters.get("to_date"))
if bank_accounts:
query = query.where(gle.against.notin(bank_accounts))
if filters.get("party"):
party = [filters.get("party")]
jv_condition = gle.against.isin(party) | (
@@ -368,7 +363,14 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
query = 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
@@ -408,7 +410,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"paid_amount_after_tax",
"base_paid_amount",
],
"Journal Entry": ["tax_withholding_category", "total_amount"],
"Journal Entry": ["total_amount"],
}
entries = frappe.get_all(

View File

@@ -5,7 +5,6 @@ import frappe
from frappe.tests.utils import FrappeTestCase
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.purchase_invoice.test_purchase_invoice import make_purchase_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
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_tcs_category()
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)
pe = create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
]
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):
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
self.assertEqual(voucher.section_code, voucher_expected_values[1])
self.assertEqual(voucher.rate, voucher_expected_values[2])
self.assertEqual(voucher.base_total, voucher_expected_values[3])
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.rate,
voucher.base_total,
voucher.tax_amount,
voucher.grand_total,
)
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def tearDown(self):
self.clear_old_entries()
@@ -67,24 +93,20 @@ def create_tax_accounts():
).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")
from_date = fiscal_year[1]
to_date = fiscal_year[2]
tax_category = create_tax_withholding_category(
category_name="TCS",
rate=0.075,
create_tax_withholding_category(
category_name=category,
rate=rate,
from_date=from_date,
to_date=to_date,
account="TCS - _TC",
cumulative_threshold=300,
account=account,
cumulative_threshold=cumulative_threshold,
)
customer = frappe.get_doc("Customer", "_Test Customer")
customer.tax_withholding_category = "TCS"
customer.save()
def create_tcs_payment_entry():
payment_entry = create_payment_entry(
@@ -109,3 +131,32 @@ def create_tcs_payment_entry():
)
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
)
jv.insert()
return jv.submit()

View File

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

View File

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

View File

@@ -237,7 +237,7 @@ def get_balance_on(
)
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 not (frappe.flags.ignore_account_permission or ignore_account_permission):
@@ -258,7 +258,7 @@ def get_balance_on(
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
in_account_currency = False
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
if account_type:
accounts = frappe.db.get_all(
@@ -278,11 +278,11 @@ def get_balance_on(
if party_type and party:
cond.append(
"""gle.party_type = %s and gle.party = %s """
% (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
% (frappe.db.escape(party_type), frappe.db.escape(party))
)
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:
precision = get_currency_precision()
@@ -348,7 +348,7 @@ def get_count_on(account, fieldname, date):
% (acc.lft, acc.rgt)
)
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
entries = frappe.db.sql(
"""
@@ -725,7 +725,7 @@ def update_reference_in_payment_entry(
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal(
@@ -982,46 +982,6 @@ def get_currency_precision():
return precision
def get_stock_rbnb_difference(posting_date, company):
stock_items = frappe.db.sql_list(
"""select distinct item_code
from `tabStock Ledger Entry` where company=%s""",
company,
)
pr_valuation_amount = frappe.db.sql(
"""
select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor)
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s
and pr.posting_date <= %s and pr_item.item_code in (%s)"""
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
tuple([company, posting_date] + stock_items),
)[0][0]
pi_valuation_amount = frappe.db.sql(
"""
select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor)
from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi
where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s
and pi.posting_date <= %s and pi_item.item_code in (%s)"""
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
tuple([company, posting_date] + stock_items),
)[0][0]
# Balance should be
stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2)
# Balance as per system
stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value(
"Company", company, "abbr"
)
sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False)
# Amount should be credited
return flt(stock_rbnb) + flt(sys_bal)
def get_held_invoices(party_type, party):
"""
Returns a list of names Purchase Invoices for the given party that are on hold
@@ -1428,8 +1388,7 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]

View File

@@ -561,15 +561,14 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
def reverse_depreciation_entry_made_after_disposal(asset, date):
for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if not asset_depr_schedule_doc:
if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"):
continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date:
if schedule.schedule_date == date and schedule.journal_entry:
if not disposal_was_made_on_original_schedule_date(
schedule_idx, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()

View File

@@ -6,7 +6,7 @@ frappe.provide("erpnext.assets");
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
setup() {
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle', 'Asset Movement'];
this.setup_posting_date_time_check();
}

View File

@@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
self.create_target_asset()
def on_submit(self):
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()
@@ -137,6 +138,7 @@ class AssetCapitalization(StockController):
"Repost Item Valuation",
"Serial and Batch Bundle",
"Asset",
"Asset Movement",
)
self.cancel_target_asset()
self.update_stock_ledger()
@@ -146,7 +148,7 @@ class AssetCapitalization(StockController):
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
asset_doc = frappe.get_doc("Asset", self.target_asset)
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
asset_doc.db_set("capitalized_in", None)
if asset_doc.docstatus == 1:
asset_doc.cancel()

View File

@@ -18,9 +18,12 @@
"amount",
"batch_and_serial_no_section",
"serial_and_batch_bundle",
"use_serial_batch_fields",
"column_break_13",
"batch_no",
"section_break_bfqc",
"serial_no",
"column_break_mbuv",
"batch_no",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -39,13 +42,13 @@
"reqd": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "section_break_6",
@@ -102,12 +105,12 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "item_code",
@@ -148,18 +151,34 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_bfqc",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_mbuv",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-04-06 01:10:17.947952",
"modified": "2024-02-25 15:57:35.007501",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",

View File

@@ -24,9 +24,10 @@ class AssetCapitalizationStockItem(Document):
parentfield: DF.Data
parenttype: DF.Data
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
stock_qty: DF.Float
stock_uom: DF.Link
use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types

View File

@@ -418,14 +418,13 @@ class AssetDepreciationSchedule(Document):
)
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
if (
n == cint(final_number_of_depreciations) - 1
and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life)
) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life):
depreciation_amount += flt(value_after_depreciation) - flt(
row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
skip_row = True
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
@@ -813,15 +812,11 @@ def make_draft_asset_depr_schedules_if_not_present(asset_doc):
asset_depr_schedules_names = []
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Draft", row.finance_book
asset_depr_schedule = get_asset_depr_schedule_name(
asset_doc.name, ["Draft", "Active"], row.finance_book
)
active_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
if not asset_depr_schedule:
name = make_draft_asset_depr_schedule(asset_doc, row)
asset_depr_schedules_names.append(name)
@@ -997,16 +992,20 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
finance_book_filter = ["finance_book", "is", "not set"]
if finance_book:
if finance_book is None:
finance_book_filter = ["finance_book", "is", "not set"]
else:
finance_book_filter = ["finance_book", "=", finance_book]
if isinstance(status, str):
status = [status]
return frappe.db.get_value(
doctype="Asset Depreciation Schedule",
filters=[
["asset", "=", asset_name],
finance_book_filter,
["status", "=", status],
["status", "in", status],
],
)

View File

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

View File

@@ -822,6 +822,30 @@ class TestPurchaseOrder(FrappeTestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
def test_blanket_order_on_po_close_and_open(self):
# Step - 1: Create Blanket Order
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
# Step - 2: Create Purchase Order
po = create_purchase_order(
item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
)
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
# Step - 3: Close Purchase Order
po.update_status("Closed")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 0)
# Step - 4: Re-Open Purchase Order
po.update_status("Re-open")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1048,6 +1072,38 @@ class TestPurchaseOrder(FrappeTestCase):
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
)
def test_po_billed_amount_against_return_entry(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
# Create a Purchase Order and Fully Bill it
po = create_purchase_order()
pi = make_pi_from_po(po.name)
pi.insert()
pi.submit()
# Debit Note - 50% Qty & enable updating PO billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_order = 1
pi_return.submit()
# Check if the billed amount reduced
po.reload()
self.assertEqual(po.per_billed, 50)
pi_return.reload()
pi_return.cancel()
# Debit Note - 50% Qty & disable updating PO billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_order = 0
pi_return.submit()
# Check if the billed amount stayed the same
po.reload()
self.assertEqual(po.per_billed, 100)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -1148,6 +1204,7 @@ def create_purchase_order(**args):
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
"against_blanket_order": args.against_blanket_order,
"against_blanket": args.against_blanket,
"material_request": args.material_request,
"material_request_item": args.material_request_item,
},

View File

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

View File

@@ -206,10 +206,30 @@ class RequestforQuotation(BuyingController):
contact.save(ignore_permissions=True)
if rfq_supplier.supplier:
self.update_user_in_supplier(rfq_supplier.supplier, user.name)
if not rfq_supplier.contact:
# return contact to later update, RFQ supplier row's contact
return contact.name
def update_user_in_supplier(self, supplier, user):
"""Update user in Supplier."""
if not frappe.db.exists("Portal User", {"parent": supplier, "user": user}):
supplier_doc = frappe.get_doc("Supplier", supplier)
supplier_doc.append(
"portal_users",
{
"user": user,
},
)
supplier_doc.flags.ignore_validate = True
supplier_doc.flags.ignore_mandatory = True
supplier_doc.flags.ignore_permissions = True
supplier_doc.save()
def create_user(self, rfq_supplier, link):
user = frappe.get_doc(
{
@@ -246,6 +266,10 @@ class RequestforQuotation(BuyingController):
"user_fullname": full_name,
}
)
if not self.email_template:
return
email_template = frappe.get_doc("Email Template", self.email_template)
message = frappe.render_template(email_template.response_, doc_args)
subject = frappe.render_template(email_template.subject, doc_args)

View File

@@ -149,6 +149,33 @@ class TestRequestforQuotation(FrappeTestCase):
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
self.assertEqual(frappe.local.response.type, "pdf")
def test_portal_user_with_new_supplier(self):
supplier_doc = frappe.get_doc(
{
"doctype": "Supplier",
"supplier_name": "Test Supplier for RFQ",
"supplier_group": "_Test Supplier Group",
}
).insert()
self.assertFalse(supplier_doc.portal_users)
rfq = make_request_for_quotation(
supplier_data=[
{
"supplier": supplier_doc.name,
"supplier_name": supplier_doc.supplier_name,
"email_id": "123_testrfquser@example.com",
}
],
do_not_submit=True,
)
for rfq_supplier in rfq.suppliers:
rfq.update_supplier_contact(rfq_supplier, rfq.get_link())
supplier_doc.reload()
self.assertTrue(supplier_doc.portal_users[0].user)
def make_request_for_quotation(**args) -> "RequestforQuotation":
"""

View File

@@ -46,6 +46,7 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
create_gain_loss_journal,
get_account_currency,
get_currency_precision,
get_fiscal_years,
validate_fiscal_year,
)
@@ -216,6 +217,19 @@ class AccountsController(TransactionBase):
)
)
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
# if self.get("is_return") and self.get("return_against"):
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
frappe.msgprint(
_(
"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
).format(
document_type,
get_link_to_form("Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
)
)
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
@@ -333,6 +347,12 @@ class AccountsController(TransactionBase):
ple = frappe.qb.DocType("Payment Ledger Entry")
frappe.qb.from_(ple).delete().where(
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
| (
(ple.against_voucher_type == self.doctype)
& (ple.against_voucher_no == self.name)
& ple.delinked
== 1
)
).run()
frappe.db.sql(
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
@@ -693,7 +713,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
@@ -1116,21 +1136,24 @@ class AccountsController(TransactionBase):
self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True):
party_account = []
if self.doctype == "Sales Invoice":
party_type = "Customer"
party = self.customer
amount_field = "credit_in_account_currency"
order_field = "sales_order"
order_doctype = "Sales Order"
party_account.append(self.debit_to)
else:
party_type = "Supplier"
party = self.supplier
amount_field = "debit_in_account_currency"
order_field = "purchase_order"
order_doctype = "Purchase Order"
party_account.append(self.credit_to)
party_account = get_party_account(
party_type, party=party, company=self.company, include_advance=True
party_account.extend(
get_party_account(party_type, party=party, company=self.company, include_advance=True)
)
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
@@ -1279,10 +1302,12 @@ class AccountsController(TransactionBase):
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
# and below logic is only for such scenarios
if args:
precision = get_currency_precision()
for arg in args:
# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
if (
arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
flt(arg.get("difference_amount", 0), precision) != 0
or flt(arg.get("exchange_gain_loss", 0), precision) != 0
) and arg.get("difference_account"):
party_account = arg.get("account")
@@ -1476,6 +1501,24 @@ class AccountsController(TransactionBase):
x.update({dim.fieldname: self.get(dim.fieldname)})
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):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
remove_from_bank_transaction,
@@ -1488,6 +1531,8 @@ class AccountsController(TransactionBase):
remove_from_bank_transaction(self.doctype, self.name)
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(self)
@@ -2414,27 +2459,20 @@ class AccountsController(TransactionBase):
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
needs_repost = doc_before_update.get("is_opening") != self.is_opening
# Parent Level Accounts excluding party account
fields_to_check += accounting_dimensions
for field in fields_to_check:
if doc_before_update.get(field) != self.get(field):
return True
if not needs_repost:
# Parent Level Accounts excluding party account
fields_to_check += accounting_dimensions
for field in fields_to_check:
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
# Check for child tables
for table in child_tables:
if check_if_child_table_updated(
doc_before_update.get(table), self.get(table), child_tables[table]
):
return True
if not needs_repost:
# Check for child tables
for table in child_tables:
needs_repost = check_if_child_table_updated(
doc_before_update.get(table), self.get(table), child_tables[table]
)
if needs_repost:
break
return needs_repost
return False
@frappe.whitelist()
def repost_accounting_entries(self):
@@ -2671,7 +2709,7 @@ def get_advance_journal_entries(
if order_list:
q = q.where(
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list))
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
)
q = q.orderby(journal_entry.posting_date)
@@ -3463,15 +3501,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
def check_if_child_table_updated(
child_table_before_update, child_table_after_update, fields_to_check
):
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if any field affecting accounting entry is altered
for index, item in enumerate(child_table_after_update):
for field in fields_to_check:
if child_table_before_update[index].get(field) != item.get(field):
return True
fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]
for dimension in accounting_dimensions:
if child_table_before_update[index].get(dimension) != item.get(dimension):
# Check if any field affecting accounting entry is altered
for index, item in enumerate(child_table_before_update):
for field in fields_to_check:
if child_table_after_update[index].get(field) != item.get(field):
return True
return False

View File

@@ -217,8 +217,8 @@ class BuyingController(SubcontractingController):
lc_voucher_data = frappe.db.sql(
"""select sum(applicable_charges), cost_center
from `tabLanded Cost Item`
where docstatus = 1 and purchase_receipt_item = %s""",
d.name,
where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""",
(d.name, self.name),
)
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
@@ -824,7 +824,8 @@ class BuyingController(SubcontractingController):
if self.doctype == "Purchase Invoice" and not self.get("update_stock"):
return
frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name)
asset_movement = frappe.db.get_value("Asset Movement", {"reference_name": self.name}, "name")
frappe.delete_doc("Asset Movement", asset_movement, force=1)
def validate_schedule_date(self):
if not self.get("items"):

View File

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

View File

@@ -28,7 +28,8 @@ class SellingController(StockController):
def validate(self):
super(SellingController, self).validate()
self.validate_items()
self.validate_max_discount()
if not self.get("is_debit_note"):
self.validate_max_discount()
self.validate_selling_price()
self.set_qty_as_per_stock_uom()
self.set_po_nos(for_validate=True)
@@ -599,7 +600,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
)
def set_customer_address(self):
@@ -703,6 +704,9 @@ def set_default_income_account_for_item(obj):
def get_serial_and_batch_bundle(child, parent):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if child.get("use_serial_batch_fields"):
return
if not frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
):

View File

@@ -7,7 +7,7 @@ from typing import List, Tuple
import frappe
from frappe import _, bold
from frappe.utils import cint, flt, get_link_to_form, getdate
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext
from erpnext.accounts.general_ledger import (
@@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_type_of_transaction,
)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -43,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError):
class StockController(AccountsController):
def validate(self):
super(StockController, self).validate()
if self.docstatus == 0:
self.validate_duplicate_serial_and_batch_bundle()
if not self.get("is_return"):
self.validate_inspection()
self.validate_serialized_batch()
@@ -52,6 +58,32 @@ class StockController(AccountsController):
self.validate_internal_transfer()
self.validate_putaway_capacity()
def validate_duplicate_serial_and_batch_bundle(self):
if sbb_list := [
item.get("serial_and_batch_bundle")
for item in self.items
if item.get("serial_and_batch_bundle")
]:
SLE = frappe.qb.DocType("Stock Ledger Entry")
data = (
frappe.qb.from_(SLE)
.select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle)
.where(
(SLE.docstatus == 1)
& (SLE.serial_and_batch_bundle.notnull())
& (SLE.serial_and_batch_bundle.isin(sbb_list))
)
.limit(1)
).run(as_dict=True)
if data:
data = data[0]
frappe.throw(
_("Serial and Batch Bundle {0} is already used in {1} {2}.").format(
frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no
)
)
def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -126,6 +158,131 @@ class StockController(AccountsController):
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if self.get("_action") == "update_after_submit":
return
# To handle test cases
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
return
if not table_name:
table_name = "items"
if self.doctype == "Asset Capitalization":
table_name = "stock_items"
for row in self.get(table_name):
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
self.validate_serial_nos_and_batches_with_bundle(row)
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")
):
row.use_serial_batch_fields = 1
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"
warehouse = row.warehouse
elif table_name == "packed_items":
qty = row.qty
warehouse = row.warehouse
type_of_transaction = "Outward"
if self.is_return:
type_of_transaction = "Inward"
else:
qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty
type_of_transaction = get_type_of_transaction(self, row)
warehouse = (
row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse
)
sn_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": 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,
}
)
else:
row.serial_and_batch_bundle = sn_doc.name
row.db_set(
{
"serial_and_batch_bundle": sn_doc.name,
}
)
def validate_serial_nos_and_batches_with_bundle(self, row):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
throw_error = False
if row.serial_no:
serial_nos = frappe.get_all(
"Serial and Batch Entry", fields=["serial_no"], filters={"parent": row.serial_and_batch_bundle}
)
serial_nos = sorted([cstr(d.serial_no) for d in serial_nos])
parsed_serial_nos = get_serial_nos(row.serial_no)
if len(serial_nos) != len(parsed_serial_nos):
throw_error = True
elif serial_nos != parsed_serial_nos:
for serial_no in serial_nos:
if serial_no not in parsed_serial_nos:
throw_error = True
break
elif row.batch_no:
batches = frappe.get_all(
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle}
)
batches = sorted([d.batch_no for d in batches])
if batches != [row.batch_no]:
throw_error = True
if throw_error:
frappe.throw(
_(
"At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields."
).format(row.idx, row.serial_and_batch_bundle)
)
def set_use_serial_batch_fields(self):
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
for row in self.items:
row.use_serial_batch_fields = 1
def get_gl_entries(
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
):
@@ -860,6 +1017,9 @@ class StockController(AccountsController):
"Stock Reconciliation",
)
if not frappe.get_all("Putaway Rule", limit=1):
return
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
valid_doctype = False

View File

@@ -539,6 +539,10 @@ class SubcontractingController(StockController):
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
if rm_obj.get("qty"):
# Qty field not exists
rm_obj.qty = 0.0
rm_obj.reference_name = item_row.name
if self.doctype == self.subcontract_data.order_doctype:

View File

@@ -98,6 +98,7 @@ class calculate_taxes_and_totals(object):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
"net_rate": item.net_rate or item.rate,
"base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
@@ -1028,7 +1029,7 @@ def get_itemised_tax_breakup_data(doc):
for item_code, taxes in itemised_tax.items():
itemised_tax_data.append(
frappe._dict(
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code, 0), **taxes}
)
)

View File

@@ -56,7 +56,8 @@ class TestAccountsController(FrappeTestCase):
20 series - Sales Invoice against Journals
30 series - Sales Invoice against Credit Notes
40 series - Company default Cost center is unset
50 series - Dimension inheritence
50 series = Journals against Journals
90 series - Dimension inheritence
"""
def setUp(self):
@@ -1108,18 +1109,18 @@ class TestAccountsController(FrappeTestCase):
cr_note.reload()
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_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 0)
self.assertEqual(len(exc_je_for_cr), 0)
# The Credit Note JE is still active and is referencing the sales invoice
# So, outstanding stays the same
# No references, full outstanding
si.reload()
self.assertEqual(si.outstanding_amount, 1)
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
self.assertEqual(si.outstanding_amount, 2)
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
def test_40_cost_center_from_payment_entry(self):
"""
@@ -1271,7 +1272,7 @@ class TestAccountsController(FrappeTestCase):
x.mandatory_for_pl = False
loc.save()
def test_50_dimensions_filter(self):
def test_90_dimensions_filter(self):
"""
Test workings of dimension filters
"""
@@ -1342,7 +1343,7 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
def test_51_cr_note_should_inherit_dimension(self):
def test_91_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
@@ -1384,7 +1385,7 @@ class TestAccountsController(FrappeTestCase):
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
)
def test_52_dimension_inhertiance_exc_gain_loss(self):
def test_92_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate = 80
@@ -1422,7 +1423,7 @@ class TestAccountsController(FrappeTestCase):
),
)
def test_53_dimension_inheritance_on_advance(self):
def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
@@ -1467,3 +1468,70 @@ class TestAccountsController(FrappeTestCase):
pluck="department",
),
)
def test_50_journal_against_journal(self):
# Invoice in Foreign Currency
journal_as_invoice = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=83,
acc2=self.cash,
acc1_amount=1,
acc2_amount=83,
acc2_exc_rate=1,
)
journal_as_invoice.accounts[0].party_type = "Customer"
journal_as_invoice.accounts[0].party = self.customer
journal_as_invoice = journal_as_invoice.save().submit()
# Payment
journal_as_payment = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=75,
acc2=self.cash,
acc1_amount=-1,
acc2_amount=-75,
acc2_exc_rate=1,
)
journal_as_payment.accounts[0].party_type = "Customer"
journal_as_payment.accounts[0].party = self.customer
journal_as_payment = journal_as_payment.save().submit()
# Reconcile the remaining amount
pr = self.create_payment_reconciliation()
# pr.receivable_payable_account = self.debit_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# There should be no outstanding in both currencies
journal_as_invoice.reload()
self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 0.0, 0.0)
# Exchange Gain/Loss Journal should've been created.
exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name)
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(
len(exc_je_for_si), 2
) # payment also has reference. so, there are 2 journals referencing invoice
self.assertEqual(len(exc_je_for_je), 1)
self.assertIn(exc_je_for_je[0], exc_je_for_si)
# Cancel Payment
journal_as_payment.reload()
journal_as_payment.cancel()
journal_as_invoice.reload()
self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 83.0, 1.0)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name)
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_je, [])

View File

@@ -401,7 +401,7 @@ class TestSubcontractingController(FrappeTestCase):
{
"main_item_code": "Subcontracted Item SA4",
"item_code": "Subcontracted SRM Item 3",
"qty": 1.0,
"qty": 3.0,
"rate": 100.0,
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
@@ -914,12 +914,6 @@ def update_item_details(child_row, details):
else child_row.get("consumed_qty")
)
if child_row.serial_no:
details.serial_no.extend(get_serial_nos(child_row.serial_no))
if child_row.batch_no:
details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
if child_row.serial_and_batch_bundle:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
for row in doc.get("entries"):
@@ -928,6 +922,12 @@ def update_item_details(child_row, details):
if row.batch_no:
details.batch_no[row.batch_no] += row.qty * (-1 if doc.type_of_transaction == "Outward" else 1)
else:
if child_row.serial_no:
details.serial_no.extend(get_serial_nos(child_row.serial_no))
if child_row.batch_no:
details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
def make_stock_transfer_entry(**args):

View File

@@ -41,7 +41,9 @@ class SalesPipelineAnalytics(object):
month_list = self.get_month_list()
for month in month_list:
self.columns.append({"fieldname": month, "fieldtype": based_on, "label": month, "width": 200})
self.columns.append(
{"fieldname": month, "fieldtype": based_on, "label": _(month), "width": 200}
)
elif self.filters.get("range") == "Quarterly":
for quarter in range(1, 5):
@@ -156,7 +158,7 @@ class SalesPipelineAnalytics(object):
for column in self.columns:
if column["fieldname"] != "opportunity_owner" and column["fieldname"] != "sales_stage":
labels.append(column["fieldname"])
labels.append(_(column["fieldname"]))
self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"}

View File

@@ -10,7 +10,6 @@ from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, sbool, today
from plaid.errors import ItemError
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
@@ -90,9 +89,15 @@ def add_bank_accounts(response, bank, company):
bank = json.loads(bank)
result = []
default_gl_account = get_default_bank_cash_account(company, "Bank")
if not default_gl_account:
frappe.throw(_("Please setup a default bank account for company {0}").format(company))
parent_gl_account = frappe.db.get_all(
"Account", {"company": company, "account_type": "Bank", "is_group": 1, "disabled": 0}
)
if not parent_gl_account:
frappe.throw(
_(
"Please setup and enable a group account with the Account Type - {0} for the company {1}"
).format(frappe.bold("Bank"), company)
)
for account in response["accounts"]:
acc_type = frappe.db.get_value("Bank Account Type", account["type"])
@@ -108,11 +113,22 @@ def add_bank_accounts(response, bank, company):
if not existing_bank_account:
try:
gl_account = frappe.get_doc(
{
"doctype": "Account",
"account_name": account["name"] + " - " + response["institution"]["name"],
"parent_account": parent_gl_account[0].name,
"account_type": "Bank",
"company": company,
}
)
gl_account.insert(ignore_if_duplicate=True)
new_account = frappe.get_doc(
{
"doctype": "Bank Account",
"bank": bank["bank_name"],
"account": default_gl_account.account,
"account": gl_account.name,
"account_name": account["name"],
"account_type": account.get("type", ""),
"account_subtype": account.get("subtype", ""),

View File

@@ -7,7 +7,6 @@ import unittest
import frappe
from frappe.utils.response import json_handler
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype,
add_account_type,
@@ -72,14 +71,6 @@ class TestPlaidSettings(unittest.TestCase):
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
company = frappe.db.get_single_value("Global Defaults", "default_company")
if frappe.db.get_value("Company", company, "default_bank_account") is None:
frappe.db.set_value(
"Company",
company,
"default_bank_account",
get_default_bank_cash_account(company, "Cash").get("account"),
)
add_bank_accounts(bank_accounts, bank, company)
transactions = {

View File

@@ -42,7 +42,6 @@ setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wiza
before_install = [
"erpnext.setup.install.check_setup_wizard_not_completed",
"erpnext.setup.install.check_frappe_version",
]
after_install = "erpnext.setup.install.after_install"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

81348
erpnext/locale/fa.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ frappe.ui.form.on('Blanket Order', {
},
refresh: function(frm) {
erpnext.hide_company();
erpnext.hide_company(frm);
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({

View File

@@ -41,11 +41,49 @@ class BlanketOrder(Document):
def validate(self):
self.validate_dates()
self.validate_duplicate_items()
self.set_party_item_code()
def validate_dates(self):
if getdate(self.from_date) > getdate(self.to_date):
frappe.throw(_("From date cannot be greater than To date"))
def set_party_item_code(self):
item_ref = {}
if self.blanket_order_type == "Selling":
item_ref = self.get_customer_items_ref()
else:
item_ref = self.get_supplier_items_ref()
if not item_ref:
return
for row in self.items:
row.party_item_code = item_ref.get(row.item_code)
def get_customer_items_ref(self):
items = [d.item_code for d in self.items]
return frappe._dict(
frappe.get_all(
"Item Customer Detail",
filters={"parent": ("in", items), "customer_name": self.customer},
fields=["parent", "ref_code"],
as_list=True,
)
)
def get_supplier_items_ref(self):
items = [d.item_code for d in self.items]
return frappe._dict(
frappe.get_all(
"Item Supplier",
filters={"parent": ("in", items), "supplier": self.supplier},
fields=["parent", "supplier_part_no"],
as_list=True,
)
)
def validate_duplicate_items(self):
item_list = []
for item in self.items:
@@ -90,6 +128,7 @@ def make_order(source_name):
def update_item(source, target, source_parent):
target_qty = source.get("qty") - source.get("ordered_qty")
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)
if item:
target.item_name = item.get("item_name")
@@ -111,6 +150,10 @@ def make_order(source_name):
},
},
)
if target_doc.doctype == "Purchase Order":
target_doc.set_missing_values()
return target_doc

View File

@@ -5,6 +5,7 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext import get_company_currency
from erpnext.stock.doctype.item.test_item import make_item
from .blanket_order import make_order
@@ -90,6 +91,30 @@ class TestBlanketOrder(FrappeTestCase):
frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
po.submit()
def test_party_item_code(self):
item_doc = make_item("_Test Item 1 for Blanket Order")
item_code = item_doc.name
customer = "_Test Customer"
supplier = "_Test Supplier"
if not frappe.db.exists(
"Item Customer Detail", {"customer_name": customer, "parent": item_code}
):
item_doc.append("customer_items", {"customer_name": customer, "ref_code": "CUST-REF-1"})
item_doc.save()
if not frappe.db.exists("Item Supplier", {"supplier": supplier, "parent": item_code}):
item_doc.append("supplier_items", {"supplier": supplier, "supplier_part_no": "SUPP-PART-1"})
item_doc.save()
# Blanket Order for Selling
bo = make_blanket_order(blanket_order_type="Selling", customer=customer, item_code=item_code)
self.assertEqual(bo.items[0].party_item_code, "CUST-REF-1")
bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code)
self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1")
def make_blanket_order(**args):
args = frappe._dict(args)

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2018-05-24 07:20:04.255236",
"doctype": "DocType",
"editable_grid": 1,
@@ -6,6 +7,7 @@
"field_order": [
"item_code",
"item_name",
"party_item_code",
"column_break_3",
"qty",
"rate",
@@ -62,10 +64,17 @@
"fieldname": "terms_and_conditions",
"fieldtype": "Text",
"label": "Terms and Conditions"
},
{
"fieldname": "party_item_code",
"fieldtype": "Data",
"label": "Party Item Code",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-11-18 19:37:46.245878",
"links": [],
"modified": "2024-02-14 18:25:26.479672",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order Item",
@@ -74,5 +83,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -20,6 +20,7 @@ class BlanketOrderItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
party_item_code: DF.Data | None
qty: DF.Float
rate: DF.Currency
terms_and_conditions: DF.Text | None

View File

@@ -1071,8 +1071,7 @@ def get_valuation_rate(data):
frappe.qb.from_(sle)
.select(sle.valuation_rate)
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
.orderby(sle.posting_date, order=frappe.qb.desc)
.orderby(sle.posting_time, order=frappe.qb.desc)
.orderby(sle.posting_datetime, order=frappe.qb.desc)
.orderby(sle.creation, order=frappe.qb.desc)
.limit(1)
).run(as_dict=True)

View File

@@ -239,12 +239,12 @@ class JobCard(Document):
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
def get_overlap_for(self, args, check_next_available_slot=False):
def get_overlap_for(self, args):
time_logs = []
time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot))
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot))
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
if not time_logs:
return {}
@@ -269,7 +269,7 @@ class JobCard(Document):
self.workstation = workstation_time.get("workstation")
return workstation_time
return time_logs[-1]
return time_logs[0]
def has_overlap(self, production_capacity, time_logs):
overlap = False
@@ -308,7 +308,7 @@ class JobCard(Document):
return True
return overlap
def get_time_logs(self, args, doctype, check_next_available_slot=False):
def get_time_logs(self, args, doctype):
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType(doctype)
@@ -318,9 +318,6 @@ class JobCard(Document):
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
]
if check_next_available_slot:
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
query = (
frappe.qb.from_(jctl)
.from_(jc)
@@ -395,18 +392,28 @@ class JobCard(Document):
def validate_overlap_for_workstation(self, args, row):
# get the last record based on the to time from the job card
data = self.get_overlap_for(args, check_next_available_slot=True)
data = self.get_overlap_for(args)
if not self.workstation:
workstations = get_workstations(self.workstation_type)
if workstations:
# Get the first workstation
self.workstation = workstations[0]
if not data:
row.planned_start_time = args.from_time
return
if data:
if data.get("planned_start_time"):
row.planned_start_time = get_datetime(data.planned_start_time)
args.planned_start_time = get_datetime(data.planned_start_time)
else:
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
args.from_time = args.planned_start_time
args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins)
self.validate_overlap_for_workstation(args, row)
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
@@ -748,7 +755,7 @@ class JobCard(Document):
fields=["total_time_in_mins", "hour_rate"],
filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order},
):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.corrective_operation_cost += flt(row.total_time_in_mins / 60) * flt(row.hour_rate)
wo.calculate_operating_cost()
wo.flags.ignore_validate_update_after_submit = True

View File

@@ -7,6 +7,7 @@
"field_order": [
"raw_materials_consumption_section",
"material_consumption",
"get_rm_cost_from_consumption_entry",
"column_break_3",
"backflush_raw_materials_based_on",
"capacity_planning",
@@ -202,13 +203,20 @@
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
"fieldtype": "Check",
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
},
{
"default": "0",
"depends_on": "eval: doc.material_consumption",
"fieldname": "get_rm_cost_from_consumption_entry",
"fieldtype": "Check",
"label": "Get Raw Materials Cost from Consumption Entry"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-28 16:37:44.874096",
"modified": "2024-02-08 19:00:37.561244",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -26,6 +26,7 @@ class ManufacturingSettings(Document):
default_scrap_warehouse: DF.Link | None
default_wip_warehouse: DF.Link | None
disable_capacity_planning: DF.Check
get_rm_cost_from_consumption_entry: DF.Check
job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check
material_consumption: DF.Check

View File

@@ -38,7 +38,8 @@
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "item_name",
@@ -53,7 +54,8 @@
"in_standard_filter": 1,
"label": "For Warehouse",
"options": "Warehouse",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"columns": 1,
@@ -141,7 +143,8 @@
"fieldname": "from_warehouse",
"fieldtype": "Link",
"label": "From Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"search_index": 1
},
{
"fetch_from": "item_code.safety_stock",
@@ -199,7 +202,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-09-12 12:09:08.358326",
"modified": "2024-02-11 16:21:11.977018",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",

View File

@@ -518,6 +518,12 @@ frappe.ui.form.on("Production Plan Sales Order", {
}
});
frappe.ui.form.on("Production Plan Sub Assembly Item", {
fg_warehouse(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
},
})
frappe.tour['Production Plan'] = [
{
fieldname: "get_items_from",

View File

@@ -298,7 +298,8 @@
"no_copy": 1,
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "amended_from",
@@ -420,9 +421,11 @@
"fieldtype": "Column Break"
},
{
"description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",
"fieldname": "sub_assembly_warehouse",
"fieldtype": "Link",
"label": "Sub Assembly Warehouse",
"mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1",
"options": "Warehouse"
},
{
@@ -436,7 +439,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-12-26 16:31:13.740777",
"modified": "2024-02-27 13:34:20.692211",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -312,9 +312,10 @@ class ProductionPlan(Document):
so_item.parent,
so_item.item_code,
so_item.warehouse,
(
(so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
).as_("pending_qty"),
so_item.qty,
so_item.work_order_qty,
so_item.delivered_qty,
so_item.conversion_factor,
so_item.description,
so_item.name,
so_item.bom_no,
@@ -337,6 +338,11 @@ class ProductionPlan(Document):
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")
packed_items_query = (
@@ -646,7 +652,10 @@ class ProductionPlan(Document):
"project": self.project,
}
key = (d.item_code, d.sales_order, d.warehouse)
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
if self.combine_items:
key = (d.item_code, d.sales_order, d.warehouse)
if not d.sales_order:
key = (d.name, d.item_code, d.warehouse)
@@ -885,8 +894,8 @@ class ProductionPlan(Document):
sub_assembly_items_store = [] # temporary store to process all subassembly items
for row in self.po_items:
if self.skip_available_sub_assembly_item and not row.warehouse:
frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx))
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
@@ -896,15 +905,24 @@ class ProductionPlan(Document):
bom_data = []
warehouse = (
(self.sub_assembly_warehouse or row.warehouse)
if self.skip_available_sub_assembly_item
else None
)
warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
if not sub_assembly_items_store and self.skip_available_sub_assembly_item:
message = (
_(
"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}."
).format(self.sub_assembly_warehouse)
+ "<br><br>"
)
message += _(
"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox."
)
frappe.msgprint(message, title=_("Note"))
if self.combine_sub_items:
# Combine subassembly items
sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
@@ -917,15 +935,19 @@ class ProductionPlan(Document):
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
"Modify bom_data, set additional details."
is_group_warehouse = frappe.db.get_value("Warehouse", self.sub_assembly_warehouse, "is_group")
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or (
"Subcontract" if data.is_sub_contracted_item else "In House"
)
if not is_group_warehouse:
data.fg_warehouse = self.sub_assembly_warehouse
def set_default_supplier_for_subcontracting_order(self):
items = [
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
@@ -1334,10 +1356,10 @@ def get_sales_orders(self):
)
date_field_mapper = {
"from_date": self.from_date >= so.transaction_date,
"to_date": self.to_date <= so.transaction_date,
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
"from_date": so.transaction_date >= self.from_date,
"to_date": so.transaction_date <= self.to_date,
"from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
"to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
}
for field, value in date_field_mapper.items():
@@ -1469,7 +1491,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details = frappe._dict()
sub_assembly_items = {}
if doc.get("skip_available_sub_assembly_item"):
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
for d in doc.get("sub_assembly_items"):
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
@@ -1498,19 +1520,17 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
if bom_no:
if (
data.get("include_exploded_items")
and doc.get("sub_assembly_items")
and doc.get("skip_available_sub_assembly_item")
):
item_details = get_raw_materials_of_sub_assembly_items(
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"):
item_details = {}
if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_sub_assembly_items(
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
elif data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM
@@ -1683,34 +1703,37 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
if warehouse:
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
if bin_dict and bin_dict[0].projected_qty > 0:
if bin_dict[0].projected_qty > stock_qty:
continue
else:
stock_qty = stock_qty - bin_dict[0].projected_qty
for _bin_dict in bin_details:
if _bin_dict.projected_qty > 0:
if _bin_dict.projected_qty > stock_qty:
stock_qty = 0
continue
else:
stock_qty = stock_qty - _bin_dict.projected_qty
bom_data.append(
frappe._dict(
{
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
}
if stock_qty > 0:
bom_data.append(
frappe._dict(
{
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
}
)
)
)
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
def set_default_warehouses(row, default_warehouses):
@@ -1762,23 +1785,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
return reserved_qty_for_production_plan - reserved_qty_for_production
@frappe.request_cache
def get_non_completed_production_plans():
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Item")
query = (
return (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(table.name)
.distinct()
.where(
(table.docstatus == 1)
& (table.status.notin(["Completed", "Closed"]))
& (child.planned_qty > child.ordered_qty)
)
).run(as_dict=True)
return list(set([d.name for d in query]))
).run(pluck="name")
def get_raw_materials_of_sub_assembly_items(

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